#!/usr/bin/env python3
# Genshin Banner Prediction Script by @atomicptr
# Last update 2021-10-11, predictions for the next banner
# NOTE: Scroll down there is an area where you might have to update data
# CHANGELOG:
# * [1.5.15] Add Hu Tao banner
# * [1.5.14] Add Tartaglia banner, add scoring for first character reappearance
# * [1.5.13] Add Kokomi banner, add Thoma
# * [1.5.12] Add Baal banner, set next character to Kokomi
# * [1.5.11] Add Klee, Ayaka and Yoimiya banners, set next character to Baal
# * [1.5.10] Add Eula banner, set next banner to klee, add kazuha
# * [1.5.9] Add Zhongli banner and set next banner to Eula
# * [1.5.8] Set next banner to zhongli
# * [1.5.7] Added Childe banner rerun, add Eula, Yan Fei and Ayaka data
# * [1.5.6] Added Venti banner
# * [1.5.5] Added new rule, each banner has to have at least one female and male character in it
# * [1.5.4] Added Hu Tao banner, remove now broken rule with duplicate element (at least with 5*),
# add rosaria, set next character to venti
# * [1.5.3] Set next character to Hu Tao
# * [1.5.2] Added Xiao and Keqing banner Source: https://genshin.mihoyo.com/m/en/news/detail/8418
# * [1.5.1] Only display Xiao since he is the next character
# * [1.5.0] Remove having to set the next banner index
# * [1.5.0] Add ability to set multiple characters as the next rate up character
# OLD VERSIONS:
# * v1.4.0 - https://glot.io/snippets/fusjr7nqed (contains all the other old ones)
from itertools import combinations
SCRIPT_VERSION = "v1.5.15"
# weapon types
WEAPON_TYPE_BOW = "Bow"
WEAPON_TYPE_CATALYST = "Catalyst"
WEAPON_TYPE_CLAYMORE = "Claymore"
WEAPON_TYPE_POLEARM = "Polearm"
WEAPON_TYPE_SWORD = "Sword"
# elements
ELEMENT_ANEMO = "Anemo"
ELEMENT_CRYO = "Cryo"
ELEMENT_ELECTRO = "Electro"
ELEMENT_DENDRO = "Dendro"
ELEMENT_GEO = "Geo"
ELEMENT_HYDRO = "Hydro"
ELEMENT_PYRO = "Pyro"
# indices
INDEX_WEAPON_TYPE = 0
INDEX_ELEMENT = 1
INDEX_STARS = 2
INDEX_IS_WAIFU = 3
# data for all 4 star characters
CHARACTER_ALBEDO = "Albedo"
CHARACTER_AMBER = "Amber"
CHARACTER_AYAKA = "Ayaka"
CHARACTER_BAAL = "Baal"
CHARACTER_BARBARA = "Barbara"
CHARACTER_BEIDOU = "Beidou"
CHARACTER_BENNET = "Bennet"
CHARACTER_CHONGYUN = "Chongyun"
CHARACTER_DILUC = "Diluc"
CHARACTER_DIONA = "Diona"
CHARACTER_EULA = "Eula"
CHARACTER_FISCHL = "Fischl"
CHARACTER_GANYU = "Ganyu"
CHARACTER_HUTAO = "Hu Tao"
CHARACTER_JEAN = "Jean"
CHARACTER_KAEYA = "Kaeya"
CHARACTER_KAZUHA = "Kazuha"
CHARACTER_KEQING = "Keqing"
CHARACTER_KLEE = "Klee"
CHARACTER_KOKOMI = "Kokomi"
CHARACTER_KUJOU = "Kujou"
CHARACTER_LISA = "Lisa"
CHARACTER_MONA = "Mona"
CHARACTER_NINGGUANG = "Ningguang"
CHARACTER_NOELLE = "Noelle"
CHARACTER_QIQI = "Qiqi"
CHARACTER_RAZOR = "Razor"
CHARACTER_ROSARIA = "Rosaria"
CHARACTER_SAYU = "Sayu"
CHARACTER_SUCROSE = "Sucrose"
CHARACTER_TARTAGLIA = "Tartaglia"
CHARACTER_THOMA = "Thoma"
CHARACTER_VENTI = "Venti"
CHARACTER_XIANGLING = "Xiangling"
CHARACTER_XIAO = "Xiao"
CHARACTER_XINQIU = "Xinqiu"
CHARACTER_XINYAN = "Xinyan"
CHARACTER_YANFEI = "Yan Fei"
CHARACTER_YOIMIYA = "Yoimiya"
CHARACTER_ZHONGLI = "Zhongli"
# because i keep forgetting the real name
CHARACTER_CHILDE = CHARACTER_TARTAGLIA
STARTER_CHARACTERS = [CHARACTER_AMBER, CHARACTER_KAEYA, CHARACTER_LISA]
FREE_PERMANENT_CHARACTERS = [CHARACTER_XIANGLING, CHARACTER_BARBARA, CHARACTER_AMBER, CHARACTER_KAEYA, CHARACTER_LISA]
CHARACTER_DATA = {
CHARACTER_ALBEDO: (WEAPON_TYPE_SWORD, ELEMENT_GEO, 5, False),
CHARACTER_AMBER: (WEAPON_TYPE_BOW, ELEMENT_PYRO, 4, True),
CHARACTER_AYAKA: (WEAPON_TYPE_SWORD, ELEMENT_CRYO, 5, True),
CHARACTER_BAAL: (WEAPON_TYPE_POLEARM, ELEMENT_ELECTRO, 5, True),
CHARACTER_BARBARA: (WEAPON_TYPE_CATALYST, ELEMENT_HYDRO, 4, True),
CHARACTER_BEIDOU: (WEAPON_TYPE_CLAYMORE, ELEMENT_ELECTRO, 4, True),
CHARACTER_BENNET: (WEAPON_TYPE_SWORD, ELEMENT_PYRO, 4, False),
CHARACTER_CHONGYUN: (WEAPON_TYPE_CLAYMORE, ELEMENT_CRYO, 4, False),
CHARACTER_DILUC: (WEAPON_TYPE_CLAYMORE, ELEMENT_PYRO, 5, False),
CHARACTER_DIONA: (WEAPON_TYPE_BOW, ELEMENT_CRYO, 4, True),
CHARACTER_EULA: (WEAPON_TYPE_CLAYMORE, ELEMENT_CRYO, 5, True),
CHARACTER_FISCHL: (WEAPON_TYPE_BOW, ELEMENT_ELECTRO, 4, True),
CHARACTER_GANYU: (WEAPON_TYPE_BOW, ELEMENT_CRYO, 5, True),
CHARACTER_HUTAO: (WEAPON_TYPE_POLEARM, ELEMENT_PYRO, 5, True),
CHARACTER_JEAN: (WEAPON_TYPE_SWORD, ELEMENT_ANEMO, 5, True),
CHARACTER_KAEYA: (WEAPON_TYPE_SWORD, ELEMENT_HYDRO, 4, False),
CHARACTER_KAZUHA: (WEAPON_TYPE_SWORD, ELEMENT_ANEMO, 5, False),
CHARACTER_KEQING: (WEAPON_TYPE_SWORD, ELEMENT_ELECTRO, 5, True),
CHARACTER_KLEE: (WEAPON_TYPE_CATALYST, ELEMENT_PYRO, 5, True),
CHARACTER_KOKOMI: (WEAPON_TYPE_CATALYST, ELEMENT_HYDRO, 5, True),
CHARACTER_KUJOU: (WEAPON_TYPE_BOW, ELEMENT_ELECTRO, 4, True),
CHARACTER_LISA: (WEAPON_TYPE_CATALYST, ELEMENT_ELECTRO, 4, True),
CHARACTER_MONA: (WEAPON_TYPE_CATALYST, ELEMENT_HYDRO, 5, True),
CHARACTER_NINGGUANG: (WEAPON_TYPE_CATALYST, ELEMENT_GEO, 4, True),
CHARACTER_NOELLE: (WEAPON_TYPE_CLAYMORE, ELEMENT_GEO, 4, True),
CHARACTER_QIQI: (WEAPON_TYPE_SWORD, ELEMENT_CRYO, 5, True),
CHARACTER_RAZOR: (WEAPON_TYPE_CLAYMORE, ELEMENT_ELECTRO, 4, False),
CHARACTER_ROSARIA: (WEAPON_TYPE_POLEARM, ELEMENT_CRYO, 4, True),
CHARACTER_SAYU: (WEAPON_TYPE_CLAYMORE, ELEMENT_ANEMO, 4, True),
CHARACTER_SUCROSE: (WEAPON_TYPE_CATALYST, ELEMENT_ANEMO, 4, True),
CHARACTER_TARTAGLIA: (WEAPON_TYPE_BOW, ELEMENT_HYDRO, 5, False),
CHARACTER_THOMA: (WEAPON_TYPE_POLEARM, ELEMENT_PYRO, 4, False),
CHARACTER_VENTI: (WEAPON_TYPE_BOW, ELEMENT_ANEMO, 5, False),
CHARACTER_XIANGLING: (WEAPON_TYPE_POLEARM, ELEMENT_PYRO, 4, True),
CHARACTER_XIAO: (WEAPON_TYPE_POLEARM, ELEMENT_ANEMO, 5, False),
CHARACTER_XINQIU: (WEAPON_TYPE_SWORD, ELEMENT_HYDRO, 4, False),
CHARACTER_XINYAN: (WEAPON_TYPE_CLAYMORE, ELEMENT_PYRO, 4, True),
CHARACTER_YANFEI: (WEAPON_TYPE_CATALYST, ELEMENT_PYRO, 4, True),
CHARACTER_YOIMIYA: (WEAPON_TYPE_BOW, ELEMENT_PYRO, 5, True),
CHARACTER_ZHONGLI: (WEAPON_TYPE_POLEARM, ELEMENT_GEO, 5, False),
}
CHARACTERS_4_STARS = [char for char in list(CHARACTER_DATA.keys()) if CHARACTER_DATA[char][INDEX_STARS] == 4]
def filter_4_stars(characters):
return [char for char in characters if CHARACTER_DATA[char][INDEX_STARS] == 4]
# banner data
#####################################################################################################
#################### NOTE: EDIT THIS AREA IF YOU WANT TO CHANGE THE RESULTS ########################
#####################################################################################################
# if character is completely unknown just Use [None] or None
NEXT_BANNER_5_STAR = [CHARACTER_ALBEDO]
BANNER_DATA = {
1: (CHARACTER_VENTI, CHARACTER_BARBARA, CHARACTER_FISCHL, CHARACTER_XIANGLING),
2: (CHARACTER_KLEE, CHARACTER_NOELLE, CHARACTER_XINQIU, CHARACTER_SUCROSE),
3: (CHARACTER_TARTAGLIA, CHARACTER_DIONA, CHARACTER_NINGGUANG, CHARACTER_BEIDOU),
4: (CHARACTER_ZHONGLI, CHARACTER_XINYAN, CHARACTER_CHONGYUN, CHARACTER_RAZOR),
5: (CHARACTER_ALBEDO, CHARACTER_FISCHL, CHARACTER_SUCROSE, CHARACTER_BENNET),
6: (CHARACTER_GANYU, CHARACTER_NOELLE, CHARACTER_XINQIU, CHARACTER_XIANGLING),
7: (CHARACTER_XIAO, CHARACTER_BEIDOU, CHARACTER_DIONA, CHARACTER_XINYAN),
8: (CHARACTER_KEQING, CHARACTER_BARBARA, CHARACTER_BENNET, CHARACTER_NINGGUANG),
9: (CHARACTER_HUTAO, CHARACTER_CHONGYUN, CHARACTER_XINQIU, CHARACTER_XIANGLING),
10: (CHARACTER_VENTI, CHARACTER_NOELLE, CHARACTER_RAZOR, CHARACTER_SUCROSE),
11: (CHARACTER_TARTAGLIA, CHARACTER_BARBARA, CHARACTER_FISCHL, CHARACTER_ROSARIA),
12: (CHARACTER_ZHONGLI, CHARACTER_DIONA, CHARACTER_NOELLE, CHARACTER_YANFEI),
13: (CHARACTER_EULA, CHARACTER_BEIDOU, CHARACTER_XINQIU, CHARACTER_XINYAN),
14: (CHARACTER_KLEE, CHARACTER_FISCHL, CHARACTER_SUCROSE, CHARACTER_BARBARA),
15: (CHARACTER_KAZUHA, CHARACTER_ROSARIA, CHARACTER_RAZOR, CHARACTER_BENNET),
16: (CHARACTER_AYAKA, CHARACTER_CHONGYUN, CHARACTER_NINGGUANG, CHARACTER_YANFEI),
17: (CHARACTER_YOIMIYA, CHARACTER_SAYU, CHARACTER_DIONA, CHARACTER_XINYAN),
18: (CHARACTER_BAAL, CHARACTER_KUJOU, CHARACTER_SUCROSE, CHARACTER_XIANGLING),
19: (CHARACTER_KOKOMI, CHARACTER_BEIDOU, CHARACTER_ROSARIA, CHARACTER_XINQIU),
20: (CHARACTER_TARTAGLIA, CHARACTER_CHONGYUN, CHARACTER_NINGGUANG, CHARACTER_YANFEI),
21: (CHARACTER_HUTAO, CHARACTER_THOMA, CHARACTER_SAYU, CHARACTER_DIONA),
}
#####################################################################################################
#####################################################################################################
#####################################################################################################
NEXT_BANNER_INDEX = max(BANNER_DATA.keys()) + 1
# rules
# Arguments:
# characters - List of all characters potentially in this banner
# Returns:
# True when the given characters don't violate the rule
# IMPORTANT: These rules are what we've seen so far and mihoyo might break them in the future
def rule_no_starter_characters(characters, next5star=None):
""" Character is not a starter character """
for char in characters:
if char in STARTER_CHARACTERS:
return False
return True
def rule_no_duplicate_element(characters, next5star=None):
""" No character in group is allowed to have the same element """
elements = []
for char in characters:
is5star = CHARACTER_DATA[char][INDEX_STARS] == 5
if is5star: # ignore 5* characters
continue
element = CHARACTER_DATA[char][INDEX_ELEMENT]
if element in elements:
return False
elements.append(element)
return True
def rule_character_wasnt_on_the_last_banner(characters, next5star=None):
""" No character was available in the last banner """
previous_banner_index = NEXT_BANNER_INDEX - 1
if not previous_banner_index in BANNER_DATA:
return False
banner = BANNER_DATA[previous_banner_index]
banner = filter_4_stars(banner)
for char in characters:
if char in banner:
return False
return True
def rule_unique_combination(characters, next5star=None):
""" Character list has to be unique aka never appeared exactly like this on a previous banner """
for banner_index in BANNER_DATA:
group = BANNER_DATA[banner_index]
group = filter_4_stars(group)
if sorted(characters) == sorted(group):
return False
return True
def rule_at_least_two_different_weapon_types_with_next_5_star(characters, next5star=None):
""" The characters should have at least 2 different weapon types otherwise mihoyo can't sell a weapon banner with 2 different types either... """
# if the next banner character isn't set, trying to evaluate this rule is pointless
# because it is possible to have all 4* characters with the same weapon type (See banner #4)
if next5star is None:
return True
weapon_type_counter = {}
characters = characters + (next5star,)
for char in characters:
weapon_type = CHARACTER_DATA[char][INDEX_WEAPON_TYPE]
if not weapon_type in weapon_type_counter:
weapon_type_counter[weapon_type] = 0
weapon_type_counter[weapon_type] += 1
return len(weapon_type_counter.keys()) >= 2
def rule_has_at_least_one_waifu(characters, next5star=None):
for char in characters:
if CHARACTER_DATA[char][INDEX_IS_WAIFU]:
return True
return False
# NOTE You can comment out rules like "No starter characters" to see what happens if that rule is actually not a thing
RULES = [
rule_no_starter_characters,
rule_no_duplicate_element,
rule_character_wasnt_on_the_last_banner,
rule_unique_combination,
rule_at_least_two_different_weapon_types_with_next_5_star,
rule_has_at_least_one_waifu,
]
def is_valid(characters, next5star=None, rules=RULES):
for rule in rules:
if not rule(characters, next5star):
return False
return True
def which_rule_failed(characters, next5star=None):
failed = []
for rule in RULES:
if not rule(characters, next5star):
failed.append(rule.__name__)
return failed
# scoring
# The higher the score the more likely it is a certain banner will appear
SCORE_CHARACTER_ABSENCE = 2
SCORE_FREE_CHARACTERS = -1
SCORE_FIRST_4STAR_REAPPEARANCE = 15
def score_character_being_absent(characters):
""" The longer a character has been absent the higher the overall score """
newest_eligible_banner_index = NEXT_BANNER_INDEX - 2
if not newest_eligible_banner_index in BANNER_DATA:
return False
character_scores = {}
score_counter = 1
for index in range(newest_eligible_banner_index, 0, -1):
group = BANNER_DATA[index]
group = filter_4_stars(group)
for char in characters:
if char in character_scores:
continue
if char in group:
character_scores[char] = score_counter
score_counter += SCORE_CHARACTER_ABSENCE
if len(character_scores.keys()) == 3:
break
for char in characters:
# if character hasn't ever appeared in a banner before they get awared the maximum points
if not char in character_scores:
character_scores[char] = len(BANNER_DATA.keys())
return sum(character_scores.values())
def score_free_character_reductions(characters):
total = 0
for char in characters:
if char in FREE_PERMANENT_CHARACTERS:
total += SCORE_FREE_CHARACTERS
return total
def score_first_4star_reappearance(characters):
def find_first_character_appearance(char):
for index in BANNER_DATA:
banner = BANNER_DATA[index]
if char in banner:
return index
return -1
for char in characters:
first_banner = find_first_character_appearance(char)
banners_since = NEXT_BANNER_INDEX - first_banner - 1
if banners_since == 2 or banners_since == 3:
# this can probably only apply to one character
return SCORE_FIRST_4STAR_REAPPEARANCE
return 0
SCORE_FUNCTIONS = [
score_character_being_absent,
score_free_character_reductions,
score_first_4star_reappearance,
]
def calculate_score(characters):
total = 0
for score_func in SCORE_FUNCTIONS:
total += score_func(characters)
return total
def sanity_check():
# idiot test, all old banners should be valid
rules_for_idiot_test = [
rule for rule in RULES if not rule.__name__ in [
"rule_character_wasnt_on_the_last_banner",
"rule_unique_combination",
"rule_character_shouldnt_be_in_shop",
"rule_no_duplicate_element_considering_next_5_star",
]
]
for banner_index in BANNER_DATA:
group = BANNER_DATA[banner_index]
if not is_valid(group, None, rules_for_idiot_test):
failed_rules = which_rule_failed(group)
print("ERROR: Banner", banner_index, group, "is apparently invalid? Check your rules...", failed_rules)
exit(1)
def calculate_next_banner(character_5star=None):
# get all character combinations
possible_combinations = combinations(CHARACTERS_4_STARS, 3)
valid_groups = []
for group in possible_combinations:
if is_valid(group, character_5star):
valid_groups.append((group, calculate_score(group)))
sorted_valid_groups = sorted(valid_groups, key=lambda group_with_value: group_with_value[1], reverse=True)
return sorted_valid_groups
def main():
print("# Genshin Banner Prediction Script", SCRIPT_VERSION, "by @atomicptr")
sanity_check()
character_5stars = NEXT_BANNER_5_STAR
if not isinstance(NEXT_BANNER_5_STAR, list):
character_5stars = [NEXT_BANNER_5_STAR]
for next5star in character_5stars:
print("\n## Banner prediction for rate up character:", next5star)
valid_groups = calculate_next_banner(next5star)
print("Found", len(valid_groups), "valid combinations.")
print("\nHighest scoring groups:")
top_groups = valid_groups[:5]
# find lowest score, we want to include all combinations that have the same score as well...
lowest_score = min([scored_pair[1] for scored_pair in top_groups])
# remove lowest score for now...
top_groups = [scored_pair for scored_pair in top_groups if scored_pair[1] != lowest_score]
# and finally readd them
top_groups = top_groups + [scored_pair for scored_pair in valid_groups if scored_pair[1] == lowest_score]
for group in valid_groups[:5]:
print(group)
if __name__ == "__main__":
main()