I normally don’t share little strange bits of unfinished code – but I thought this was pretty cool. I was messing around with coding a little “fish tank” game in Python utilizing Pygame. One thing I’m bad about doing is attempting to make everything modular – which is why my Text Adventure game turned out to be a Text Adventure Engine. I wanted to utilize Pygame, but I wanted to load in sprite sheets in a predictable manner. In this case I found some sprites that are generally utilized for RPG Maker that always contain a predictable pattern (Multiple sprites on the same sheet, differing only by color). Given that there are so many available due to its popularity, it also makes things easy to play with. I believe there are licensing problems here, but this is just for personal use as an example. You could utilize any sheets that are uniform in size. Here’s a little example of one if those sheets.

I found some example code to use as a base, but it contained errors and didn’t account for multiple sprites in the same sheet. Below is the SpriteSheet class. Essentially – this code reads the size of the total image in pixels, and you pass in the number of columns/rows (distinct cells groupings of blue fish, green fish, etc.). In the image above, that would be two rows by four columns. After that, some simple (sort of) math determines the size of each fish individually (I believe this example is 32px by 32px). Essentially – this means you can load in any sprite sheet that follows this same pattern (regardless of size), and this class can take account of it. Really it’s just kind of cool, it minimizes the code a bit.

class SpriteSheet():
    
    def __init__(self, filename, o_rows, o_cols, rows, cols, p_rows, p_cols):
        """SpriteSheet Contructor"""
        self.image = pg.image.load(filename).convert_alpha()
        self.o_rows = o_rows    #Number of rows of options in sprite total
        self.o_cols = o_cols    #Number of cols of options in sprite total
        self.rows = rows        #Number of rows for sprite chosen
        self.cols = cols        #Number of cols for sprite chosen
        self.p_rows = p_rows    #Row number of chosen sprite in sheet
        self.p_cols = p_cols    #Column number of chosen sprite in sheet
        
        self.totalCellCount = cols * rows #Total cells in the specific sprite option chosen
        
        self.option_w = self.image.get_rect().width / self.o_cols #Width of particular option in sheet
        self.option_h = self.image.get_rect().height / self.o_rows #Height of particular option in sheet
        
        self.offset_x = self.option_w * p_cols #Offset the X based on which option in the sheet
        self.offset_y = self.option_h * p_rows #Offset the Y based on which option in the sheet
        
        self.init_rect = pg.Rect(0, 0, self.option_w, self.option_h) #Form initial rectangle (before we break it up into cells)

        self.w = self.cellWidth = self.init_rect.width / cols
        self.h = self.cellHeight = self.init_rect.height / rows
        
        #Get a list of (X,Y) coordinates of each cell in the sprite sheet with associated heights/widths
        #self.cells = list([((index % cols * self.w) + self.offset_x , (index / cols * self.h) + self.offset_y, self.w, self.h) for index in range(self.totalCellCount)])
        #self.cells = list([((index % cols * self.w), (index % rows * self.h), self.w, self.h) for index in range(self.totalCellCount)])
        
        self.cells = []
        myCounter = 0
        
        for ry in range(0, rows):
            for rx in range(0,cols):
                self.cells.append(((myCounter % cols * self.w) + self.offset_x, (ry * self.h) + self.offset_y, self.w, self.h))
                myCounter += 1
                #print(self.cells)
        
    def draw(self, cellIndex = 0, x = 25, y = 25):
        self.rect = pg.Rect(x,y,self.w,self.h) #Form rectangle around cell
        #pg.draw.rect(screen,(100,100,100),self.rect,0) #debug, show rectangles
        screen.blit(self.image, (x, y), self.cells[cellIndex])

You’ll notice a bit of debugging code that is disabled as it’s not 100% polished, but it works. In my Main function, I draw a randomized selection of fish. Really this means I’m choosing a sprite sheet and picking a random (X,Y) of the sheet which corresponds to (Row,Column) and therefore am essentially randomizing the color and number of fish to appear.

def main():
    
    #My Fish
    fish = []
    
    #Tiny Fishes
    for _ in range(10):
        fish.append(Fish(1,"../Imgs/FishSheet3.png",2,4,4,3,random.randint(0,1),random.randint(0,3)))
        
    #Smallish Fishes
    for _ in range(3):
        fish.append(Fish(1,"../Imgs/FishSheet1.png",2,4,4,3,random.randint(0,1),random.randint(0,3)))
        
    #Medium Fishes
    for _ in range(3):
        fish.append(Fish(1,"../Imgs/FishSheet4.png",2,4,4,3,random.randint(0,1),random.randint(0,3)))
    
    for f in fish:
        #Draw all fish on the screen
        f.draw(x = random.randrange(0,screen.get_width()), y = random.randrange(0, screen.get_height()))
    
    while True:
        event = pg.event.poll()
        if event.type == pg.QUIT:
            return
        
        if event.type == pg.MOUSEBUTTONDOWN:
            # Set the x, y postions of the mouse click
            x, y = event.pos
            for f in fish:
                if f.rect.collidepoint(x, y):
                    print("collide!")
        
        for f in fish:
            f.move()
        
        pg.display.update()
        
        clock.tick(30) #never run more than 30fps
        
        screen.fill((30, 30, 30))
    
