Quick actions

cmd+k|ctrl+k

Navigation

Languages

Genshin Banner 4* Prediction Script v1.5

Snippet info

Language

Python

Visibility

public

Author

atomicptr

Created

2021-01-10T12:50:33Z

Updated

2021-10-28T16:20:00.608971Z

#!/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()
INFO