def _unpack_(obj): """ The inverse of `_pack_`; creates a Decision from a simple object. Note that the choice, option, and outcomes of this decision are new, disentangled objects, so this it isn't terribly memory efficient to pack and unpack Decision objects, and true linkage shouldn't be assumed. """ return Decision( unpack(obj["choice"], choice.Choice), unpack(obj["option"], choice.Option), [ unpack(o, choice.Outcome) for o in obj["outcomes"] ], Decision.unpack_decision_model(obj["prospective_impressions"]), [ Decision.unpack_decision_model(dm) for dm in obj["factored_decision_models"] ] if obj["factored_decision_models"] else None, { gn: unpack(obj["goal_relevance"][gn], Salience) for gn in obj["goal_relevance"] } if obj["goal_relevance"] else None, { gn: [ unpack(o, perception.Retrospective) for o in obj["retrospective_impressions"][gn] ] for gn in obj["retrospective_impressions"] } if obj["retrospective_impressions"] else None )
def _unpack_(obj): """ Delegates unpacking to the relevant subclass. """ action = obj.split(' ')[0] if action == "set": return unpack(obj, SetValue) elif action == "add": return unpack(obj, IncrementValue) elif action == "invert": return unpack(obj, InvertValue)
def _unpack_(obj): """ The inverse of `_pack_`; constructs an instance from a simple object (e.g., one produced by json.loads). """ return Outcome( obj["name"], obj["effects"], unpack(obj["salience"], Salience), unpack(obj["apparent_likelihood"], Certainty), unpack(obj["actual_likelihood"], Certainty) \ if "actual_likelihood" in obj else None )
def test_packable(): test_stuff = cls._pack_.__doc__.split("```") tinst = eval(utils.dedent(test_stuff[1])) tobj = eval(utils.dedent(test_stuff[2])) uinst = unpack(tobj, cls) pobj = pack(tinst) urec = unpack(pobj, cls) prec = pack(uinst) assert tinst == uinst, ( ( "Unpacked object doesn't match eval'd version:\n```\n{}\n```\n{}\n```" "\nDifferences:\n {}" ).format(str(tinst), str(uinst), "\n ".join(diff(tinst, uinst))) ) assert pobj == tobj, ( ( "Packed object doesn't match given:\n```\n{}\n```\n{}\n```" "\nDifferences:\n {}" ).format( str(tobj), str(pobj), "\n ".join(diff(tobj, pobj)) ) ) assert tinst == urec, ( ( "Pack/unpacked object doesn't match:\n```\n{}\n```\n{}\n```" "\nDifferences:\n {}" ).format( str(tinst), str(urec), "\n ".join(diff(tinst, urec)) ) ) assert tobj == prec, ( ( "Unpack/packed object doesn't match:\n```\n{}\n```\n{}\n```" "\nDifferences:\n {}" ).format( str(tobj), str(prec), "\n ".join(diff(tobj, prec)) ) ) return True
def _unpack_(obj): """ The inverse of `_pack_`; creates a Percept from a simple object. This method handles unpacking for subtypes Prospective and Retrospective as well. """ if obj["type"] == "Percept": return Percept( obj["goal"], obj["choice"], obj["option"], obj["outcome"], unpack(obj["valence"], Valence), unpack(obj["salience"], Salience), ) elif obj["type"] == "Prospective": return Prospective( obj["goal"], obj["choice"], obj["option"], obj["outcome"], unpack(obj["valence"], Valence), unpack(obj["salience"], Salience), certainty=unpack(obj["certainty"], Certainty) ) elif obj["type"] == "Retrospective": return Retrospective( obj["goal"], obj["choice"], obj["option"], obj["outcome"], unpack(obj["valence"], Valence), unpack(obj["salience"], Salience), prospective=unpack(obj["prospective"], Prospective) )
def _unpack_(obj): """ Inverse of `_pack_`; creates an instance from a simple object. """ return ModeOfEngagement(obj["name"], [unpack(g, PlayerGoal) for g in obj["goals"]], obj["priorities"])
def _unpack_(obj): """ The inverse of `_pack_`; takes a simple object and returns a Choice instance. """ opts = obj["options"] return Choice(obj["name"], [unpack(opts[k], Option) for k in opts])
def _unpack_(obj): """ The inverse of `_pack_`; takes a simple object (e.g., from json.loads) and returns an Option instance. """ return Option(obj["name"], [unpack(o, Outcome) for o in obj["outcomes"]])
def _unpack_(obj): """ Creates a StoryNode from a simple object (see packable.py). Note that the unpacked story node will not have a module finder. """ return Story( obj["title"], obj["author"], obj["start"], {k: unpack(obj["nodes"][k], StoryNode) for k in obj["nodes"]}, modules=obj["modules"] if "modules" in obj else None, setup=obj["setup"] if "setup" in obj else None)
def _unpack_(obj): """ The inverse of `_pack_`; constructs an instance from a simple object (e.g., one produced by json.loads). """ return PlayerModel( obj["name"], unpack(obj["decision_method"], decision.DecisionMethod), { key: unpack(val, engagement.ModeOfEngagement) for (key, val) in obj["modes"].items() }, unpack(obj["priority_method"], engagement.PriorityMethod) \ if "priority_method" in obj else engagement.PriorityMethod.softpriority, mode_ranking=obj["mode_ranking"] if "mode_ranking" in obj else None, mode_adjustments=obj["mode_adjustments"] \ if "mode_adjustments" in obj else None, goal_adjustments=obj["goal_adjustments"] \ if "goal_adjustments" in obj else None, goal_overrides = obj["goal_overrides"] \ if "goal_overrides" in obj else None )
def recall_story(self, title, author=None, is_module=False): """ Looks up a story (or module) in the database and returns it, or None if no such story exists. If multiple stories match (when author is given as None), a list will be returned. """ cached = self.find_cached(title, author, is_module) if cached: return cached table = "modules" if is_module else "stories" cur = self.connection.cursor() if author is None: cur.execute( "SELECT package FROM {} WHERE title = ?;".format(table), (title.title(), )) else: cur.execute( "SELECT package FROM {} WHERE title = ? AND author = ?;". format(table), (title.title(), author.title())) rows = cur.fetchall() if len(rows) < 1: return None if len(rows) > 1: # Don't cache anything in this case result = [unpack(json.loads(r["package"]), Story) for r in rows] result.set_module_finder(self.find_module) else: result = unpack(json.loads(rows[0]["package"]), Story) result.set_module_finder(self.find_module) if is_module: self.cache_module(result) else: self.cache_story(result) return result
def unpack_decision_model(dm): """ Helper method for _unpack_ that unpacks a decision model (a mapping from option names to mappings from goal names to lists of Prospective impressions). """ return { optname: { goalname: [ unpack(pri, perception.Prospective) for pri in dm[optname][goalname] ] for goalname in dm[optname] } for optname in dm } if dm else None
def load_story_from_file(filename, fmt="auto"): """ Loads a Story object from a filename. Prints a warning and does nothing if the file cannot be loaded. The fmt argument decides which format to load, "json" for JSON format, and "story" for Markdown-like format, or "auto" to choose based on the start of the file (just checks whether the first two non-whitespace characters are '{' and '"', in which case it uses JSON). """ with open(filename, 'r') as fin: try: raw = fin.read() fmtstr = fmt if fmt == "auto": if raw[:1024].translate({ord(c): None for c in ' \t\n\r'}).startswith('{"'): fmt = "json" fmtstr = "json (detected)" else: fmt = "story" fmtstr = "story (default)" if fmt == "json": st = unpack(json.loads(raw), story.Story) elif fmt == "story": st = parse.parse_story(raw) else: raise ValueError("Invalid story format '{}'.".format(fmtstr)) except Exception as e: print( "Warning: file '{}' could not be read as a Story in format '{}'." .format(filename, fmtstr)) if config.DEBUG: print(e, file=sys.stderr) traceback.print_tb(e.__traceback__, file=sys.stderr) return None return st
def package(story): with open("story_engine.js", 'r') as fin: ejs = fin.read() return TEMPLATE.format(title=story.name.title(), story_content=pack(story), engine=ejs) if __name__ == "__main__": import sys, os if len(sys.argv) < 2: print("Error: Missing argument 'target stories'", file=sys.stderr) for tf in sys.argv[1:]: with open(tf, 'r') as fin: html = package(unpack(json.load(fin), Story)) basename = os.path.basename(tf) output = "{}.html".format(basename) i = 0 while os.path.exists(output): old = output output = "{}.{}.html".format(basename, i) print( "Warning: target file '{}' already exists, using '{}' instead..." .format(old, output), file=sys.stderr) i += 1 with open(output, 'w') as fout: fout.write(html)
def parse_first_node(src): """ Takes a chunk of story source and parses the first node from it (which should start immediately at the top of the source modulo comments and empty space). Returns (None, src) if there isn't a node definition present, or the parsed node plus the remaining source if there is. Example: ``` StoryNode( "at_the_beach", "You walk along the beach, watching seabirds dance with the waves. The sea calls to you, but you should go home.", { "The sea": ["wading_out", "(set: mood | desolate)"], "go home": ["back_home", "(set: mood | warm)"] } ) ``` # at_the_beach You walk along the beach, watching seabirds dance with the waves. [[The sea|wading_out|(set: mood | desolate)]] calls to you, but you should [[go home|back_home|(set: mood | warm)]]. ``` """ src = src.strip() first = NODE_START.search(src) # No node found: if not first or first.start() != 0: return (None, src) node = {} # Search for the next node, or else use the end of the source: second = NODE_START.search(src, pos=first.end()) if second: node_end = second.start() else: node_end = len(src) # Grab the name and content: node["name"] = first.group(1) content = reflow(src[first.end():node_end]) leftovers = src[node_end:] # Find and transform all links in the text, replacing them with their display # text and creating link entries for each. Note that if the display text is # not unique, all instances within the node will be highlighted. link_extents = [] i = 0 while i < len(content) - 1: i += 1 # we intentionally skip i = 0 since we match the second '[' if content[i - 1:i + 1] == "[[": try: close = utils.matching_brace(content, i, '[', ']') except utils.UnmatchedError: # TODO: Print a warning here? continue # Push to create reverse ordering so link replacement doesn't change # indices of earlier link_extents. link_extents.insert(0, (i + 1, close)) # jump ahead to the end of this link i = close node["successors"] = {} for start, end in link_extents: # Ordering is last-to-first, so that replacement here doesn't affect # indices of earlier links. contents = content[start:end] contents = contents.strip() # TODO: Error if this strip results in an empty string? # First, figure out display text: if contents[0] == '"': end, val = utils.string_literal(contents, 0, '"') if '|' in contents[end:]: display_end = contents.index('|', end) display = val + contents[end:display_end] else: display_end = len(contents) display = val + contents[end:] else: try: display_end = contents.index('|') display = contents[:display_end] except ValueError: display_end = len(contents) display = contents # Next figure out the link destination: if display_end < len(contents): try: dest_end = contents.index('|', display_end + 1) destination = contents[display_end + 1:dest_end].strip() if destination == "": destination = display except ValueError: dest_end = len(contents) destination = contents[display_end + 1:].strip() else: destination = display dest_end = len(contents) # Finally, grab the transition text: transition = contents[dest_end + 1:].strip() # Revise the source to replace the link literal with its display text: content = content[:start - 2] + display + content[end + 2:] # Add to our links information: if transition: node["successors"][display] = [destination, transition] else: node["successors"][display] = [destination, ""] # Put the revised content into our node: node["content"] = content return (unpack(node, StoryNode), leftovers)
def _unpack_(obj): bits = obj.split(' ') if bits[0] != "set": raise ValueError( "Can't unpack '{}' as a SetValue change.".format(obj)) return SetValue(bits[1], unpack(json.loads(' '.join(bits[2:]))))
def main(models_str, choices_str, trace_strings): """ Takes JSON strings specifying models and choices and a list of JSON strings specifying traces and runs analyze_trace on each trace before summarizing results. """ packed_models = json.loads(models_str) models = [] for i, obj in enumerate(packed_models): try: models.append(unpack(obj, player.PlayerModel)) except Exception as e: raise ValueError( "Failed to unpack player model #{}".format(i)) from e packed_choices = json.loads(choices_str) choices = [] for i, obj in enumerate(packed_choices): try: choices.append(unpack(obj, choice.Choice)) except Exception as e: raise ValueError("Failed to unpack choice #{}".format(i)) from e traces = {fn: json.loads(trace_strings[fn]) for fn in trace_strings} results = {tr: analyze_trace(models, choices, traces[tr]) for tr in traces} all_decisions = {} for ch in choices: cn = ch.name if cn not in all_decisions: all_decisions[cn] = {} for tr in traces: for tch in traces[tr]: add_to = all_decisions[tch[0]] if tch[1] in add_to: add_to[tch[1]] += 1 else: add_to[tch[1]] = 1 overall_per_choice = combine_per_choice( *[res[2] for res in results.values()]) # TODO: More formatting here for tn in results: res = results[tn] print("Averages for {}:".format(tn)) for model in models: pmn = model.name print(" {:.3g} {}".format(res[0][pmn], pmn)) print('-' * 80) for cn in overall_per_choice: times, agreements = overall_per_choice[cn] print("Choice: '{}' (seen {} times)".format(cn, times)) print("Decisions:") for dc in all_decisions[cn]: print(" {}: {}".format(dc, all_decisions[cn][dc])) print("Model agreement:") for pmn in agreements: print(" {}: {:.3g}".format(pmn, agreements[pmn])) print('-' * 80)