示例#1
0
def generate_national_spells(targetnumberofnationalspells: int, spelleffects: Dict[str, SpellEffect],
                             researchmod: int,
                             alreadygeneratedeffectsatlevels: Dict[int, List[str]], generatedspells: List[Spell],
                             nationstogeneratefor: List[int], options: Dict[str, str]):
    _writetoconsole("Generating national spells...\n")
    if targetnumberofnationalspells < 1:
        return

    # Initialize national mage map
    nationcount = len(nationstogeneratefor)
    print(f"Nations to generate for: {nationstogeneratefor}")
    index = 0  # used for progress report
    for nationid, nation in nationals.nations.items():
        if nationid not in nationstogeneratefor:
            continue
        beginningnationlogtext = f"Progress: Beginning nation {index} of {nationcount}...\n"
        if index % 20 == 0:
            _writetoconsole(beginningnationlogtext)
        DebugLogger.debuglog(beginningnationlogtext, debugkeys.NATIONALSPELLGENERATION)
        index += 1

        _generate_spells_for_nation(
            nation=nation,
            researchmod=researchmod,
            spelleffects=spelleffects,
            alreadygeneratedeffectsatlevels=alreadygeneratedeffectsatlevels,
            generatedspells=generatedspells,
            options=options,
            targetnumberofnationalspells=targetnumberofnationalspells
        )

    _writetoconsole(
        f"Attempted to generate {targetnumberofnationalspells} national spells per nation for {nationcount} nations.\n")
    NationalSpellGenerationInfoCollector.print()
示例#2
0
文件: nation.py 项目: Logg-y/magicgen
 def get_commander_with_path(self, path: int) -> Union[NationalMage, None]:
     random.shuffle(self.mages)
     for mage in self.mages:
         DebugLogger.debuglog(
             f"Testing {mage.to_text()} for {utils.pathstotext(path)}: {mage.has_access_to_path(path)}",
             debugkeys.MAGESELECTIONFORPATHFORNATIONALSPELL)
         if mage.has_access_to_path(path):
             return mage
     raise ValueError(
         f"Could not find mage for path {utils.pathstotext(path)} in {self.to_text()}"
     )
示例#3
0
文件: nation.py 项目: Logg-y/magicgen
    def get_pathweights(self) -> Dict[int, int]:
        mageweights: Dict[
            NationalMage,
            float] = self._get_natspell_weight_distribution_for_mages()
        weights: Dict[int,
                      float] = self._calculate_raw_pathweights(mageweights)
        weights: Dict[int, int] = {i: int(round(weights[i])) for i in weights}

        DebugLogger.debuglog(
            f"Pathweights for nation {self.name} (ID{self.id})\n"
            f"Mages:{[i.to_text() for i in self.mages]}\n"
            f"MageWeights: {mageweights}\n"
            f"Weights:{[str(utils.pathstotext(i)) + ' ' + str(weights[i]) + ', ' for i in weights]}",
            debugkeys.NATIONALSPELLGENERATIONWEIGHTING)
        return weights
示例#4
0
def _choose_effect(effectpool: Dict[str, SpellEffect], primarypath: int, alreadygeneratedeffectsatlevels: Dict[int, List[str]],
                   researchlevel: int) -> SpellEffect:
    availableeffects = list(filter(lambda x: ((primarypath & x.paths) != 0) and  # Matching path
                                             (x.name not in alreadygeneratedeffectsatlevels[researchlevel]),
                                   # generic with same effect not already existing in same researchlevel
                                   effectpool.values()))
    if len(availableeffects) == 0:
        raise ValueError("No Spelleffect found available")

    choseneffect: Union[SpellEffect] = availableeffects[random.randrange(0, len(availableeffects))]
    DebugLogger.debuglog(f"Selected effect: {choseneffect.name}\n"
                       f"Primary path: {utils.pathstotext(primarypath)}\n"
                       f"Already generated effects: {alreadygeneratedeffectsatlevels}\n"
                       f"Current research level: {researchlevel}", debugkeys.NATIONALSPELLGENERATION)
    return choseneffect
示例#5
0
def _select_research_level(researchmod: int, generatedeffectsatlevels: Dict[int, List[str]]) -> int:
    researchlevelstotry: List[int] = []
    for level in range(1, 10):
        if level in generatedeffectsatlevels:
            duplicates = 5 - abs(5 - level)
            realLevel = level + researchmod
            for i in range(0, duplicates):
                researchlevelstotry.append(realLevel)

    random.shuffle(researchlevelstotry)
    researchlevel = researchlevelstotry.pop(0)
    if len(researchlevelstotry) == 0:
        raise ValueError(f"Failed to select research level for spell")
    DebugLogger.debuglog(f"Selecting researchlevel {researchlevel}", debugkeys.NATIONALSPELLGENERATION)
    return researchlevel
