6
\$\begingroup\$

I recently started replaying Final Fantasy XII. One of it's optional bosses, Zalera, has, among others, the following mechanics:

His attack sequence is Lv. 2 Sleep, Lv. 3 Disable, Lv. 4 Break and Lv. 5 Reverse, affecting characters whose levels are divisible by 2, 3, 4 or 5, respectively, and then Prime Lv. Death, which affects characters whose levels are a prime number.

Character levels not affected by any of these spells are levels 49, 77, and 91. Levels (under 100) susceptible to Prime Lv. Death are: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, and 97.

To calculate which levels were affected by what attack, I wrote a calculator. It takes 1 to 6 numbers between 1 and 99 inclusive and reports what will affect the player characters with those numbers. Since there's at maximum 6 player characters under the player's control, I limited the amount to a full party size of 6.

Code:

from argparse import ArgumentParser


class ZaleraCalculator:
    def __init__(self, vars):
        self.vars = vars
        self.vulnerable = {
            2: "Sleep",
            3: "Disable",
            4: "Break",
            5: "Reverse"
        }

    def isDivisibleByN(self, x, n):
        if x % n == 0:
            print("Divisible by {}, vulnerable to {}."
                  .format(n, self.vulnerable[n]))

    def isPrime(self, x):
        # Primality test, works good enough for values this low
        if 2 in [x, pow(2, x, x)]:
            print("Prime, vulnerable to Death.")

    def generateReport(self):
        for i in self.vars:
            print("\nCharacter with Lvl: {}".format(i))
            if i < 1 or i > 99:
                print("Arguments not between 1 and 99 inclusive are illegal.")
            else:
                if i < 40:
                    print("Character level lower than Zalera's level, 40.")
                self.isDivisibleByN(i, 5)
                self.isDivisibleByN(i, 4)
                self.isDivisibleByN(i, 3)
                self.isDivisibleByN(i, 2)
                self.isPrime(i)


def main():
    parser = ArgumentParser(description='FFXII Zalera calculator')
    parser.add_argument(
        'vars',
        type=int,
        help="One or more character levels, excluding guest characters.",
        nargs='*'
        )
    args = parser.parse_args()

    if len(args.vars) > 6:
        print(
        "Too many arguments were provided. Guest characters are irrelevant.")
    elif not args.vars:
        print("No arguments were provided.")
    else:
        calc = ZaleraCalculator(args.vars)
        calc.generateReport()

if __name__ == '__main__':
    main()

Example usage:

python ffxii.py 38 45 47 41 49 60

Example output:

Character with Lvl: 38
Character level lower than Zalera's level, 40.
Divisible by 2, vulnerable to Sleep.

Character with Lvl: 45
Divisible by 5, vulnerable to Reverse.
Divisible by 3, vulnerable to Disable.

Character with Lvl: 47
Prime, vulnerable to Death.

Character with Lvl: 41
Prime, vulnerable to Death.

Character with Lvl: 49

Character with Lvl: 60
Divisible by 5, vulnerable to Reverse.
Divisible by 4, vulnerable to Break.
Divisible by 3, vulnerable to Disable.
Divisible by 2, vulnerable to Sleep.

Line 51 violates PEP8, but all alternatives I could think of wouldn't be any better. There's a couple of magic numbers in the code, notably 6, 40 and 99, but since those are never going to change I'm not inclined to put them into pseudo-constants.

The program is 'dumb', it doesn't remember much. So regardless of whether a number is divisible by 4, it will also check whether it's divisible by 2. It's written in a straight-forward way and can probably be written in a more generic manner. However, I couldn't figure out how without overly complicating the program. A smarter approach with a Report class containing all results would probably be better. Prime not needing it's own output method would probably be better too, but I couldn't figure out how to merge it into the dictionary I use for the other vulnerabilities.

Since the player can't reach a level higher than 99, values above are invalid. Because 99 is a relatively small number, a cheap prime test has been used instead of a Sieve.

Any and all feedback is appreciated.

\$\endgroup\$
6
\$\begingroup\$

I think that using a class here is overkill. You don't need to maintain any state, you just need to analyze the values provided by the user. Moreover, primality or divisability testing have almost nothing to do with this class, they are just tools it uses and should not be methods.

So I would write these functions, one of them using a constant:

SPELLS = {
    2: "Sleep",
    3: "Disable",
    4: "Break",
    5: "Reverse",
}


def usable_spells(level):
    for spell_level, effect in SPELLS.iteritems():
        if level % spell_level == 0:
            yield spell_level, effect


def is_prime(x):
    # Primality test, works good enough for values this low
    return 2 in [x, pow(2, x, x)]


def generate_report(character_levels, zalera_level=40):
    for level in character_levels:
        print "\nCharacter with Lvl:", level
        if level < zalera_level:
            print "Character level lower than Zalera's level,", zalera_level
        for spell in usable_spells(level):
            print "Divisible by {}, vulnerable to {}.".format(*spell)
        if is_prime(level):
            print "Prime, vulnerable to Death."

I changed a couple of things:

  • used Python's naming conventions (snake_case);
  • removed some calls to format because you can get the same result using plain old print;
  • iterated through the spells using a loop rather than manually enumerating all the values (you can force the top-down ordering using sorted(SPELLS.iteritems(), reverse=True);
  • added Zalera's level as a parameter with a default value, to name it;
  • removed the check for level correctness as we can ask argparse to do it for us.

Now let's look at how to build a better command-line parser.

First off, nargs can take the value '+' to indicate that at least one value is required. Second, instead of printing the error yourself (to stdout!), you can tell argparse that you encountered an error analyzing its results: just call error() on your parser object.

Lastly, you may want to make use of the choices parameter on add_argument as it allow you to pass an iterable of values that will be considered acceptable by argparse; the rest being invalid. In the end, I would write:

def parse_command_line():
    parser = ArgumentParser(description='FFXII Zalera calculator')
    parser.add_argument(
        "levels",
        type=int,
        help="One or more character levels, excluding guest characters.",
        nargs="+",
        choices=xrange(1, 100),
        metavar="CHARACTER_LEVEL")
    args = parser.parse_args()

    if len(args.levels) > 6:
        parser.error(
            "Too many arguments were provided. "
            "Guest characters are irrelevant.")
    return args.levels


if __name__ == "__main__":
    generate_report(parse_command_line())

The metavar parameter is here to avoid that argparse prints the whole set of choices on the help message. Also note the way to break too long strings literal: as long as there is a line continuation (implicit such as inside parenthesis or explicit using \ at the end of a line) consecutive string literals will be considered the same string.

\$\endgroup\$
  • \$\begingroup\$ On which mathematical principle is the prime check function based? I have never seen one prime check like that. \$\endgroup\$ – Caridorc Nov 7 '16 at 20:34
  • 3
    \$\begingroup\$ @Caridorc Sounds like Fermat's little theorem. \$\endgroup\$ – Mathias Ettinger Nov 7 '16 at 20:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.