def from_url(qparams): params = SeedGenParams() params.seed = qparams.get("seed") if not params.seed: log.error("No seed in %r! returning None" % qparams) return None params.variations = enums_from_strlist(Variation, qparams.getall("var")) params.logic_paths = enums_from_strlist(LogicPath, qparams.getall("path")) if not params.logic_paths: log.error("No logic paths in %r! returning None" % qparams) return None params.key_mode = KeyMode(qparams.get("key_mode", "Clues")) params.path_diff = PathDifficulty(qparams.get("path_diff", "Normal")) params.exp_pool = int(qparams.get("exp_pool", 10000)) params.balanced = qparams.get("gen_mode") != "Classic" params.players = int(qparams.get("players", 1)) params.tracking = qparams.get("tracking") != "Disabled" params.warmth = WarmthFragmentOptions.from_url(qparams) params.sync = MultiplayerOptions.from_url(qparams) raw_fass = qparams.get("fass") if raw_fass: params.placements = [] for fass in raw_fass.split("|"): loc, _, item = fass.partition(":") stuff = [Stuff(code=item[:2], id=item[2:], player="")] params.placements.append(Placement(location=loc, zone="", stuff=stuff)) return params.put()
def from_json(json): params = SeedGenParams() params.seed = str(json.get("seed")) if not params.seed: log.error("No seed in %r! returning None" % json) return None params.variations = enums_from_strlist(Variation, json.get("variations", [])) params.logic_paths = enums_from_strlist(LogicPath, json.get("paths", [])) if not params.logic_paths: log.error("No logic paths in %r! returning None" % json) return None params.key_mode = KeyMode(json.get("keyMode", "Clues")) params.path_diff = PathDifficulty(json.get("pathDiff", "Normal")) params.exp_pool = json.get("expPool", 10000) params.balanced = json.get("fillAlg") != "Classic" params.players = json.get("players", 1) params.tracking = json.get("tracking") params.frag_count = json.get("fragCount", 30) params.frag_req = json.get("fragReq", 20) params.relic_count = json.get("relicCount", 8) params.cell_freq = json.get("cellFreq", 256) params.sync = MultiplayerOptions.from_json(json) params.sense = json.get("senseData") params.item_pool = json.get("itemPool", {}) params.bingo_lines = json.get("bingoLines", 3) params.pool_preset = json.get("selectedPool", "Standard") params.placements = [ Placement( location=fass["loc"], zone="", stuff=[Stuff(code=fass["code"], id=fass["id"], player="")]) for fass in json.get("fass", []) ] return params.put()
def from_url(qparams): params = SeedGenParams() params.seed = qparams.get("seed") if not params.seed: log.error("No seed in %r! returning None" % qparams) return None params.variations = enums_from_strlist(Variation, qparams.getall("var")) params.logic_paths = enums_from_strlist(LogicPath, qparams.getall("path")) if not params.logic_paths: log.error("No logic paths in %r! returning None" % qparams) return None params.key_mode = KeyMode(qparams.get("key_mode", "Clues")) params.path_diff = PathDifficulty(qparams.get("path_diff", "Normal")) params.exp_pool = int(qparams.get("exp_pool", 10000)) params.balanced = qparams.get("gen_mode") != "Classic" params.players = int(qparams.get("players", 1)) params.tracking = qparams.get("tracking") != "Disabled" params.frag_count = int(qparams.get("frags", 30)) params.frag_req = int(qparams.get("frags_req", 20)) params.relic_count = int(qparams.get("relics", 8)) params.cell_freq = int(qparams.get("cell_freq", 256)) params.sync = MultiplayerOptions.from_url(qparams) params.sense = qparams.get("sense") params.pool_preset = qparams.get("pool_preset", "Standard").title() params.item_pool = {} raw_pool = qparams.get("item_pool") if raw_pool: for itemcnt in raw_pool.split("|"): item, _, count = itemcnt.partition(":") params.item_pool[item] = int(count) else: if Variation.EXTRA_BONUS_PICKUPS in params.variations or params.pool_preset == "Extra Bonus": params.pool_preset = "Extra Bonus" params.item_pool = { "TP|Grove": [1], "TP|Swamp": [1], "TP|Grotto": [1], "TP|Valley": [1], "TP|Sorrow": [1], "TP|Ginso": [1], "TP|Horu": [1], "TP|Forlorn": [1], "HC|1": [12], "EC|1": [14], "AC|1": [33], "RP|RB/0": [3], "RP|RB/1": [3], "RB|6": [5], "RB|9": [1], "RB|10": [1], "RB|11": [1], "RB|12": [3], "RB|37": [3], "RB|13": [3], "RB|15": [3], "RB|31": [1], "RB|32": [1], "RB|33": [3], "BS|*": [4], "WP|*": [4, 8], } elif params.pool_preset == "Bonus Lite": params.item_pool = { "TP|Grove": [1], "TP|Swamp": [1], "TP|Grotto": [1], "TP|Valley": [1], "TP|Sorrow": [1], "TP|Ginso": [1], "TP|Horu": [1], "TP|Forlorn": [1], "HC|1": [12], "EC|1": [14], "AC|1": [33], "RB|0": [3], "RB|1": [3], "RB|6": [5], "RB|9": [1], "RB|10": [1], "RB|11": [1], "RB|12": [3], "RB|37": [3], "RB|13": [3], "RB|15": [3], "RB|31": [1], "RB|32": [1], "RB|33": [3], "RB|36": [1], "WP|*": [4, 8], } elif params.pool_preset == "Competitive": params.item_pool = { "TP|Grove": [1], "TP|Swamp": [1], "TP|Grotto": [1], "TP|Valley": [1], "TP|Sorrow": [1], "TP|Forlorn": [1], "HC|1": [12], "EC|1": [14], "AC|1": [33], "RB|0": [3], "RB|1": [3], "RB|6": [3], "RB|9": [1], "RB|10": [1], "RB|11": [1], "RB|12": [1], "RB|13": [3], "RB|15": [3], } elif params.pool_preset == "Hard": params.item_pool = { "TP|Grove": [1], "TP|Swamp": [1], "TP|Grotto": [1], "TP|Valley": [1], "TP|Sorrow": [1], "EC|1": [3] } else: params.pool_preset = "Standard" params.item_pool = { "TP|Grove": [1], "TP|Swamp": [1], "TP|Grotto": [1], "TP|Valley": [1], "TP|Sorrow": [1], "TP|Ginso": [1], "TP|Horu": [1], "TP|Forlorn": [1], "HC|1": [12], "EC|1": [14], "AC|1": [33], "RB|0": [3], "RB|1": [3], "RB|6": [3], "RB|9": [1], "RB|10": [1], "RB|11": [1], "RB|12": [1], "RB|13": [3], "RB|15": [3], } raw_fass = qparams.get("fass") if raw_fass: params.placements = [] for fass in raw_fass.split("|"): loc, _, item = fass.partition(":") stuff = [Stuff(code=item[:2], id=item[2:], player="")] params.placements.append( Placement(location=loc, zone="", stuff=stuff)) return params.put()
def get_keymode(self): return KeyMode(self.str_keymode)
def get_keymode(self): return KeyMode.from_ndb(self.ndb_keymode) def set_keymode(self, key_mode): self.ndb_keymode = key_mode.to_ndb()
def from_cli(self): parser = argparse.ArgumentParser() parser.add_argument("--output-dir", help="directory to put the seeds in", type=str, default=".") parser.add_argument( "--preset", help="Choose a preset group of paths for the generator to use") parser.add_argument( "--custom-logic", help= "Customize paths that the generator will use, comma-separated: %s" % ", ".join(vals(LogicPath))) parser.add_argument("--seed", help="Seed value (default 'test')", type=str, default="test") parser.add_argument( "--keymode", help= """Changes how the dungeon keys (The Water Vein, Gumon Seal, and Sunstone) are handled: Default: The dungeon keys are placed without any special consideration. Clues: For each 3 trees visited, the zone of a random dungeon key will be revealed Shards: The dungeon keys will be awarded after 3/5 shards are found LimitKeys: The Water Vein, Gumon Seal, and Sunstone will only appear at skill trees or event sources Free: The dungeon keys are given to the player upon picking up the first Energy Cell. """, type=str) # variations parser.add_argument("--hard", help="Enable hard mode", action="store_true") parser.add_argument("--ohko", help="Enable one-hit-ko mode", action="store_true") parser.add_argument("--zeroxp", help="Enable 0xp mode", action="store_true") parser.add_argument( "--starved", help= "Reduces the rate at which skills will appear when not required to advance", action="store_true") parser.add_argument( "--tp-starved", help= "Reduces the rate at which teleporters will appear early game when not required to advance", action="store_true") parser.add_argument( "--wall-starved", help= "Reduces the rate at which WallJump and Climb will appear early game when not required to advance", action="store_true") parser.add_argument( "--non-progressive-mapstones", help= "Map Stones will retain their behaviour from before v1.2, having their own unique drops", action="store_true") parser.add_argument( "--force-trees", help= "Prevent Ori from entering the final escape room until all skill trees have been visited", action="store_true") parser.add_argument( "--force-mapstones", help= "Prevent Ori from entering the final escape room until all mapstone altars have been activated", action="store_true") parser.add_argument("--entrance", help="Randomize entrances", action="store_true") parser.add_argument("--closed-dungeons", help="deactivate open mode within dungeons", action="store_true") parser.add_argument("--open-world", help="Activate open mode on the world map", action="store_true") parser.add_argument( "--bonus-pickups", help= "Adds some extra bonus pickups not balanced for competitive play", action="store_true") parser.add_argument( "--easy", help= "Add an extra copy of double jump, bash, stomp, glide, charge jump, dash, grenade, water, and wind", action="store_true") parser.add_argument( "--strict-mapstones", help= "Require a mapstone to be placed when a map monument becomes accessible", action="store_true") parser.add_argument( "--world-tour", help= "Prevent Ori from entering the final escape until collecting one relic from each of the zones in the world. Recommended default: 8", type=int) parser.add_argument( "--warmth-frags", help= "Prevent Ori from entering the final escape until collecting some number of warmth fragments. Recommended default: 40", type=int) # misc parser.add_argument( "--verbose-paths", help="print every logic path in the flagline for debug purposes", action="store_true") parser.add_argument("--exp-pool", help="Size of the experience pool (default 10000)", type=int, default=10000) parser.add_argument( "--extra-frags", help= """Sets the number of extra warmth fragments. Total frag number is still the value passed to --warmth-frags; --warmth-frags 40 --extra-frags 10 will place 40 total frags, 30 of which will be required to finish""", type=int, default=10) parser.add_argument( "--prefer-path-difficulty", help= "Increase the chances of putting items in more convenient (easy) or less convenient (hard) locations", choices=["easy", "hard"]) parser.add_argument( "--balanced", help= "Reduce the value of newly discovered locations for progression placements", action="store_true") parser.add_argument( "--force-cells", help= "Force health and energy cells to appear every N pickups, if they don't randomly", type=int, default=256) # anal TODO: IMPL parser.add_argument( "--analysis", help="Report stats on the skill order for all seeds generated", action="store_true") parser.add_argument( "--loc-analysis", help="Report stats on where skills are placed over multiple seeds", action="store_true") parser.add_argument("--count", help="Number of seeds to generate (default 1)", type=int, default=1) # sync parser.add_argument("--players", help="Player count for paired randomizer", type=int, default=1) parser.add_argument( "--tracking", help="Place a sync ID in a seed for tracking purposes", action="store_true") parser.add_argument( "--sync-id", help="Team identifier number for paired randomizer", type=int) parser.add_argument( "--shared-items", help= "What will be shared by sync, comma-separated: skills,worldevents,misc,teleporters,upgrades", default="skills,worldevents") parser.add_argument( "--share-mode", help= "How the server will handle shared pickups, one of: shared,swap,split,none", default="shared") parser.add_argument( "--cloned", help="Make a split cloned seed instead of seperate seeds", action="store_true") parser.add_argument( "--teams", help= "Cloned seeds only: define teams. Format: 1|2,3,4|5,6. Each player must appear once", type=str) parser.add_argument( "--hints", help= "Cloned seeds only: display a hint with the item category on a shared location instead of 'Warmth Returned'", action="store_true") parser.add_argument( "--do-reachability-analysis", help= "Analyze how many locations are opened by various progression items in various inventory states", action="store_true") args = parser.parse_args() """ path_diff = property(get_pathdiff, set_pathdiff) exp_pool = ndb.IntegerProperty(default=10000) balanced = ndb.BooleanProperty(default=True) tracking = ndb.BooleanProperty(default=True) players = ndb.IntegerProperty(default=1) sync = ndb.LocalStructuredProperty(MultiplayerOptions) frag_count = ndb.IntegerProperty(default=40) frag_extra = ndb.IntegerProperty(default=10) cell_freq = ndb.IntegerProperty(default=256) """ self.seed = args.seed if args.preset: self.logic_paths = presets[args.preset.capitalize()] elif args.custom_logic: self.logic_paths = enums_from_strlist(LogicPath, args.custom_logic.split(",")) else: self.logic_paths = presets["Standard"] self.key_mode = KeyMode.mk(args.keymode) or KeyMode.NONE # variations (help) varMap = { "zeroxp": "0XP", "non_progressive_mapstones": "NonProgressMapStones", "ohko": "OHKO", "force_trees": "ForceTrees", "starved": "Starved", "force_mapstones": "ForceMapStones", "entrance": "Entrance", "open_world": "OpenWorld", "easy": "DoubleSkills", "strict_mapstones": "StrictMapstones", "warmth_frags": "WarmthFrags", "world_tour": "WorldTour", "closed_dungeons": "ClosedDungeons", "tp_starved": "TPStarved", "wall_starved": "WallStarved" } self.variations = [] for argName, flagStr in varMap.iteritems(): if getattr(args, argName, False): v = Variation.mk(flagStr) if v: self.variations.append(v) else: log.warning("Failed to make a Variation from %s" % flagStr) if Variation.WORLD_TOUR in self.variations: self.relic_count = args.world_tour if Variation.WARMTH_FRAGMENTS in self.variations: self.frag_count = args.warmth_frags self.frag_extra = args.extra_frags #misc self.exp_pool = args.exp_pool if args.prefer_path_difficulty: if args.prefer_path_difficulty == "easy": self.path_diff = PathDifficulty.EASY else: self.path_diff = PathDifficulty.HARD else: self.path_diff = PathDifficulty.NORMAL self.balanced = args.balanced or False self.cell_freq = args.force_cells self.players = args.players self.tracking = args.tracking or False self.sync = CLIMultiOptions() if Variation.EXTRA_BONUS_PICKUPS in self.variations: self.pool_preset = "Extra Bonus" self.item_pool = { "TP|Grove": [1], "TP|Swamp": [1], "TP|Grotto": [1], "TP|Valley": [1], "TP|Sorrow": [1], "TP|Ginso": [1], "TP|Horu": [1], "TP|Forlorn": [1], "HC|1": [12], "EC|1": [14], "AC|1": [33], "RB|0": [3], "RB|1": [3], "RB|6": [5], "RB|9": [1], "RB|10": [1], "RB|11": [1], "RB|12": [5], "RB|13": [3], "RB|15": [3], "RB|31": [1], "RB|32": [1], "RB|33": [3], "BS|*": [4], "WP|*": [4, 8], } else: self.pool_preset = "Standard" self.item_pool = { "TP|Grove": [1], "TP|Swamp": [1], "TP|Grotto": [1], "TP|Valley": [1], "TP|Sorrow": [1], "TP|Ginso": [1], "TP|Horu": [1], "TP|Forlorn": [1], "HC|1": [12], "EC|1": [14], "AC|1": [33], "RB|0": [3], "RB|1": [3], "RB|6": [3], "RB|9": [1], "RB|10": [1], "RB|11": [1], "RB|12": [1], "RB|13": [3], "RB|15": [3], } if self.players > 1 or self.tracking: self.sync_id = args.sync_id or int(time.time() * 1000 % 1073741824) if self.players > 1: self.sync.enabled = True self.sync.mode = MultiplayerGameType.mk( args.share_mode) or MultiplayerGameType.SIMUSOLO self.sync.shared = enums_from_strlist(ShareType, args.shared_items.split(",")) self.sync.cloned = args.cloned or False if self.sync.cloned: self.sync.hints = args.hints or False if args.teams: cnt = 1 self.sync.teams = {} for team in args.teams.split("|"): self.sync.teams[cnt] = [ int(p) for p in team.split(",") ] cnt += 1 # todo: respect these LMAO self.do_analysis = args.analysis self.do_loc_analysis = args.loc_analysis self.repeat_count = args.count base_seed = self.seed[:] if self.do_analysis: key_items = [ "WallJump", "ChargeFlame", "DoubleJump", "Bash", "Stomp", "Glide", "Climb", "ChargeJump", "Dash", "Grenade", "GinsoKey", "ForlornKey", "HoruKey", "Water", "Wind", "TPForlorn", "TPGrotto", "TPSorrow", "TPGrove", "TPSwamp", "TPValley", "TPGinso", "TPHoru" ] info_by_group = defaultdict(defaultgroup) if self.do_loc_analysis: self.locationAnalysis = {} self.itemsToAnalyze = { "WallJump": 0, "ChargeFlame": 0, "DoubleJump": 0, "Bash": 0, "Stomp": 0, "Glide": 0, "Climb": 0, "ChargeJump": 0, "Dash": 0, "Grenade": 0, "GinsoKey": 0, "ForlornKey": 0, "HoruKey": 0, "Water": 0, "Wind": 0, "WaterVeinShard": 0, "GumonSealShard": 0, "SunstoneShard": 0, "TPForlorn": 0, "TPGrotto": 0, "TPSorrow": 0, "TPGrove": 0, "TPSwamp": 0, "TPValley": 0, "TPGinso": 0, "TPHoru": 0, "Relic": 0 } for i in range(1, 10): self.locationAnalysis["MapStone " + str(i)] = self.itemsToAnalyze.copy() self.locationAnalysis["MapStone " + str(i)]["Zone"] = "MapStone" for count in range(0, self.repeat_count): if self.repeat_count > 1: self.seed = "%s_%s" % (base_seed, count) if self.do_loc_analysis: print(self.seed) sg = SeedGenerator() if args.do_reachability_analysis: sg.do_reachability_analysis(self) return raw = sg.setSeedAndPlaceItems(self, preplaced={}) seeds = [] spoilers = [] if not raw: log.error("Couldn't build seed!") if self.do_loc_analysis: continue return player = 0 for player_raw in raw: player += 1 seed, spoiler = tuple(player_raw) if self.tracking: seed = "Sync%s.%s," % (self.sync_id, player) + seed seedfile = "randomizer_%s.dat" % player spoilerfile = "spoiler_%s.txt" % player if self.players == 1: seedfile = "randomizer" + str(count) + ".dat" spoilerfile = "spoiler" + str(count) + ".txt" if not self.do_analysis and not self.do_loc_analysis: with open(args.output_dir + "/" + seedfile, 'w') as f: f.write(seed) with open(args.output_dir + "/" + spoilerfile, 'w') as f: f.write(spoiler) if self.do_analysis: i = 0 for spoiler_line in spoiler.split("\n"): fl = first_line_pattern.match(spoiler_line) if fl: raw_group, raw_locs = fl.group(1, 2) i = int(raw_group) info_by_group[i]["seeds"] += 1 info_by_group[i]["locs"] += raw_locs.count(",") + 1 continue fp = forced_pickup_pattern.match(spoiler_line) if fp: info_by_group[i]["force"] += 1 for raw in fp.group(1).split(","): info_by_group[i]["forced"][raw.strip( " '")] += 1 continue nl = normal_line_pattern.match(spoiler_line) if nl: item = nl.group(1) if item in key_items: info_by_group[i]["items"][item] += 1 if self.do_analysis: # output = open("analysis.csv", 'w') # output.write("Location,Zone,WallJump,ChargeFlame,DoubleJump,Bash,Stomp,Glide,Climb,ChargeJump,Dash,Grenade,GinsoKey,ForlornKey,HoruKey,Water,Wind,WaterVeinShard,GumonSealShard,SunstoneShard,TPGrove,TPGrotto,TPSwamp,TPValley,TPSorrow,TPGinso,TPForlorn,TPHoru,Relic\n") for i, group in info_by_group.items(): seeds = float(group["seeds"]) print "%d (%d): " % (i, int(seeds)) print "\tkey items: [", for item, count in group["items"].items(): print '%s: %02.2f%%,' % (item, 100 * float(count) / seeds), print "]\n\tforced: [", for item, count in group["forced"].items(): print '%s: %02.2f%%,' % (item, 100 * float(count) / float(group["force"])), print "]\n\taverage locs", float(group['locs']) / seeds with open("anal.pickle", 'w') as out_file: pickle.dump(info_by_group, out_file) with open("analysis.csv", 'w') as out_file: out_file.write( "Group,Seeds,Forced,Locs,WallJump,ChargeFlame,DoubleJump,Bash,Stomp,Glide,Climb,ChargeJump,Dash,Grenade,GinsoKey,ForlornKey,HoruKey,Water,Wind,TPGrove,TPGrotto,TPSwamp,TPValley,TPSorrow,TPGinso,TPForlorn,TPHoru,WallJump,ChargeFlame,DoubleJump,Bash,Stomp,Glide,Climb,ChargeJump,Dash,Grenade,GinsoKey,ForlornKey,HoruKey,Water,Wind,TPGrove,TPGrotto,TPSwamp,TPValley,TPSorrow,TPGinso,TPForlorn,TPHoru,KS,EC,HC,MS,AC\n" ) for i, group in info_by_group.items(): seeds = float(group["seeds"]) line = [ i, int(seeds), int(group["force"]), "%02.2f" % (float(group["locs"]) / seeds) ] for key_item in [ "WallJump", "ChargeFlame", "DoubleJump", "Bash", "Stomp", "Glide", "Climb", "ChargeJump", "Dash", "Grenade", "GinsoKey", "ForlornKey", "HoruKey", "Water", "Wind", "TPGrove", "TPGrotto", "TPSwamp", "TPValley", "TPSorrow", "TPGinso", "TPForlorn", "TPHoru" ]: line.append( "%02.2f%%" % (100 * float(group["items"][key_item]) / seeds)) for forced_prog in [ "WallJump", "ChargeFlame", "DoubleJump", "Bash", "Stomp", "Glide", "Climb", "ChargeJump", "Dash", "Grenade", "GinsoKey", "ForlornKey", "HoruKey", "Water", "Wind", "TPGrove", "TPGrotto", "TPSwamp", "TPValley", "TPSorrow", "TPGinso", "TPForlorn", "TPHoru", "KS", "EC", "HC", "MS", "AC" ]: line.append( "%02.2f%%" % (100 * float(group["forced"][forced_prog]) / float(group["force"]) if group["forced"][forced_prog] else 0)) out_file.write(",".join([str(x) for x in line]) + '\n') if self.do_loc_analysis: output = open("analysis.csv", 'w') output.write( "Group,WallJump,ChargeFlame,DoubleJump,Bash,Stomp,Glide,Climb,ChargeJump,Dash,Grenade,Water,Wind,WaterVeinShard,GumonSealShard,SunstoneShard,TPGrove,TPGrotto,TPSwamp,TPValley,TPSorrow,TPGinso,TPForlorn,TPHoru,\n" ) for key in self.locationAnalysis.keys(): line = key + "," line += str(self.locationAnalysis[key]["Zone"]) + "," line += str(self.locationAnalysis[key]["WallJump"]) + "," line += str(self.locationAnalysis[key]["ChargeFlame"]) + "," line += str(self.locationAnalysis[key]["DoubleJump"]) + "," line += str(self.locationAnalysis[key]["Bash"]) + "," line += str(self.locationAnalysis[key]["Stomp"]) + "," line += str(self.locationAnalysis[key]["Glide"]) + "," line += str(self.locationAnalysis[key]["Climb"]) + "," line += str(self.locationAnalysis[key]["ChargeJump"]) + "," line += str(self.locationAnalysis[key]["Dash"]) + "," line += str(self.locationAnalysis[key]["Grenade"]) + "," line += str(self.locationAnalysis[key]["GinsoKey"]) + "," line += str(self.locationAnalysis[key]["ForlornKey"]) + "," line += str(self.locationAnalysis[key]["HoruKey"]) + "," line += str(self.locationAnalysis[key]["Water"]) + "," line += str(self.locationAnalysis[key]["Wind"]) + "," line += str(self.locationAnalysis[key]["WaterVeinShard"]) + "," line += str(self.locationAnalysis[key]["GumonSealShard"]) + "," line += str(self.locationAnalysis[key]["SunstoneShard"]) + "," line += str(self.locationAnalysis[key]["TPGrove"]) + "," line += str(self.locationAnalysis[key]["TPGrotto"]) + "," line += str(self.locationAnalysis[key]["TPSwamp"]) + "," line += str(self.locationAnalysis[key]["TPValley"]) + "," line += str(self.locationAnalysis[key]["TPSorrow"]) + "," line += str(self.locationAnalysis[key]["TPGinso"]) + "," line += str(self.locationAnalysis[key]["TPForlorn"]) + "," line += str(self.locationAnalysis[key]["TPHoru"]) + "," line += str(self.locationAnalysis[key]["Relic"]) output.write(line + "\n")