[Tutor] a ScoreList class (was: Multiple identical keys in a dictionary)

Bruce Sass bsass@freenet.edmonton.ab.ca
Sun, 27 May 2001 14:45:41 -0600 (MDT)


On Sun, 27 May 2001, wheelege wrote:

<...>
>   So I'm left thinking - what other data structure to use?  A class
for just the scores seems a little excessive...especially as I'm
already using a class to handle all the reading and writing to the
actual file and things like that.

Whether or not a class makes sense for a score list depends on what
you want to do with the scores, and should have nothing to do with how
many other classes you may have already devised for the program.

If all you are doing is keeping a list of scores which you want to
display every now and then, a simple list of tuples is probably best.
A dictionary looks attractive in general, but depends on having a
unique key for each entry to really shine... if you don't have a
unique key available you may end up losing any advantage dicts give
you through needing to handle collisions yourself, and manually
looking through the values of the dict to find whatever data you need.
i.e., It may be easier to generalize extracting data from a flat list
of tuples, where all datum are equal, than the case where one piece of
data is artifically elevated in importance and everything else must be
done in terms of a relationship to it.

An example is aways best...
(works with 1.5.2, lots of tweaks can be done to improve it,
especially if you are using a 2.x interpreter)

---8<--- scores.py ---
class Score:
    """Defines the basic properties of a 'score'."""
    def __init__(self, player=None, value=None, level=None):
        self.player = player
        self.value = value
        self.level = level
        # there may be other things to keep track of, like...
        #... is "level" the game play level or the players skill level
        #... when they finished.  Putting stuff into a class lets you
        #... add properties later on without needing to rewrite old
        #... code (new code uses the new stuff, old code doesn't even
        #... know it exists).

    def __cmp__(self, other):
        """Allows for comparison with other Score-s and numerics."""
        if isinstance(other, Score):
            cmpval = other.value
        else:
            cmpval = other
        if self.value < cmpval:
            return -1
        elif self.value == cmpval:
            return 0
        else:
            return 1

    # these are setup to have an ok display in this specific
    #... situation, you would probably want something a little
    #... fancier/better for an actual application.
    def __str__(self):
        return "%s %s (%s)" % \
               (self.player, self.value, self.level)

    def __repr__(self):
        return "Score(%s, %s, %s)" % \
               (self.player, self.value, self.level)


class ScoreList:
    """A high-level interface to a list of scores.

    You get:
    - "cutoff", so only the most deserving make it into the list
    - "+" operator, for an implementation independent interface
    - "top" method to easily extract the top scores
    - "name" and "level" methods, for extracting specific info

    """
    def __init__(self, scores=None, cutoff=None):
        if scores == None:
            self.values = []
        else:
            self.values = scores
        self.cutoff = cutoff

    def __add__(self, score):
        # I think this bit is most significant; it defines the
        #... semantics of adding to the list and is where you would
        #... do any 'magic' needed to ensure the list has the
        #... properties you want.  So far the only 'magic' is the
        #... disappearing score if it is below the cutoff.
        if self.cutoff == None or score > self.cutoff:
            self.values.append(score)
        return ScoreList(self.values, self.cutoff)

    def __str__(self):
        import string
        self.values.sort()
        return string.join(map(str, self.values), '\n')


    def top(self, number):
        self.values.sort()
        return ScoreList(self.values[-number:], self.cutoff)

    # These two methods are identical in form, and that form should
    #... be generalized so one can use any attribute as a key and get
    #... all the associated information back, IMO.
    def name(self, name):
        result = []
        for item in self.values:
            if item.player == name:
                result.append((item.value, item.level))
        return result

    def level(self, level):
        result = []
        for item in self.values:
            if item.level == level:
                result.append((item.player, item.value))
        return result


if __name__ == '__main__':
    # set things up
    from random import randint
    ri = randint

    # you need a score of 2500 to get into the hiscore list
    hiscores = ScoreList(cutoff=2500)

    # generate random test data...
    for i in range(3):
        for j in range(1,7):
            ascore = Score('player'+str(j), ri(0,5000), ri(1,10))
            hiscores = hiscores + ascore
    # ...with some specific properties...
    for i in range(3):
        for j in range(1,4):
            part = ri(0,5000), ri(1,10)
            hiscores = hiscores \
                       + Score('player'+str(j), part[0], part[1]) \
                       + Score('player'+str(j+3), part[0], part[1])
    # ...although you probably want something a little more thorough.

    # after going through all the above, use had better be simple...
    print "\nAll scores:\n", hiscores
    print "\nTop 3:\n", hiscores.top(3)
    print "\nTop 10:\n", hiscores.top(10)
    print "\nBy player..."
    for i in range(1,6):
        name = 'player' + str(i)
        print name + ": ", hiscores.name(name)
    print "\nBy level..."
    for i in range(1,11):
        print "level" + str(i) + ": ", hiscores.level(i)

--->8---

Overkill???, sure... unless, for example:  you have written the next
wildly popular networked game and have a thousand dedicated users
asking you for a way to analyse hiscore lists, or pestering you
because they don't like the hardcoded default you think looks great;
perhaps you just don't want to re-write or integrate(-yet-again) a
score list subsystem everytime you code up a game, having all the core
bits in a class or two make for easy code re-use; or maybe you want to
allow for others to expand on what you have done, so they can have
their own fancy hiscore display, without having to re-write your code
and risk introducing incompatibilities.

Generally, I think you will find that as programs get larger, and the
user base increases, it becomes more and more attractive to use
classes.  Simply because it is easier to modify the behaviour of a
class than to track down and change a convention you scattered
throughout your code, or keep track of a bunch of stand-alone
functions and data structure declarations when you need to change or
replicate some behaviour.

Food for thought, I hope.


- Bruce