aboutsummaryrefslogtreecommitdiff
path: root/artifact_optimizer.py
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-05-28 07:12:43 -0700
committerFuwn <[email protected]>2025-05-28 07:12:43 -0700
commit035008f3ee1bf5ee84a0cd290d37f222a1cd5e61 (patch)
tree950ffad958908892a40ed9cb69c0db6a23e59a3a /artifact_optimizer.py
downloadgenshin-artifact-playground-main.tar.xz
genshin-artifact-playground-main.zip
feat: Initial commitHEADmain
Diffstat (limited to 'artifact_optimizer.py')
-rw-r--r--artifact_optimizer.py441
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()