If you’re not familiar with the app Disco Zoo, you’re probably in the majority. From the wikipedia page, Disco Zoo is a zoo simulation video game developed by Milkbag Games and published by NimbleBit for iOS and Android. It was published into the App Store on 21 February 2014 for iOS.

In the game, you go on rescue missions to save animals and expand your zoo. On each rescue mission, you are given a 5x5 grid, and between 1 and 3 animals to find. Each animal has an associated pattern, and you have 10 tries to find all the animals. Here is a screenshot from the game:

The grid for a rescue mission at the farm

The grid for a rescue mission at the farm

I want to build a tool that can guide move making based on all possible sets of arrangements. There’s a lot of other functionality that I could build into this as well, but for now I’m going to stay simple .

import numpy as np
import random
#Dim is the dimension of the grid. Later on it could be fun to change it.
#But for now, the standard grid is 5x5.
dim=5

Next I’m going to set a class for the animals including a pattern dictionary containing the value, pattern, and rescue location of the animals. I’m also defining a function called findall which finds all of the possible positions the animal’s pattern can take in a grid of the specified dimension.

For now I am sticking with values of 1 for common, 2 for uncommon, and 3 for rare. Somewhere down the line I will make a point to test these values.

class Animals:
    def __init__(self, name):
        Patterns = {'sheep' : [1, np.array([['sh', 'sh', 'sh', 'sh']]), 'sh', 'farm'],
            'pig' : [1, np.array([['pi', 'pi'],[ 'pi', 'pi']]),  'pi',  'farm'],
            'rabbit' : [1, np.array([['ra'],[ 'ra'],[ 'ra'],[ 'ra']]), 'ra', 'farm'],
            'horse' : [2, np.array([['ho'],[ 'ho'],[ 'ho']]), 'ho', 'farm'],
            'cow' : [2, np.array([['co', 'co', 'co']]), 'co', 'farm'],
            'unicorn' : [3, np.array([['un','',''],['','un', 'un']]), 'un', 'farm'],
            'kangaroo' : [1, np.array([['ka','','',''],['','ka','',''],['','','ka',''],['','','','ka']]), 'ka', 'outback'],
            'platypus' : [1, np.array([['pl', 'pl',''],['','pl', 'pl']]), 'pl', 'outback'],
            'crocodile' : [1, np.array([['cr', 'cr', 'cr', 'cr']]), 'cr', 'outback'],
            'koala' : [2, np.array([['ko', 'ko'],['', 'ko']]), 'ko', 'outback'],
            'cockatoo' : [2, np.array([['ct',''],['','ct'],['', 'ct']]), 'ct', 'outback'],
            'tiddalik' : [3, np.array([['','ti',''],['ti','','ti']]), 'ti', 'outback'],
            'zebra' : [1, np.array([['','ze',''],['ze','','ze'],['', 'ze','']]),'ze', 'savanna'],
            'hippo' : [1, np.array([['hi','','hi'],['','',''],['hi','','hi']]), 'hi', 'savanna'],
            'giraffe' : [1, np.array([['gi'],[ 'gi'],[ 'gi'],[ 'gi']]), 'gi', 'savanna'],
            'lion' : [2, np.array([['li', 'li', 'li']]), 'li', 'savanna'],
            'elephant' : [2, np.array([['el', 'el'],[ 'el','']]),'el', 'savanna'],
            'gryphon' : [3, np.array([['gr','','gr'],['', 'gr','']]),'gr', 'savanna'],
            'bear' : [1, np.array([['be', 'be'],['', 'be'],['', 'be'],['', 'be']]), 'be', 'northern'],
            'skunk' : [1, np.array([['','sk', 'sk'],[ 'sk', 'sk','']]),'sk' 'northern'],
            'beaver' : [1, np.array([['','','ba'],[ 'ba', 'ba',''],['','','ba']]), 'ba', 'northern'],
            'moose' : [2, np.array([['mo','','mo'],['', 'mo','']]),'mo', 'northern'],
            'fox' : [2, np.array([['fo', 'fo',''],['','','fo']]), 'fo', 'northern'],
            'sasquatch' : [3, np.array([['sa'],[ 'sa']]), 'sa', 'northern'],
            'penguin' : [1, np.array([['','pe',''],['','pe',''],['pe','','pe']]), 'pe', 'polar'],
            'seal' : [1, np.array([['se','','',''],['','se','','se'],['','','se','']]),'se', 'polar'],
            'muskox' : [1, np.array([['mu', 'mu',''],['mu','','mu']]), 'mu', 'polar'],
            'polarbear' : [2, np.array([['pb','','pb'],['','','pb']]), 'pb', 'polar'],
            'walrus' : [2, np.array([['wa','',''],['','wa', 'wa']]), 'wa', 'polar'],
            'yeti' : [3, np.array([['ye'],[''],[ 'ye']]), 'ye', 'polar'],
            'monkey' : [1, np.array([['mk','','mk',''],['','mk','','mk']]), 'mk', 'jungle'],
            'toucan' : [1, np.array([['','to'],[ 'to',''],['','to'],['', 'to']]), 'to', 'jungle'],
            'gorilla' : [1, np.array([['go','','go'],[ 'go','','go']]), 'go', 'jungle'],
            'panda' : [2, np.array([['','','pa'],[ 'pa','',''],['','','pa']]), 'pa', 'jungle'],
            'tiger' : [2, np.array([['ti','','ti', 'ti']]), 'ti', 'jungle'],
            'pheonix' : [3, np.array([['ph','',''],['','',''],['','','ph']]), 'ph', 'jungle'],
            'diplodocus' : [1, np.array([['di','',''],['','di', 'di'],['', 'di','']]),'di', 'jurassic'],
            'stegosaurus' : [1, np.array([['','st', 'st',''],['st','','','st']]), 'st', 'jurassic'],
            'raptor' : [1, np.array([['ra', 'ra',''],['','ra',''],['','','ra']]), 'ra', 'jurassic'],
            'trex' : [2, np.array([['tr',''],['',''],['tr', 'tr']]), 'tr', 'jurassic'],
            'triceratops' : [2, np.array([['tc','',''],['','','tc'],[ 'tc','','']]),'tc', 'jurassic'],
            'dragon' : [3, np.array([['dr','',''],['','','dr']]), 'dr', 'jurassic'],
            'woolyrhino' : [1, np.array([['','','wo',''],['wo','','','wo'],['', 'wo','','']]),'wo', 'iceage'],
            'giantsloth' : [1, np.array([['gs','',''],['','','gs'],[ 'gs','','gs']]), 'gs', 'iceage'],
            'direwolf' : [1, np.array([['','dw','',''],['dw','','','dw'],['', 'dw','','']]),'dw', 'iceage'],
            'sabertooth' : [2, np.array([['st','',''],['','','st'],['', 'st','']]),'st', 'iceage'],
            'mammoth' : [2, np.array([['','ma',''],['ma','',''],['','','ma']]), 'ma', 'iceage'],
            'akhult' : [3, np.array([['','','ak'],[ 'ak','',''],['','','ak']]), 'ak', 'iceage'],
            'raccoon' : [1, np.array([['ra','','ra',''],['ra','','','ra']]), 'ra', 'city'],
            'pigeon' : [1, np.array([['pg','',''],['','pg',''],['','pg', 'pg']]), 'pg', 'city'],
            'rat' : [1, np.array([['rt', 'rt','',''],['','rt','','rt']]), 'rt', 'city'],
            'squirrel' : [2, np.array([['','','sq'],[ 'sq','',''],['','sq','']]),'sq', 'city'],
            'opossum' : [2, np.array([['op','',''],['op','','op']]), 'op', 'city'],
            'sewerturtle' : [3, np.array([['se', 'se']]), 'se', 'city'],
            'goat' : [1, np.array([['go','',''],['go', 'go', 'go']]), 'go', 'mountain'],
            'cougar' : [1, np.array([['co','',''],['','co',''],['co','','co']]), 'co', 'mountain'],
            'elk' : [1, np.array([['ek','','ek'],['', 'ek', 'ek']]), 'ek', 'mountain'],
            'eagle' : [2, np.array([['ea',''],['ea',''],['','ea']]), 'ea' 'mountain'],
            'coyote' : [2, np.array([['cy', 'cy',''],['','','cy']]), 'cy', 'mountain'],
            'aatxe' : [3, np.array([['','','aa'],[ 'aa','','']]),'aa', 'mountain'],
            'moonkey' : [1, np.array([['mk','',''],['mk','','mk'],['','','mk']]), 'mk', 'moon'],
            'lunartick' : [1, np.array([['','lu',''],['','',''],['','lu',''],['lu','','lu']]), 'lu', 'moon'],
            'tribble' : [1, np.array([['','tb',''],['tb', 'tb', 'tb']]), 'tb', 'moon'],
            'moonicorn' : [2, np.array([['mn',''],['mn', 'mn']]), 'mn', 'moon'],
            'lunamoth' : [2, np.array([['lm','','lm'],['','',''],['','lm','']]),'lm', 'moon'],
            'jaderabbit' : [3, np.array([['jr',''],['',''],['','jr']]), 'jr', 'moon'],
            'rock' : [1, np.array([['rk', 'rk'],[ 'rk', 'rk']]), 'rk', 'mars'],
            'marsmot' : [1, np.array([['','ms'],['', 'ms'],[ 'ms', 'ms']]), 'ms', 'mars'],
            'marsmoset' : [1, np.array([['mt','','mt'],['','','mt'],['', 'mt','']]),'mt', 'mars'],
            'rover' : [2, np.array([['','rv',''],['rv','','rv']]), 'rv', 'mars'],
            'martian' : [2, np.array([['mr','','mr'],['', 'mr','']]),'mr', 'mars'],
            'marsmallow' : [3, np.array([['mw'],[''],[ 'mw']]), 'mw', 'mars'],
            'discobucks' : [3, np.array([['db']]), 'db', 'NA']}

        self.name=name
        self.value=Patterns[name][0]
        self.pattern=Patterns[name][1]
        self.abbr=Patterns[name][2]
        self.location=Patterns[name][3]

        
    def findall(self):
        a = self.pattern
        abbr = self.abbr
        value = self.value
        rows = a.shape[0]
        cols = a.shape[1]
        
        rd = dim-rows+1
        cd = dim-cols+1
        ls = []
        ls2 = []
        
        for i in range(cd):
            if i == 0 :
                precol = np.full((dim, cd-1),'')
            elif i == cd:
                postcol = np.full((dim, cd-1),'')
            else:
                precol = np.full((dim, cd-1-i),'')
                postcol = np.full((dim, i),'')
        
            for j in range(rd) :
                if j == 0:
                    prerow = np.full((rd-1, cols),'')
                    col = np.append(prerow, a, axis = 0)
                elif j == rd:
                    postrow = np.full((rd-1, cols),'')
                    col = np.append(a, postrow, axis=0)
                else:
                    prerow = np.full((rd-1-j, cols),'')
                    postrow = np.full((j, cols),'')
                    col = np.concatenate((prerow, a, postrow), axis=0)
            
                if i == 0:
                    grid = np.append(precol, col, axis=1)
                elif i == cd:
                    grid = np.append(col, postcol, axis=1)
                else:
                    grid = np.concatenate((precol, col, postcol), axis=1)
                ls.append(grid) 
                grid2 = np.where(grid == '', 0, value)
                ls2.append(grid2)
        return(ls, ls2)         