if __name__ == "__main__" :
    pg.init()
    main()
    pg.quit()

The collision code is just a placeholder in case I need it later for interaction. Here’s where things took a weird turn. I thought to myself… well fish don’t really always move at a predictable speed or direction. So I need to randomize both of those depending on the size of the fish Obviously they will move Right/Left more often than Up/Down so I weight those first. Then I randomized the length of travel before switching directions.

To be honest this also highlights a big problem with the current version of the code, my sprites are also highly predictable in terms of what specific cell represents up/down/left/right. Without that, I would have to pass the values into each object created which would be annoying – so ignore that problem and make them static.

class WeightedChoice(object):
    def __init__(self, weights):
        self.totals = []
        self.weights = weights
        running_total = 0

        for w in weights:
            running_total += w[1]
            self.totals.append(running_total)

    def next(self):
        rnd = random.random() * self.totals[-1]
        i = bisect.bisect_right(self.totals, rnd)
        return self.weights[i][0]

class Fish(SpriteSheet):
    
    rightPos = 6   #Image index to use while moving right
    leftPos = 3    #Image index to use while moving left
    upPos = 9      #Image index to use while moving up
    downPos = 0    #Image index to use while moving down
    
    switchImg = 6  #Switch image after every 'x' moves
    
    def __init__(self, speed, filename, o_rows, o_cols, rows, cols, p_rows, p_cols):
        super().__init__(filename, o_rows, o_cols, rows, cols, p_rows, p_cols)

        self.speed = speed
        self.traveled = 0
        self.randomizeTravel()
        self.randomizeDirection()
        
        #Set current index of sprite - tried to randomize so that all fins weren't doing the same thing
        self.curRightPos = random.randint(Fish.rightPos,8)
        self.curLeftPos = random.randint(Fish.leftPos,5)
        self.curUpPos = random.randint(Fish.upPos,11)
        self.curDownPos = random.randint(Fish.downPos,3)
        
        
    def randomizeTravel(self):
        self.traveled = 0
        self.counter = 0
        self.lenOfTravel = random.randint(10,20)
        
        
    def randomizeDirection(self):
        
        # The weighted list of fish. Each item is a tuple: (VALUE, WEIGHT)
        dirList = (
            ('Right', 85),
            ('Left', 85),
            ('Up', 15),
            ('Down', 15),
        )
        
        weightedChoice = WeightedChoice(dirList);
        
        # do this each time you want to grab a random fish:
        self.direction = weightedChoice.next()

Finally – I can call the move (bottom of the Fish class.

    def move(self):
        self.counter += 1
        #print(self.counter)
        
        #Switch up images
        if self.counter >= Fish.switchImg and self.direction == 'Right':
            if self.curRightPos >= Fish.rightPos + 2:
                self.curRightPos = Fish.rightPos
            else:
                self.curRightPos += 1
                self.counter = 0
        if self.counter >= Fish.switchImg and self.direction == 'Left':
            if self.curLeftPos >= Fish.leftPos + 2:
                self.curLeftPos = Fish.leftPos
            else:
                self.curLeftPos += 1
                self.counter = 0
        if self.counter >= Fish.switchImg and self.direction == 'Up':
            if self.curUpPos >= Fish.upPos + 2:
                self.curUpPos = Fish.upPos
            else:
                self.curUpPos += 1
                self.counter = 0
        if self.counter >= Fish.switchImg and self.direction == 'Down':
            if self.curDownPos >= Fish.downPos + 2:
                self.curDownPos = Fish.downPos
            else:
                self.curDownPos += 1
                self.counter = 0
                
        
        #Move the fish
        #self.draw(self.curRightPos, self.rect.x + self.speed, self.rect.y)
        #self.traveled += self.speed
        
        if self.direction == 'Right':
            self.draw(self.curRightPos, self.rect.x + self.speed, self.rect.y)
            #self.rect.x += self.speed
            self.traveled += self.speed
        elif self.direction == 'Left':
            self.draw(self.curLeftPos, self.rect.x - self.speed, self.rect.y)
            #self.rect.x -= self.speed
            self.traveled -= self.speed
        elif self.direction == 'Up':
            self.draw(self.curUpPos, self.rect.x, self.rect.y - self.speed)
            #self.rect.y -= self.speed
            self.traveled -= self.speed
        else:
            self.draw(self.curDownPos, self.rect.x, self.rect.y + self.speed)
            #self.rect.y += self.speed
            self.traveled += self.speed
            
        #Check if the fish is off the screen
        if self.rect.top > screen.get_height() - self.h:
            self.direction = 'Up'
        elif self.rect.right > screen.get_width():
            self.direction = 'Left'
        elif self.rect.left < 0:
            self.direction = 'Right'
        elif self.rect.top < 0:
            self.direction = 'Down'
            
        #Check if we're done traveling
        if self.traveled > self.lenOfTravel:
            self.randomizeTravel()
            self.randomizeDirection()

VOILA! – Here be fish.

I know what you’re thinking, what happens if I change my list to randomize 1000 fish. It looks amazing is what – why stop at 1000. The “final” code is posted here at GitHub in case your interested. I’ll probably keep tinkering and make it a fully fledged project sometime down the road or sooner.