def setup_starting_area_elements(self): game_description = default_prime2_game_description() world_to_group = {} self._starting_location_for_area = {} for row, world in enumerate(game_description.world_list.worlds): for column, is_dark_world in enumerate([False, True]): group_box = QGroupBox(self.starting_locations_contents) group_box.setTitle(world.correct_name(is_dark_world)) vertical_layout = QVBoxLayout(group_box) vertical_layout.setContentsMargins(8, 4, 8, 4) vertical_layout.setSpacing(2) vertical_layout.setAlignment(QtCore.Qt.AlignTop) group_box.vertical_layout = vertical_layout world_to_group[world.correct_name(is_dark_world)] = group_box self.starting_locations_layout.addWidget(group_box, row, column) for world in game_description.world_list.worlds: for area in sorted(world.areas, key=lambda a: a.name): group_box = world_to_group[world.correct_name(area.in_dark_aether)] check = QtWidgets.QCheckBox(group_box) check.setText(area.name) check.area_location = AreaLocation(world.world_asset_id, area.area_asset_id) check.stateChanged.connect(functools.partial(self._on_check_starting_area, check)) group_box.vertical_layout.addWidget(check) self._starting_location_for_area[area.area_asset_id] = check self.starting_area_quick_fill_ship.clicked.connect(self._starting_location_on_select_ship) self.starting_area_quick_fill_save_station.clicked.connect(self._starting_location_on_select_save_station)
def __init__(self, main_window: MainWindow, background_processor: BackgroundTaskMixin, options: Options): super().__init__() self.setupUi(self) self._options = options self._main_window = main_window self.game_description = default_database.default_prime2_game_description() self.world_list = self.game_description.world_list self.resource_database = self.game_description.resource_database # Update with Options self.setup_trick_level_elements() self.setup_elevator_elements() self.setup_sky_temple_elements() self.setup_starting_area_elements() self.setup_translators_elements() self.setup_hint_elements() # Alignment self.trick_level_layout.setAlignment(QtCore.Qt.AlignTop) self.elevator_layout.setAlignment(QtCore.Qt.AlignTop) self.goal_layout.setAlignment(QtCore.Qt.AlignTop) self.starting_area_layout.setAlignment(QtCore.Qt.AlignTop) self.translators_layout.setAlignment(QtCore.Qt.AlignTop) self.hint_layout.setAlignment(QtCore.Qt.AlignTop)
def _setup_difficulties_menu(self): game = default_database.default_prime2_game_description() for i, trick_level in enumerate(LayoutTrickLevel): if trick_level not in { LayoutTrickLevel.NO_TRICKS, LayoutTrickLevel.MINIMAL_RESTRICTIONS }: difficulty_action = QAction(self) difficulty_action.setText(trick_level.long_name) self.menu_difficulties.addAction(difficulty_action) difficulty_action.triggered.connect( functools.partial(self._open_difficulty_details_popup, trick_level)) configurable_tricks = TrickLevelConfiguration.all_possible_tricks() tricks_in_use = used_tricks(game.world_list) for trick in sorted(game.resource_database.trick, key=lambda _trick: _trick.long_name): if trick.index not in configurable_tricks or trick not in tricks_in_use: continue trick_menu = QMenu(self) trick_menu.setTitle(trick.long_name) self.menu_trick_details.addAction(trick_menu.menuAction()) used_difficulties = difficulties_for_trick(game.world_list, trick) for i, trick_level in enumerate(LayoutTrickLevel): if trick_level in used_difficulties: difficulty_action = QAction(self) difficulty_action.setText(trick_level.long_name) trick_menu.addAction(difficulty_action) difficulty_action.triggered.connect( functools.partial(self._open_trick_details_popup, trick, trick_level))
def setup_location_pool_elements(self): self.randomization_mode_combo.setItemData(0, RandomizationMode.FULL) self.randomization_mode_combo.setItemData( 1, RandomizationMode.MAJOR_MINOR_SPLIT) self.randomization_mode_combo.currentIndexChanged.connect( self._on_update_randomization_mode) game_description = default_prime2_game_description() world_to_group = {} self._location_pool_for_node = {} for world in game_description.world_list.worlds: for is_dark_world in [False, True]: group_box = QGroupBox(self.excluded_locations_area_contents) group_box.setTitle(world.correct_name(is_dark_world)) vertical_layout = QVBoxLayout(group_box) vertical_layout.setContentsMargins(8, 4, 8, 4) vertical_layout.setSpacing(2) group_box.vertical_layout = vertical_layout world_to_group[world.correct_name(is_dark_world)] = group_box self.excluded_locations_area_layout.addWidget(group_box) for world, area, node in game_description.world_list.all_worlds_areas_nodes: if not isinstance(node, PickupNode): continue group_box = world_to_group[world.correct_name(area.in_dark_aether)] check = QtWidgets.QCheckBox(group_box) check.setText(game_description.world_list.node_name(node)) check.node = node check.stateChanged.connect( functools.partial(self._on_check_location, check)) group_box.vertical_layout.addWidget(check) self._location_pool_for_node[node] = check
def add_elevator_connections_to_patches( layout_configuration: LayoutConfiguration, rng: Random, patches: GamePatches) -> GamePatches: """ :param layout_configuration: :param rng: :param patches: :return: """ if layout_configuration.elevators == LayoutElevators.RANDOMIZED: if rng is None: raise MissingRng("Elevator") world_list = default_database.default_prime2_game_description( ).world_list areas_to_not_change = { 2278776548, # Sky Temple Gateway 2068511343, # Sky Temple Energy Controller 3136899603, # Aerie Transport Station 1564082177, # Aerie } elevator_connection = copy.copy(patches.elevator_connection) elevator_connection.update( elevator_distributor.elevator_connections_for_seed_number( rng=rng, elevator_database=elevator_distributor. create_elevator_database(world_list, areas_to_not_change))) return dataclasses.replace(patches, elevator_connection=elevator_connection) else: return patches
def as_json(self) -> list: world_list = default_database.default_prime2_game_description().world_list return list(sorted( world_list.area_name(world_list.area_by_area_location(location), separator="/", distinguish_dark_aether=False) for location in self.locations ))
async def _setup_locations_combo(self): network_client = common_qt_lib.get_network_client() game_session = network_client.current_game_session user = network_client.current_user game = default_database.default_prime2_game_description() index_to_name = { node.pickup_index.index: game.world_list.area_name(area) for world, area, node in game.world_list.all_worlds_areas_nodes if isinstance(node, PickupNode) } if game_session is None: names = index_to_name else: patcher_data = await network_client.session_admin_player( user.id, SessionAdminUserAction.CREATE_PATCHER_FILE, CosmeticPatches().as_json) names = { pickup["pickup_index"]: "{}: {}".format(index_to_name[pickup["pickup_index"]], pickup["hud_text"][0]) for pickup in patcher_data["pickups"] } self.collect_location_combo.clear() for index, name in sorted(names.items()): self.collect_location_combo.addItem(name, index) self.collect_location_button.setEnabled(True) self.collect_location_combo.setVisible(True) self.setup_collect_location_combo_button.deleteLater()
def _areas_list(): world_list = default_database.default_prime2_game_description().world_list areas = [ AreaLocation(world.world_asset_id, area.area_asset_id) for world in world_list.worlds for area in world.areas ] return list(sorted(areas))
def _open_difficulty_details_popup(self, difficulty: LayoutTrickLevel): self._exec_trick_details( TrickDetailsPopup( self, self, default_database.default_prime2_game_description(), None, difficulty, ))
def _open_trick_details_popup(self, trick: TrickResourceInfo, level: LayoutTrickLevel): self._exec_trick_details( TrickDetailsPopup( self, self, default_database.default_prime2_game_description(), trick, level, ))
def __init__(self): super().__init__() self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.DEBUG) self.dolphin = dolphin_memory_engine self.game = default_prime2_game_description() self.message_queue = [] self._pickups_to_give = [] self._permanent_pickups = []
def _create_pickup_spoilers(self): self.pickup_spoiler_show_all_button.clicked.connect( self._toggle_show_all_pickup_spoiler) self.pickup_spoiler_show_all_button.currently_show_all = True self._create_pickup_spoiler_combobox() game_description = default_prime2_game_description() world_to_group = {} for world in game_description.world_list.worlds: for is_dark_world in [False, True]: group_box = QGroupBox(self.pickup_spoiler_scroll_contents) group_box.setTitle(world.correct_name(is_dark_world)) vertical_layout = QVBoxLayout(group_box) vertical_layout.setContentsMargins(8, 4, 8, 4) vertical_layout.setSpacing(2) group_box.vertical_layout = vertical_layout vertical_layout.horizontal_layouts = [] world_to_group[world.correct_name(is_dark_world)] = group_box self.pickup_spoiler_scroll_content_layout.addWidget(group_box) for world, area, node in game_description.world_list.all_worlds_areas_nodes: if not isinstance(node, PickupNode): continue group_box = world_to_group[world.correct_name(area.in_dark_aether)] horizontal_layout = QHBoxLayout() horizontal_layout.setSpacing(2) label = QLabel(group_box) label.setText(game_description.world_list.node_name(node)) horizontal_layout.addWidget(label) horizontal_layout.label = label push_button = QPushButton(group_box) push_button.setFlat(True) push_button.setText("Hidden") push_button.item_is_hidden = True push_button.pickup_index = node.pickup_index push_button.clicked.connect( partial(self._toggle_pickup_spoiler, push_button)) push_button.item_name = "Nothing was Set, ohno" push_button.row = horizontal_layout horizontal_layout.addWidget(push_button) horizontal_layout.button = push_button self.pickup_spoiler_buttons.append(push_button) group_box.vertical_layout.addLayout(horizontal_layout) group_box.vertical_layout.horizontal_layouts.append( horizontal_layout)
def add_elevator_connections_to_patches( layout_configuration: LayoutConfiguration, rng: Random, patches: GamePatches) -> GamePatches: """ :param layout_configuration: :param rng: :param patches: :return: """ elevator_connection = copy.copy(patches.elevator_connection) if layout_configuration.elevators != LayoutElevators.VANILLA: if rng is None: raise MissingRng("Elevator") world_list = default_database.default_prime2_game_description( ).world_list areas_to_not_change = { 2278776548, # Sky Temple Gateway 2068511343, # Sky Temple Energy Controller 3136899603, # Aerie Transport Station 1564082177, # Aerie } elevator_db = elevator_distributor.create_elevator_database( world_list, areas_to_not_change) if layout_configuration.elevators in { LayoutElevators.TWO_WAY_RANDOMIZED, LayoutElevators.TWO_WAY_UNCHECKED }: connections = elevator_distributor.two_way_elevator_connections( rng=rng, elevator_database=elevator_db, between_areas=layout_configuration.elevators == LayoutElevators.TWO_WAY_RANDOMIZED) else: connections = elevator_distributor.one_way_elevator_connections( rng=rng, elevator_database=elevator_db, world_list=world_list, elevator_target=layout_configuration.elevators == LayoutElevators.ONE_WAY_ELEVATOR) elevator_connection.update(connections) if layout_configuration.skip_final_bosses: elevator_connection[136970379] = AreaLocation(1006255871, 1393588666) return dataclasses.replace(patches, elevator_connection=elevator_connection)
def test_create_patches( mock_random: MagicMock, mock_calculate_item_pool: MagicMock, mock_create_base_patches: MagicMock, mock_retcon_playthrough_filler: MagicMock, mock_indices_for_unassigned_pickups: MagicMock, ): # Setup seed_number: int = 91319 game = default_prime2_game_description() status_update: Union[MagicMock, Callable[[str], None]] = MagicMock() configuration = LayoutConfiguration.from_params( trick_level=LayoutTrickLevel.NO_TRICKS, sky_temple_keys=LayoutSkyTempleKeyMode.FULLY_RANDOM, elevators=LayoutRandomizedFlag.VANILLA, pickup_quantities={}, starting_location=StartingLocation.default(), starting_resources=StartingResources.from_item_loss(False), ) permalink = Permalink( seed_number=seed_number, spoiler=True, patcher_configuration=PatcherConfiguration.default(), layout_configuration=configuration, ) mock_calculate_item_pool.return_value = list( sorted(game.pickup_database.original_pickup_mapping.values())) mock_create_base_patches.return_value.starting_location = game.starting_location mock_create_base_patches.return_value.custom_initial_items = None filler_patches = mock_retcon_playthrough_filler.return_value # Run result = generator._create_patches(permalink, game, status_update) # Assert mock_random.assert_called_once_with(permalink.as_str) mock_calculate_item_pool.assert_called_once_with(permalink, game) mock_create_base_patches.assert_called_once_with(mock_random.return_value, game, permalink, ANY) mock_retcon_playthrough_filler.assert_called_once_with( ANY, ANY, ANY, mock_random.return_value, status_update) mock_indices_for_unassigned_pickups.assert_called_once_with( mock_random.return_value, game, filler_patches.pickup_assignment, ANY) filler_patches.assign_new_pickups.assert_called_once_with( mock_indices_for_unassigned_pickups.return_value) assert result == filler_patches.assign_new_pickups.return_value
def from_json(cls, value: list) -> "StartingLocation": if not isinstance(value, list): raise ValueError("StartingLocation from_json must receive a list, got {}".format(type(value))) world_list = default_database.default_prime2_game_description().world_list elements = [] for location in value: world_name, area_name = location.split("/") world = world_list.world_with_name(world_name) area = world.area_by_name(area_name) elements.append(AreaLocation(world.world_asset_id, area.area_asset_id)) return cls.with_elements(elements)
def test_create_patches( mock_random: MagicMock, mock_calculate_item_pool: MagicMock, mock_create_base_patches: MagicMock, mock_validate_item_pool_size: MagicMock, mock_run_filler: MagicMock, mock_assign_remaining_items: MagicMock, ): # Setup game = default_prime2_game_description() status_update: Union[MagicMock, Callable[[str], None]] = MagicMock() permalink = MagicMock() pool_patches = MagicMock() item_pool = MagicMock() filler_patches = MagicMock() remaining_items = MagicMock() mock_calculate_item_pool.return_value = pool_patches, item_pool mock_run_filler.return_value = filler_patches, remaining_items # Run result = generator._create_randomized_patches(permalink, game, status_update) # Assert mock_random.assert_called_once_with(permalink.as_str) mock_create_base_patches.assert_called_once_with( permalink.layout_configuration, mock_random.return_value, game) # pool mock_calculate_item_pool.assert_called_once_with( permalink.layout_configuration, game.resource_database, mock_create_base_patches.return_value) mock_validate_item_pool_size.assert_called_once_with(item_pool, game) mock_run_filler.assert_called_once_with(permalink.layout_configuration, game, item_pool, pool_patches, mock_random.return_value, status_update) mock_assign_remaining_items.assert_called_once_with( mock_random.return_value, game.world_list, filler_patches.pickup_assignment, remaining_items, permalink.layout_configuration.randomization_mode) filler_patches.assign_pickup_assignment.assert_called_once_with( mock_assign_remaining_items.return_value) assert result == filler_patches.assign_pickup_assignment.return_value
def _update_hints_text(self): game_description = default_database.default_prime2_game_description() number_for_hint_type = { hint_type: i + 1 for i, hint_type in enumerate(LoreType) } used_hint_types = set() self.hint_tree_widget.setSortingEnabled(False) # TODO: This ignores the Dark World names. But there's currently no logbook nodes in Dark World. for world in game_description.world_list.worlds: world_item = QtWidgets.QTreeWidgetItem(self.hint_tree_widget) world_item.setText(0, world.name) world_item.setExpanded(True) for area in world.areas: hint_types = {} for node in area.nodes: if isinstance(node, LogbookNode): if node.required_translator is not None: hint_types[ node. lore_type] = node.required_translator.short_name else: hint_types[node.lore_type] = "✓" if hint_types: area_item = QtWidgets.QTreeWidgetItem(world_item) area_item.setText(0, area.name) for hint_type, text in hint_types.items(): area_item.setText(number_for_hint_type[hint_type], text) used_hint_types.add(hint_type) self.hint_tree_widget.resizeColumnToContents(0) self.hint_tree_widget.setSortingEnabled(True) self.hint_tree_widget.sortByColumn(0, QtCore.Qt.AscendingOrder) for hint_type in used_hint_types: self.hint_tree_widget.headerItem().setText( number_for_hint_type[hint_type], hint_type.long_name)
async def _setup_locations_combo(self): game = default_database.default_prime2_game_description() index_to_name = { node.pickup_index.index: game.world_list.area_name(area, distinguish_dark_aether=True, separator=" - ") for world, area, node in game.world_list.all_worlds_areas_nodes if isinstance(node, PickupNode) } self.collect_location_combo.clear() for index, name in sorted(index_to_name.items()): self.collect_location_combo.addItem(name, index) self.collect_location_button.setEnabled(True) self.collect_location_combo.setVisible(True) self.setup_collect_location_combo_button.deleteLater()
def __init__(self, window_manager: Optional[WindowManager], editor: PresetEditor): super().__init__() self.setupUi(self) common_qt_lib.set_default_window_icon(self) self._editor = editor self._window_manager = window_manager self._main_rules = MainRulesWindow(editor) self._game_patches = GamePatchesWindow(editor) self.game_description = default_database.default_prime2_game_description( ) self.world_list = self.game_description.world_list self.resource_database = self.game_description.resource_database # Update with Options self.logic_tab_widget.addTab(self._main_rules.centralWidget, "Item Pool") self.patches_tab_widget.addTab(self._game_patches.centralWidget, "Other") self.name_edit.textEdited.connect(self._edit_name) self.setup_trick_level_elements() self.setup_damage_elements() self.setup_elevator_elements() self.setup_sky_temple_elements() self.setup_starting_area_elements() self.setup_location_pool_elements() self.setup_translators_elements() self.setup_hint_elements() self.setup_beam_configuration_elements() # Alignment self.trick_level_layout.setAlignment(QtCore.Qt.AlignTop) self.elevator_layout.setAlignment(QtCore.Qt.AlignTop) self.goal_layout.setAlignment(QtCore.Qt.AlignTop) self.starting_area_layout.setAlignment(QtCore.Qt.AlignTop) self.translators_layout.setAlignment(QtCore.Qt.AlignTop) self.hint_layout.setAlignment(QtCore.Qt.AlignTop) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject)
def _setup_difficulties_menu(self): game = default_database.default_prime2_game_description() tricks_in_use = used_tricks(game) for trick in sorted(game.resource_database.trick, key=lambda _trick: _trick.long_name): if trick not in tricks_in_use: continue trick_menu = QMenu(self) trick_menu.setTitle(trick.long_name) self.menu_trick_details.addAction(trick_menu.menuAction()) used_difficulties = difficulties_for_trick(game, trick) for i, trick_level in enumerate(iterate_enum(LayoutTrickLevel)): if trick_level in used_difficulties: difficulty_action = QAction(self) difficulty_action.setText(trick_level.long_name) trick_menu.addAction(difficulty_action) difficulty_action.triggered.connect( functools.partial(self._open_trick_details_popup, trick, trick_level))
def _all_trick_indices(): resource_db = default_database.default_prime2_game_description().resource_database return {trick.index for trick in resource_db.trick}
def _update_hints_text(self): game_description = default_database.default_prime2_game_description() item_database = default_database.default_prime2_item_database() rows = [] for item in item_database.major_items.values(): rows.append(( item.name, item.item_category.hint_details[1], item.item_category.general_details[1], item.broad_category.hint_details[1], )) from randovania.games.prime.echoes_items import DARK_TEMPLE_KEY_NAMES for dark_temple_key in DARK_TEMPLE_KEY_NAMES: rows.append(( dark_temple_key.format("").strip(), ItemCategory.TEMPLE_KEY.hint_details[1], ItemCategory.TEMPLE_KEY.general_details[1], ItemCategory.KEY.hint_details[1], )) rows.append(( "Sky Temple Key", ItemCategory.SKY_TEMPLE_KEY.hint_details[1], ItemCategory.SKY_TEMPLE_KEY.general_details[1], ItemCategory.KEY.hint_details[1], )) for item in item_database.ammo.values(): rows.append(( item.name, ItemCategory.EXPANSION.hint_details[1], ItemCategory.EXPANSION.general_details[1], item.broad_category.hint_details[1], )) self.hint_item_names_tree_widget.setRowCount(len(rows)) for i, elements in enumerate(rows): for j, element in enumerate(elements): self.hint_item_names_tree_widget.setItem( i, j, QtWidgets.QTableWidgetItem(element)) for i in range(4): self.hint_item_names_tree_widget.resizeColumnToContents(i) number_for_hint_type = { hint_type: i + 1 for i, hint_type in enumerate(LoreType) } used_hint_types = set() self.hint_tree_widget.setSortingEnabled(False) # TODO: This ignores the Dark World names. But there's currently no logbook nodes in Dark World. for world in game_description.world_list.worlds: world_item = QtWidgets.QTreeWidgetItem(self.hint_tree_widget) world_item.setText(0, world.name) world_item.setExpanded(True) for area in world.areas: hint_types = {} for node in area.nodes: if isinstance(node, LogbookNode): if node.required_translator is not None: hint_types[ node. lore_type] = node.required_translator.short_name else: hint_types[node.lore_type] = "✓" if hint_types: area_item = QtWidgets.QTreeWidgetItem(world_item) area_item.setText(0, area.name) for hint_type, text in hint_types.items(): area_item.setText(number_for_hint_type[hint_type], text) used_hint_types.add(hint_type) self.hint_tree_widget.resizeColumnToContents(0) self.hint_tree_widget.setSortingEnabled(True) self.hint_tree_widget.sortByColumn(0, QtCore.Qt.AscendingOrder) for hint_type in used_hint_types: self.hint_tree_widget.headerItem().setText( number_for_hint_type[hint_type], hint_type.long_name)
def _all_tricks(): return default_database.default_prime2_game_description( ).resource_database.trick
def as_json(self) -> list: world_list = default_database.default_prime2_game_description().world_list return list(sorted( world_list.area_name(world_list.area_by_area_location(location)) for location in self.locations ))
def _areas_list(): world_list = default_database.default_prime2_game_description( False).world_list return world_list, list(world_list.all_areas)
def test_copy_worlds(): game_description = default_prime2_game_description() game_copy = copy.deepcopy(game_description) assert game_description.world_list.worlds == game_copy.world_list.worlds assert game_description.world_list.worlds is not game_copy.world_list.worlds
def _update_current_player(self): description = self.layout_description current_player = self.current_player_index preset = description.permalink.get_preset(current_player) title_text = """ <p> Permalink: <span style='font-weight:600;'>{description.permalink.as_str}</span><br/> Seed Hash: {description.shareable_word_hash} ({description.shareable_hash})<br/> Preset Name: {preset.name} </p> """.format(description=description, preset=preset) self.layout_title_label.setText(title_text) categories = list(preset_describer.describe(preset)) self.layout_description_left_label.setText(preset_describer.merge_categories(categories[::2])) self.layout_description_right_label.setText(preset_describer.merge_categories(categories[1::2])) # Game Spoiler has_spoiler = description.permalink.spoiler self.pickup_tab.setEnabled(has_spoiler) patches = description.all_patches[current_player] if has_spoiler: pickup_names = { pickup.pickup.name for pickup in patches.pickup_assignment.values() } game_description = default_prime2_game_description() starting_area = game_description.world_list.area_by_area_location(patches.starting_location) extra_items = patcher_file.additional_starting_items(preset.layout_configuration, game_description.resource_database, patches.starting_items) self.spoiler_starting_location_label.setText("Starting Location: {}".format( game_description.world_list.area_name(starting_area, distinguish_dark_aether=True, separator=" - ") )) self.spoiler_starting_items_label.setText("Random Starting Items: {}".format( ", ".join(extra_items) if extra_items else "None" )) else: pickup_names = {} self.layout_info_tab.removeTab(self.layout_info_tab.indexOf(self.pickup_tab)) self.spoiler_starting_location_label.setText("Starting Location") self.spoiler_starting_items_label.setText("Random Starting Items") self.pickup_spoiler_pickup_combobox.clear() self.pickup_spoiler_pickup_combobox.addItem("None") for pickup_name in sorted(pickup_names): self.pickup_spoiler_pickup_combobox.addItem(pickup_name) for pickup_button in self.pickup_spoiler_buttons: pickup_target = patches.pickup_assignment.get(pickup_button.pickup_index) pickup_button.target_player = None if pickup_target is not None: pickup_button.item_name = pickup_target.pickup.name if has_spoiler else "????" if has_spoiler and description.permalink.player_count > 1: pickup_button.target_player = pickup_target.player pickup_button.player_names = self._player_names else: pickup_button.item_name = "Nothing" if not pickup_button.item_is_hidden: pickup_button.setText(pickup_button.item_name)
def create_report(seeds_dir: str, output_file: str, csv_dir: Optional[str]): def item_creator(): return collections.defaultdict(int) items = collections.defaultdict(item_creator) locations = collections.defaultdict(item_creator) item_hints = collections.defaultdict(item_creator) location_hints = collections.defaultdict(item_creator) game_description = default_prime2_game_description() world_list = game_description.world_list index_to_location = { node.pickup_index.index: (world_list.world_name_from_node(node, distinguish_dark_aether=True), world_list.node_name(node)) for node in game_description.world_list.all_nodes if isinstance(node, PickupNode) } logbook_to_name = { str(node.string_asset_id): game_description.world_list.node_name(node) for node in game_description.world_list.all_nodes if isinstance(node, LogbookNode) } seed_count = 0 pickup_count = None for seed in Path(seeds_dir).glob("**/*.json"): for game_modifications in read_json(seed)["game_modifications"]: accumulate_results(game_modifications, items, locations, item_hints, location_hints, index_to_location, logbook_to_name) if seed_count == 0: pickup_count = calculate_pickup_count(items) seed_count += 1 if pickup_count is None: raise Exception("No seeds found") stddev_by_location = { location: calculate_stddev(pickup_count, locations[location]) for location in locations.keys() } final_results = { "seed_count": seed_count, "stddev_by_location": { location: stddev for location, stddev in sorted( stddev_by_location.items(), key=lambda t: t[1], reverse=True) }, "items": sort_by_contents(items), "locations": sort_by_contents(locations), "item_hints": sort_by_contents(item_hints), "location_hints": sort_by_contents(location_hints), } if csv_dir is not None: os.makedirs(csv_dir, exist_ok=True) for field in "items", "locations", "item_hints", "location_hints": data = final_results[field] possible_columns = set() for potential_values in data.values(): possible_columns |= set(potential_values.keys()) possible_columns = list(sorted(possible_columns)) possible_columns.insert(0, "row_name") with open(os.path.join(csv_dir, field + ".csv"), "w", newline='') as csv_file: writer = csv.DictWriter(csv_file, fieldnames=possible_columns) writer.writeheader() for column, row_data in data.items(): row_data = copy.copy(row_data) row_data["row_name"] = column writer.writerow(row_data) with open(output_file, "w") as output: json.dump(final_results, output, indent=4, separators=(',', ': '))