示例#6
0
def _roll_path_for_national_spell(nation: Nation) -> int:
    pathweights = nation.get_pathweights()
    totalweights = 0
    for i, weight in pathweights.items():
        totalweights += weight
    if totalweights == 0:
        raise ValueError(f"Failed calculating national spell path weights.\n"
                         f"Nation: {nation.to_text()}\n"
                         f"Weights: {pprint.pformat(pathweights)}\n"
                         f"Total Weight: {totalweights}")
    initialroll = roll = random.randrange(0, totalweights, 1)
    for path, weight in pathweights.items():
        if roll < weight:
            DebugLogger.debuglog(f"Selected path {utils.pathstotext(path)} from weights {pathweights.items()}, "
                               f"rolled {initialroll}",
                                 debugkeys.NATIONALSPELLGENERATION,
                                 debugkeys.NATIONALSPELLGENERATIONWEIGHTING)
            return path
        roll -= weight
    raise ValueError(f"Attempted and failed to roll for weighted path\n  Rolled {initialroll}\n  Total weight: "
                     f"{totalweights}\n  Weights: {pprint.pformat(pathweights)}\n")
示例#7
0
    def _calculate_pathweight_proportions(self):
        DebugLogger.debuglog(
            f"Generating pathweights for mage {self.to_text()}",
            debugkeys.NATIONALSPELLGENERATIONWEIGHTING)
        self.pathweightsinitialised = True
        for i in range(0, 8):
            self.pathweights[2**i] = self.get_average_level_in_path(2**i)**1.5
        DebugLogger.debuglog(
            f"Average levels (default weight) in paths: " + str([
                utils.pathstotext(i) + " " + str(self.pathweights[i])
                for i in self.pathweights
            ]), debugkeys.NATIONALSPELLGENERATIONWEIGHTING)
        s = sum(self.pathweights.values())
        DebugLogger.debuglog(f"Total weights sum {s}",
                             debugkeys.NATIONALSPELLGENERATIONWEIGHTING)
        # Avoid ZeroDivisionError for non mages
        if s == 0.0:
            DebugLogger.debuglog(f"No weight, therefore skipping adjustment",
                                 debugkeys.NATIONALSPELLGENERATIONWEIGHTING)
            return
        for pathflag, weight in self.pathweights.items():  # Normalize
            self.pathweights[pathflag] = float(weight) / s

        DebugLogger.debuglog(
            f"Adjusted weights: " + str([
                utils.pathstotext(i) + " " + str(self.pathweights[i])
                for i in self.pathweights
            ]), debugkeys.NATIONALSPELLGENERATIONWEIGHTING)
        s = sum(self.pathweights.values())
        DebugLogger.debuglog(f"Total weights sum {s}",
                             debugkeys.NATIONALSPELLGENERATIONWEIGHTING)