For a sanity check:

animal1 = Animals('unicorn')
print(animal1.name, animal1.value, animal1.pattern, animal1.location)
## unicorn 3 [['un' '' '']
##  ['' 'un' 'un']] farm
test=animal1.findall()
print("there are", len(test[1]), "possible locations for a", animal1.name)
## there are 12 possible locations for a unicorn
print(test[0][0])
## [['' '' '' '' '']
##  ['' '' '' '' '']
##  ['' '' '' '' '']
##  ['' '' 'un' '' '']
##  ['' '' '' 'un' 'un']]

Everything is looking good. Here’s the sample I’m going to run with for this example:

AnimalList = ['elk', 'goat']

First I’m going to make a list that takes in the animals given and determines all possible locations for each animal independently.

allpat = []
for k in AnimalList:
    allpat.append((k, Animals(k).findall()[0], Animals(k).findall()[1]))

In this step, I combine the lists of independent possible animal locations to find all combinations of the animals provided. In the second part of this chunk, I filter out options that are not feasible (ie two animals cannot occupy the same square of the grid)

combined = []
combined_values = []

for l in range(len(AnimalList)):
    if len(AnimalList)<= 1:
        print("Too Few Animals")
    elif l == 0:
        for m in range(len(allpat[l][1])):
            for n in range(len(allpat[l+1][1])):
                combined.append(np.core.defchararray.add(allpat[l][1][m],allpat[l+1][1][n]))
                combined_values.append(np.core.add(allpat[l][2][m],allpat[l+1][2][n]))


