diff options
| author | Fuwn <[email protected]> | 2025-05-28 07:12:43 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-05-28 07:12:43 -0700 |
| commit | 035008f3ee1bf5ee84a0cd290d37f222a1cd5e61 (patch) | |
| tree | 950ffad958908892a40ed9cb69c0db6a23e59a3a /artifact_optimizer.py | |
| download | genshin-artifact-playground-main.tar.xz genshin-artifact-playground-main.zip | |
Diffstat (limited to 'artifact_optimizer.py')
| -rw-r--r-- | artifact_optimizer.py | 441 |
1 files changed, 441 insertions, 0 deletions
diff --git a/artifact_optimizer.py b/artifact_optimizer.py new file mode 100644 index 0000000..fddf25f --- /dev/null +++ b/artifact_optimizer.py @@ -0,0 +1,441 @@ +import json +import copy +from typing import Dict, List, Tuple, Optional + +class ArtifactOptimizer: + def __init__(self): + # Special numbers for scoring (from the post) + self.stat_weights = { + 'hp_': 5.0, # HP% + 'hp': 700.0, # Flat HP + 'atk_': 5.0, # ATK% + 'atk': 45.0, # Flat ATK + 'def_': 6.0, # DEF% + 'def': 50.0, # Flat DEF + 'eleMas': 20.0, # Elemental Mastery + 'enerRech_': 5.5, # Energy Recharge% + 'critRate_': 3.0, # CRIT Rate% + 'critDMG_': 6.0 # CRIT DMG% + } + + # Slot mappings + self.slot_names = { + 'flower': 'Flower', + 'plume': 'Feather', + 'sands': 'Sands', + 'goblet': 'Goblet', + 'circlet': 'Circlet' + } + + def load_data(self, artifacts_file: str, characters_file: str): + """Load artifact and character data from JSON files""" + with open(artifacts_file, 'r') as f: + self.artifact_data = json.load(f) + + with open(characters_file, 'r') as f: + self.character_data = json.load(f) + + self.artifacts = self.artifact_data['artifacts'] + + def get_character_relevant_stats(self, character: str) -> List[str]: + """Get the stats that are relevant for a character based on their substat priority""" + char_data = self.character_data.get(character, {}) + substat_priority = char_data.get('substat_priority', []) + + # Convert display names to internal stat keys + stat_mapping = { + 'ER%': 'enerRech_', + 'CRIT Rate': 'critRate_', + 'CRIT DMG': 'critDMG_', + 'HP%': 'hp_', + 'Flat HP': 'hp', + 'ATK%': 'atk_', + 'Flat ATK': 'atk', + 'DEF%': 'def_', + 'Flat DEF': 'def', + 'Elemental Mastery': 'eleMas' + } + + relevant_stats = [] + for stat in substat_priority: + if stat in stat_mapping: + relevant_stats.append(stat_mapping[stat]) + + return relevant_stats + + def calculate_artifact_score(self, artifact: Dict, character: str) -> float: + """Calculate artifact score for a specific character""" + relevant_stats = self.get_character_relevant_stats(character) + + total_value = 0.0 + + # Score substats + for substat in artifact.get('substats', []): + stat_key = substat['key'] + stat_value = substat['value'] + + if stat_key in relevant_stats and stat_key in self.stat_weights: + total_value += stat_value / self.stat_weights[stat_key] + + # Convert to percentage score (max 9 substat rolls possible) + score = (total_value / 9.0) * 100.0 + return score + + def is_set_suitable(self, artifact: Dict, character: str) -> bool: + """Check if artifact's set is suitable for the character""" + char_data = self.character_data.get(character, {}) + required_sets = char_data.get('required_sets', []) + + # If no required sets specified, accept any set + if not required_sets: + return True + + return artifact['setKey'] in required_sets + + def is_main_stat_suitable(self, artifact: Dict, character: str) -> bool: + """Check if artifact's main stat is suitable for the character""" + char_data = self.character_data.get(character, {}) + ideal_main_stats = char_data.get('ideal_main_stats', {}) + + slot = artifact['slotKey'] + main_stat = artifact['mainStatKey'] + + # Convert internal stat keys to display names for comparison + stat_display_mapping = { + 'hp': 'HP', + 'atk': 'ATK', + 'hp_': 'HP%', + 'atk_': 'ATK%', + 'def_': 'DEF%', + 'enerRech_': 'ER%', + 'eleMas': 'Elemental Mastery', + 'critRate_': 'CRIT Rate', + 'critDMG_': 'CRIT DMG', + 'heal_': 'Healing Bonus', + 'physical_dmg_': 'Physical DMG Bonus', + 'anemo_dmg_': 'Anemo DMG Bonus', + 'geo_dmg_': 'Geo DMG Bonus', + 'electro_dmg_': 'Electro DMG Bonus', + 'dendro_dmg_': 'Dendro DMG Bonus', + 'hydro_dmg_': 'Hydro DMG Bonus', + 'pyro_dmg_': 'Pyro DMG Bonus', + 'cryo_dmg_': 'Cryo DMG Bonus' + } + + main_stat_display = stat_display_mapping.get(main_stat, main_stat) + + # Flower and Feather have fixed main stats + if slot == 'flower': + return main_stat == 'hp' + elif slot == 'plume': + return main_stat == 'atk' + + # Check if main stat is in the ideal list for this slot + slot_ideals = ideal_main_stats.get(slot, []) + + # If no ideal main stats specified for this slot, accept any reasonable main stat + if not slot_ideals: + # Accept common main stats for each slot + if slot == 'sands': + return main_stat in ['atk_', 'hp_', 'def_', 'enerRech_', 'eleMas'] + elif slot == 'goblet': + return main_stat in ['atk_', 'hp_', 'def_', 'eleMas'] or main_stat.endswith('_dmg_') + elif slot == 'circlet': + return main_stat in ['atk_', 'hp_', 'def_', 'critRate_', 'critDMG_', 'heal_', 'eleMas'] + + return main_stat_display in slot_ideals + + def check_er_requirement(self, artifacts: List[Dict], character: str) -> Dict: + """Check if the artifact set meets ER requirements""" + char_data = self.character_data.get(character, {}) + er_req = char_data.get('er_requirement', {}) + + # Calculate total ER from artifacts + total_er = 100.0 # Base ER + + for artifact in artifacts: + # Main stat ER + if artifact['mainStatKey'] == 'enerRech_': + if artifact['level'] == 20: + total_er += 51.8 # Max ER% main stat at level 20 + else: + # Approximate ER based on level + total_er += 51.8 * (artifact['level'] / 20.0) + + # Substat ER + for substat in artifact.get('substats', []): + if substat['key'] == 'enerRech_': + total_er += substat['value'] + + # Determine requirement type + if isinstance(er_req, dict): + if 'min' in er_req: + # Simple min/max requirement + req_min = er_req['min'] + req_max = er_req.get('max', req_min) + meets_req = req_min <= total_er <= req_max + 20 # Allow some flexibility + req_type = f"{req_min}-{req_max}%" + else: + # Multiple scenarios (like Fischl) + meets_req = False + req_type = "Various scenarios" + for scenario, req_data in er_req.items(): + if req_data['min'] <= total_er <= req_data['max'] + 20: + meets_req = True + req_type = f"{scenario}: {req_data['min']}-{req_data['max']}%" + break + else: + meets_req = True + req_type = "No specific requirement" + + return { + 'total_er': total_er, + 'meets_requirement': meets_req, + 'requirement_type': req_type + } + + def get_available_artifacts_by_slot(self, slot: str, exclude_ids: set, character: str = None, min_rarity: int = 5) -> List[Dict]: + """Get all available artifacts for a specific slot, optionally filtered by character requirements""" + available = [] + for artifact in self.artifacts: + if (artifact['slotKey'] == slot and + artifact['id'] not in exclude_ids and + artifact['rarity'] >= min_rarity): + + # If character specified, check set requirements + if character and not self.is_set_suitable(artifact, character): + continue + + available.append(artifact) + return available + + def find_best_build_for_character(self, character: str, exclude_ids: set) -> Tuple[List[Dict], float, Dict, Dict]: + """Find the best artifact build for a character""" + slots = ['flower', 'plume', 'sands', 'goblet', 'circlet'] + best_build = None + best_score = -1 + best_er_info = None + + # Get available artifacts for each slot and collect debug info + slot_artifacts = {} + debug_info = {} + + # First try with 5-star artifacts only + for slot in slots: + available_5star = self.get_available_artifacts_by_slot(slot, exclude_ids, character, min_rarity=5) + suitable_5star = [art for art in available_5star if self.is_main_stat_suitable(art, character)] + + # If no suitable 5-star artifacts, include 4-star as fallback + if len(suitable_5star) == 0: + available_all = self.get_available_artifacts_by_slot(slot, exclude_ids, character, min_rarity=4) + suitable_all = [art for art in available_all if self.is_main_stat_suitable(art, character)] + slot_artifacts[slot] = suitable_all + used_fallback = len(suitable_all) > 0 + else: + slot_artifacts[slot] = suitable_5star + available_all = available_5star + used_fallback = False + + # Collect debug information + char_data = self.character_data.get(character, {}) + required_sets = char_data.get('required_sets', []) + + main_stats_found = list(set(art['mainStatKey'] for art in available_all)) + suitable_main_stats_found = list(set(art['mainStatKey'] for art in slot_artifacts[slot])) + sets_found = list(set(art['setKey'] for art in available_all)) + + debug_info[slot] = { + 'total_available': len(available_all), + 'suitable_main_stats': len(slot_artifacts[slot]), + 'main_stats_found': main_stats_found, + 'suitable_main_stats_found': suitable_main_stats_found, + 'sets_found': sets_found, + 'required_sets': required_sets, + 'used_4star_fallback': used_fallback + } + + # Try all combinations (this is computationally expensive but thorough) + from itertools import product + + artifact_combinations = list(product(*[slot_artifacts[slot] for slot in slots])) + + for combination in artifact_combinations: + if len(set(art['id'] for art in combination)) != 5: + continue # Skip if any artifacts are duplicated + + # Calculate total score + total_score = sum(self.calculate_artifact_score(art, character) for art in combination) + avg_score = total_score / 5 + + # Check ER requirement + er_info = self.check_er_requirement(list(combination), character) + + # Prioritize builds that meet ER requirements + adjusted_score = avg_score + if er_info['meets_requirement']: + adjusted_score += 10 # Bonus for meeting ER req + + if adjusted_score > best_score: + best_score = adjusted_score + best_build = list(combination) + best_er_info = er_info + + return best_build, best_score - (10 if best_er_info and best_er_info['meets_requirement'] else 0), best_er_info, debug_info + + def optimize_builds(self, character_priority: List[str]) -> Dict: + """Optimize artifact builds for characters in priority order""" + results = {} + used_artifact_ids = set() + + for character in character_priority: + if character not in self.character_data: + print(f"Warning: Character '{character}' not found in character data") + continue + + print(f"Finding best build for {character}...") + + best_build, score, er_info, debug_info = self.find_best_build_for_character(character, used_artifact_ids) + + if best_build: + # Mark these artifacts as used + for artifact in best_build: + used_artifact_ids.add(artifact['id']) + + # Calculate individual artifact scores + artifact_scores = [] + for artifact in best_build: + art_score = self.calculate_artifact_score(artifact, character) + artifact_scores.append({ + 'artifact': artifact, + 'score': art_score + }) + + results[character] = { + 'build': best_build, + 'average_score': score, + 'artifact_scores': artifact_scores, + 'er_info': er_info, + 'debug_info': debug_info + } + else: + results[character] = { + 'build': None, + 'average_score': 0, + 'artifact_scores': [], + 'er_info': {'total_er': 100, 'meets_requirement': False, 'requirement_type': 'No artifacts available'}, + 'debug_info': debug_info + } + + return results + + def format_results(self, results: Dict) -> str: + """Format the optimization results for display""" + output = [] + output.append("=" * 80) + output.append("ARTIFACT OPTIMIZATION RESULTS") + output.append("=" * 80) + + for character, data in results.items(): + output.append(f"\n{'='*20} {character.upper()} {'='*20}") + + if not data['build']: + output.append("ā No suitable build found") + + # Add debug information + debug_info = data.get('debug_info', {}) + output.append("\nš DEBUG INFO:") + for slot, info in debug_info.items(): + fallback_note = " (using 4ā fallback)" if info.get('used_4star_fallback') else "" + output.append(f" {slot}: {info['suitable_main_stats']}/{info['total_available']} suitable artifacts{fallback_note}") + if info.get('required_sets'): + output.append(f" Required sets: {', '.join(info['required_sets'])}") + if info.get('sets_found'): + output.append(f" Available sets: {', '.join(info['sets_found'])}") + if info['main_stats_found']: + output.append(f" Available main stats: {', '.join(info['main_stats_found'])}") + if info['suitable_main_stats_found']: + output.append(f" Suitable main stats: {', '.join(info['suitable_main_stats_found'])}") + + continue + + output.append(f"š Average Artifact Score: {data['average_score']:.2f}/100") + + # ER Information + er_info = data['er_info'] + er_status = "ā
" if er_info['meets_requirement'] else "ā ļø" + output.append(f"{er_status} Energy Recharge: {er_info['total_er']:.1f}% ({er_info['requirement_type']})") + + output.append("\nš ARTIFACT BUILD:") + + for i, art_data in enumerate(data['artifact_scores']): + artifact = art_data['artifact'] + score = art_data['score'] + + slot_name = self.slot_names[artifact['slotKey']] + set_name = artifact['setKey'] + main_stat = artifact['mainStatKey'] + level = artifact['level'] + rarity = artifact['rarity'] + rarity_stars = 'ā' * rarity + + output.append(f"\n{slot_name} ({set_name}) - Level {level} {rarity_stars}") + output.append(f" Main Stat: {main_stat}") + output.append(f" Score: {score:.2f}/100") + output.append(f" Substats:") + + for substat in artifact['substats']: + stat_key = substat['key'] + stat_value = substat['value'] + + # Format the value appropriately + if stat_key in ['hp', 'atk', 'def', 'eleMas']: + formatted_value = f"{stat_value:.0f}" + else: + formatted_value = f"{stat_value:.1f}%" + + output.append(f" ⢠{stat_key}: {formatted_value}") + + # Summary + output.append(f"\n{'='*20} SUMMARY {'='*20}") + total_chars = len(results) + successful_builds = sum(1 for data in results.values() if data['build'] is not None) + + output.append(f"Characters processed: {total_chars}") + output.append(f"Successful builds: {successful_builds}") + output.append(f"Failed builds: {total_chars - successful_builds}") + + if successful_builds > 0: + avg_score = sum(data['average_score'] for data in results.values() if data['build']) / successful_builds + output.append(f"Average build quality: {avg_score:.2f}/100") + + return "\n".join(output) + +def main(): + # Initialize optimizer + optimizer = ArtifactOptimizer() + + # Load data + optimizer.load_data('data.json', 'characters.json') + + # Define character priority (example) + character_priority = ['Furina', 'Escoffier', 'Fischl', 'Chiori'] + + print("Starting artifact optimization...") + print(f"Character priority: {' -> '.join(character_priority)}") + print(f"Total artifacts available: {len(optimizer.artifacts)}") + + # Optimize builds + results = optimizer.optimize_builds(character_priority) + + # Display results + formatted_output = optimizer.format_results(results) + print(formatted_output) + + # Save results to file + with open('optimization_results.txt', 'w') as f: + f.write(formatted_output) + + print(f"\nResults saved to 'optimization_results.txt'") + +if __name__ == "__main__": + main() |