示例#8
0
    def _compatibility(self, eff, modifier, researchlevel):
        # Skipchance is done by the main processing loop now
        # it makes determining if there are legal modifiers for a spell a LOT better
        DebugLogger.debuglog(f"Begin secondary compatibility for {self.name} and {eff.name} "
                             f"with mod {modifier.name} at RL {researchlevel}", debugkeys.SECONDARYEFFECTCOMPATIBILITY)

        # see if the event list allows this unitmod
        # if yes then we need to be allowed, ignoring all the other reqs
        if eff.eventset is not None:
            realeventset = eventsets[eff.eventset]
            if self.unitmod in realeventset.allowedunitmods:
                DebugLogger.debuglog(f"Secondary is valid: allowed unit mod for eventset",
                                     debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                return True
            if realeventset.unitmodlist is not None:
                unitmodlist = utils.unitmodlists[realeventset.unitmodlist]
                if self.unitmod in unitmodlist:
                    DebugLogger.debuglog(f"Secondary is valid: this unitmod is in unitmodlist for eventset",
                                         debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                    return True

        if self.nextspell != "" and eff.noadditionalnextspells > 0:
            DebugLogger.debuglog(f"Secondary is invalid: this effect does not allow nextspells",
                                 debugkeys.SECONDARYEFFECTCOMPATIBILITY)
            return False

        if self.requiredresearchlevel is not None and self.requiredresearchlevel != researchlevel:
            DebugLogger.debuglog(f"Secondary is invalid: required research level is {self.requiredresearchlevel}",
                                 debugkeys.SECONDARYEFFECTCOMPATIBILITY)
            return False

        finalpower = researchlevel + self.power + modifier.power

        if self.anysummon:
            if eff.effect in [1, 10001, 10050, 10038, 21, 10021]:
                if eff.effect in [1, 10001, 10050, 10038, 21]:
                    pass
                elif eff.effect in [10021]:
                    # Block weapon mods on permanent summon commanders
                    if self.unitmod != "":
                        unitmod = unitmods[self.unitmod]
                        if unitmod.weaponmod != "":
                            DebugLogger.debuglog(
                                f"Secondary is invalid: no weapon mods allowed on permanent summon commanders",
                                debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                            return False
            else:
                DebugLogger.debuglog(f"Secondary is invalid: secondary requires summon, but this spell effect is not",
                                     debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                return False
            # Calculate what final power this should be, accounting for magic value and chassis value mismatches
            # This is so squishy human mages don't get pushed up to really high research for simple modifications
            # that don't really affect their combat ability
            if eff.chassisvalue is not None:
                thispower = self.calcModifiedPowerForSpellEffect(eff)
                finalpower = researchlevel + thispower + modifier.power


        if eff.isnextspell and self.name != "Do Nothing":
            DebugLogger.debuglog(f"Secondary is invalid: this is a nextspell, and nextspells are only allowed Do Nothing",
                                 debugkeys.SECONDARYEFFECTCOMPATIBILITY)
            return False

        # do not give nextspells secondary effects as they don't respect the path requirements the way the main spell does
        # oh and it could chain to ridiculous levels like "add a set on fire effect" "add an entangle to that set on fire" etc etc etc

        okay = self.paths == 0
        for flag in utils.breakdownflagcomponents(self.paths):
            if (eff.paths & flag):
                okay = True
                break
        if not okay:
            DebugLogger.debuglog(f"Secondary is invalid: effect paths {eff.paths} do not contain required paths {self.paths}",
                                 debugkeys.SECONDARYEFFECTCOMPATIBILITY)
            return False

        if self.nobattlefield:
            if 660 <= eff.aoe <= 670:
                DebugLogger.debuglog(f"Secondary is invalid: not allowed on battlefield wide effects",
                                     debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                return False

        for flag in utils.breakdownflagcomponents(self.spelltype):
            if not (eff.spelltype & flag):
                DebugLogger.debuglog(f"Secondary is invalid: effect's spelltype missing flag {flag}",
                                     debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                return False

        # Check #reqs
        for r in self.reqs:
            if not r.test(eff):
                DebugLogger.debuglog(f"Secondary is invalid: failed req {str(r)}",
                                     debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                return False

        # Make sure that various things cannot be pushed out of range
        finalrange = self.range + modifier.range + (eff.range % 1000)
        if finalrange < 0:
            DebugLogger.debuglog(f"Secondary is invalid: final range would be negative",
                                 debugkeys.SECONDARYEFFECTCOMPATIBILITY)
            return False

        cast = (100 if eff.casttime is None else eff.casttime) + modifier.casttime
        finalcast = self.casttime + cast
        if finalcast < 5:
            DebugLogger.debuglog(f"Secondary is invalid: final cast time would be {finalcast} (<5% cast time)",
                                 debugkeys.SECONDARYEFFECTCOMPATIBILITY)
            return False


        # power extremes don't matter for holy spells
        # do nothing should also always be allowed
        if not eff.isnextspell and self.name != "Do Nothing":
            if finalpower < max(0, eff.power) and eff.paths != 256:
                DebugLogger.debuglog(f"Secondary is invalid: final power level of {finalpower} below effect minimum of {eff.power}",
                                     debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                return False
            if finalpower > eff.maxpower and eff.paths != 256:
                DebugLogger.debuglog(f"Secondary is invalid: final powerlevel {finalpower} above effect max of {eff.maxpower}",
                                     debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                return False

        finalnreff = self.nreff + modifier.nreff + (eff.nreff % 1000)
        if finalnreff <= 0:
            DebugLogger.debuglog(f"Secondary is invalid: final number of effects {finalnreff} must be positive",
                                 debugkeys.SECONDARYEFFECTCOMPATIBILITY)
            return False

        finalaoe = self.aoe + modifier.aoe + (eff.aoe % 1000) + (eff.aoe // 1000)*eff.pathlevel
        if finalaoe < 0:
            DebugLogger.debuglog(f"Secondary is invalid: final aoe {finalaoe} must not be negative",
                                 debugkeys.SECONDARYEFFECTCOMPATIBILITY)
            return False

        finalbounces = self.maxbounces + eff.maxbounces + modifier.maxbounces
        if finalbounces < 0:
            DebugLogger.debuglog(f"Secondary is invalid: final maxbounces {finalbounces} must not be negative",
                                 debugkeys.SECONDARYEFFECTCOMPATIBILITY)
            return False

        finalpathlevel = self.pathlevel + eff.pathlevel + modifier.pathlevel
        # skip for holy
        if eff.paths != 256:
            if finalpathlevel <= 0 and eff.pathlevel > 0:
                DebugLogger.debuglog(f"Secondary is invalid: finalpathlevel {finalpathlevel} not positive",
                                     debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                return False

        if self.unitmod != "":
            umod = unitmods[self.unitmod]
            if not umod.compatibilityWithSpellEffect(eff):
                DebugLogger.debuglog(f"Secondary is invalid: unitmod incompatible with effect",
                                     debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                return False

        extraresearch = researchlevel - finalpower
        actualpowerlvl = (researchlevel - eff.power) + self.power + modifier.power
        if not eff.canGenerateAtPowerlvl(actualpowerlvl, modifier, self):
            return False

        scaleamt = eff.scalerate * ((actualpowerlvl * (actualpowerlvl + 1)) / 2)
        if eff.spelltype & SpellTypes.POWER_SCALES_AOE:
            finalaoe += scaleamt

        # aoe limit so that mass rust doesn't get decay/burning etc
        # don't apply to holy spells as (at research 0) they need to be allowed this
        if self.scalingaoelimit is None and self.offensiveeffect != 0 and eff.paths != 256:
            scalingaoelimit = 3
        else:
            scalingaoelimit = self.scalingaoelimit
        if scalingaoelimit is not None:
            if finalaoe > 600:
                finalaoe = 50
            maxbaseaoe = scalingaoelimit * ((researchlevel * (researchlevel + 1)) / 2)
            # this is quadratic, negative power differentials (from slower casting etc) should be considered 0
            finalpower = max(0, finalpower)
            print(f"scalingaoelimit: {scalingaoelimit} at rl{researchlevel} has maxbaseaoe {maxbaseaoe}, "
                  f"spell has {finalaoe}, scaleamt was {scaleamt}")
            if finalaoe > maxbaseaoe:
                DebugLogger.debuglog(f"Secondary is invalid: failed scalingaoelimit",
                                     debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                return False

        if self.reqdamagingeffect is not None:
            if self.reqdamagingeffect:
                if not utils.isDamagingSpellEffect(eff):
                    DebugLogger.debuglog(
                        f"Secondary is invalid: spell effect is not damaging",
                        debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                    return False
            else:
                if utils.isDamagingSpellEffect(eff):
                    DebugLogger.debuglog(
                        f"Secondary is invalid: spell effect is damaging",
                        debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                    return False

        if self.ondamage:
            curr = eff
            while 1:
                # look for existing on damage effects, having multiple in the same spell
                # does not work correctly
                if isinstance(curr, str):
                    curr = spelleffects[curr]
                if curr.spec & 0x1000000000000000:
                    DebugLogger.debuglog(
                        f"Secondary is invalid: this spell already has an ondamage effect",
                        debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                    return False
                if curr.effect > 1000:
                    DebugLogger.debuglog(
                        f"Secondary is invalid: ondamage effect not compatible with lingering spell effect",
                        debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                    return False
                if curr.nextspell is not None and curr.nextspell != "":
                    curr = curr.nextspell
                    continue
                # this does currently not work on chain lightning effects (134)
                if (not utils.isDamagingSpellEffect(curr)) or curr.effect % 1000 == 134:
                    DebugLogger.debuglog(
                        f"Secondary is invalid: is ondamage secondary, spell effect chain lightning or nondamaging",
                        debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                    return False
                break

        # Modifiers cannot check this including the secondary as well, this is
        # because modifiers come first: secondaries need to also check the modifier value
        maxfinalfatiguecost = self.maxfinalfatiguecost
        if modifier.maxfinalfatiguecost is not None:
            if self.maxfinalfatiguecost is None:
                maxfinalfatiguecost = modifier.maxfinalfatiguecost
            else:
                maxfinalfatiguecost = min(modifier.maxfinalfatiguecost, self.maxfinalfatiguecost)

        minfinalfatiguecost = self.minfinalfatiguecost
        if modifier.minfinalfatiguecost is not None:
            if self.minfinalfatiguecost is None:
                minfinalfatiguecost = modifier.minfinalfatiguecost
            else:
                minfinalfatiguecost = max(modifier.minfinalfatiguecost, self.minfinalfatiguecost)

        if maxfinalfatiguecost is not None or minfinalfatiguecost is not None:
            finalfatigue = eff.calculateExpectedFinalFatigue(researchlevel, modifier, self)

            if maxfinalfatiguecost is not None and finalfatigue >= maxfinalfatiguecost:
                print(f"maxfinalfatiguecost of {finalfatigue} too high (vs {maxfinalfatiguecost})")
                return False
            if minfinalfatiguecost is not None and finalfatigue < minfinalfatiguecost:
                print(f"minfinalfatiguecost of {finalfatigue} too low (vs {maxfinalfatiguecost})")
                return False




        if self.minfinalaoe is not None:
            if eff.spelltype & SpellTypes.POWER_SCALES_AOE:
                finalaoe = eff.aoe % 1000 + (eff.aoe // 1000 * eff.pathlevel)
                finalaoe += round(eff.scalerate * ((actualpowerlvl*(actualpowerlvl+1))/2), 0)
            else:
                finalaoe = eff.aoe
            if finalaoe < self.minfinalaoe:
                DebugLogger.debuglog(f"Secondary is invalid: finalaoe {finalaoe} below specified min of {self.minfinalaoe}",
                                     debugkeys.SECONDARYEFFECTCOMPATIBILITY)
                return False

        DebugLogger.debuglog(f"Secondary is valid!",
                             debugkeys.SECONDARYEFFECTCOMPATIBILITY)
        return True
示例#9
0
def _generate_spells_for_nation(nation: Nation, researchmod: int, spelleffects: Dict[str, SpellEffect],
                                alreadygeneratedeffectsatlevels: Dict[int, List[str]], generatedspells: List[Spell],
                                targetnumberofnationalspells: int, options: Dict[str, str]):
    DebugLogger.debuglog(f"Generating spells for nation: {nation.to_text()}",
                         debugkeys.NATIONALSPELLGENERATION)
    if not nation.has_mages():
        _writetoconsole(f"Skipping nation {nation.to_text()} because no national mages were found\n")
        return
    availableeffectpool = copy.copy(spelleffects)
    while len(nation.nationalspells) < targetnumberofnationalspells:
        primarypath: int = _roll_path_for_national_spell(nation)
        DebugLogger.debuglog(f"Attempting to generate for primary path {utils.pathstotext(primarypath)}\n",
                             debugkeys.NATIONALSPELLGENERATION)

        # Select a commander to generate this spell for
        commander: NationalMage = nation.get_commander_with_path(primarypath)

        # calculate if blood shall be allowed as path in the spell
        allowblood = commander.can_have_blood()

        # Select effect for spell
        DebugLogger.debuglog(f"Selecting national spell effect\n", debugkeys.NATIONALSPELLGENERATION)

        researchlevel = _select_research_level(researchmod, alreadygeneratedeffectsatlevels)
        try:
            choseneffect = _choose_effect(
                effectpool=availableeffectpool,
                primarypath=primarypath,
                alreadygeneratedeffectsatlevels=alreadygeneratedeffectsatlevels,
                researchlevel=researchlevel
            )
            # Only one attempt an effect per nation
            del availableeffectpool[choseneffect.name]
        except ValueError:
            raise ValueError(
                f"Couldn't make a national spell for nation {nation.name} (ID:{nation.id})\n"
                f"Primarypath={utils.pathstotext(primarypath)}\n"
                f"Researchlevel={researchlevel}\n "
                f"Available effects: {availableeffectpool}\n"
                f"No effect available\n")

        DebugLogger.debuglog(
            f"Try generating national spell for nation {nation.id} with effect {choseneffect.name}, "
            f"primarypath={utils.pathstotext(primarypath)}, secondaries={commander.get_total_possible_paths_mask()}, there are "
            f"{len(availableeffectpool)} effects available\n", debugkeys.NATIONALSPELLGENERATION)
        spell = _try_to_generate_a_national_spell(
            nation=nation,
            spelleffect=choseneffect,
            researchlevel=researchlevel,
            primarypath=primarypath,
            allowblood=allowblood,
            options=options,
            secondarypathoptions=commander.get_total_possible_paths_mask()
        )
        if spell is None:
            DebugLogger.debuglog(f"Failed to generate spell for effect {choseneffect.name}\n",
                                 debugkeys.NATIONALSPELLGENERATION)
        else:
            generatedspells.append(spell)
            nation.register_national_spell(spell)
            NationalSpellGenerationInfoCollector.numberofgeneratedspells += 1
            DebugLogger.debuglog("Spell successfully generated\n", debugkeys.NATIONALSPELLGENERATION)