ogpossible = []
ogpossible_values = []
for p in range(len(combined)):
    maxls = []
    for q in range(dim):
        maxls.append(len(max(combined[p][q], key=len)))
    if max(maxls)<=2:
        ogpossible.append(combined[p])
        ogpossible_values.append(combined_values[p])

In the next step I calculate the expected value of each of the 25 grid squares in order to determine the optimal first move.

def expected_value(possible):
    expected_value = np.sum([possible], axis = (0,1))/len(possible)
    expected_value = np.where(expected_value == 1, 0, expected_value)
    argmax = np.unravel_index(np.argmax(expected_value), (dim,dim))
    return(expected_value, argmax)

original_expected, location = expected_value(ogpossible_values)

possible = ogpossible
possible_values= ogpossible_values

This part is built for actually playing the game. It takes in a value observed and filters the list of possible options to new possible options given the observed value. It will also recalculate the expected values for the new set of possible locations and provide a next move.

def remove(pattern, location):
    new_list1 = []
    new_list2 = []
    
    
    for option in range(len(possible)):
        if possible[option][location[0]][location[1]] == pattern:
            new_list1.append(possible[option])
            new_list2.append(possible_values[option])
    return(new_list1, new_list2)

def play_that_game(true_value, location):
    if true_value in AnimalList:
        abbr = Animals(true_value).abbr
        possible, possible_values = remove(abbr, location)
        new_expected, new_location = expected_value(possible_values)
        return(possible, possible_values, new_expected, new_location)
    elif true_value == 'NONE':
        abbr = ''
        possible, possible_values = remove(abbr, location)
        new_expected, new_location = expected_value(possible_values)
        return(possible, possible_values, new_expected, new_location)
    else:
        print('ERROR - PICK AN ANIMAL FROM THE LIST OR NONE')
        
        
        
possible, possible_values, new_expected, new_location = play_that_game('elk', location)   

Having fed in the optimal choice provided to me above, below is the new suggested move:

new_location
## (3, 2)

And there you have it. In order to make this functional, I will turn it into an R Shiny app that users can interact with.