Data Analysis and Pixel Art of Towns and Cities in Pokemon
Posted on July 15, 2018
A Real Treat!
Over the past few days, I've done some data analysis, scripting, and art with the 20 towns and cities from Pokemon Gold. The results were fairly neat. Follow along through all the technical steps I took to generate the images if you're interested. If not, just head down to the bottom to see the results!
1. Acquiring The Pixel Maps
I found map sprites of all 20 towns from the amazing Spriter's Resource: https://www.spriters-resource.com/game_boy_gbc/pokemongoldsilver/
2. Breakdown of Maps into 16x16 Pixel Tiles
After getting all 20 towns, the only requirement is that the map that can be actually be broken into 16x16 pieces - as you'll notice the sprite people often put all the insides of buildings onto the same .png file, so this just took careful cropping to get the part that interested me - the 'outside' part of the towns with the buildings, flora, and road/path tiles.
(Yes, I know - with some effort this too could have been automated, but hey, it took me a total of about 15 minutes to crop the 20 towns myself, so in this case I think good old elbow grease was the faster method)
I wrote a Python function createTiles
that moves in 16x16 blocks across the maps, saving each block as it's own separate .png file:
from PIL import Imageimport numpyimport os# definitionsdef createTiles(sTownName):oImage = Image.open(sTownName + "/" + sTownName + ".png") # big town pngoRGBAImage = oImage.convert('RGBA') # covert to RGBA valuesiWidth = oImage.size[0]iHeight = oImage.size[1]data = numpy.asarray(oRGBAImage)count = 0for j in range(16,iHeight,16): # y downfor k in range(16,iWidth,16): # x acrossrow = data[j-16:j]tile = []for i in range(0,16):tile.append(row[i][k-16:k])oTile = numpy.asarray(tile)im = Image.fromarray(oTile) # this array represents the current tile we are atif not os.path.exists(sTownName):os.makedirs(sTownName)im.save(sTownName + "/" + str(count) + ".png")count = count + 1
3. Converting PNG Tiles to SVG
But hey,.pngs that are only 16x16 pixels in size are no fun - you can't scale that up to giant resolution wihtout inevitable blurring or such - to keep them crisp, I converted each 16x16 tile into an .svg using a previous tool that I built, pixelmatic.
Great, now we just need to print the tiles in a creative fashion!
4. Printing Tiles in a Hexagonal Pattern
I had this neat idea to create a spiral and print all the tiles, including repeats, from each town into an outward spiraling fashion. As you can see from my beloved notebook notes, I spent almost too long try how the heck to print the tiles out in such a pattern:
Trying to figure it out.
Getting confused... STILL trying!
After lots of brainstorming, I realized it was easiest to implement by creating a walking vector. Each ring has a known count of tiles, and then you just walk around the hexagon:
import mathfrom duplicates import buildDuplicatesList # refactored duplicate functionfrom PIL import ImageiSquareRadius = 8 # square radius of 16x16 tiles is 8# given int, returns array representing x, y coordinatesdef getStartingCoordinatesOfRing(iRingNumber, iSquareRadius):return [2*iRingNumber*iSquareRadius, iRingNumber*iSquareRadius]def printHex(sTownName):# initialize varslDirectionVectors = [(0,-2*iSquareRadius),(-2*iSquareRadius,-1*iSquareRadius),(-2*iSquareRadius,1*iSquareRadius),(0,2*iSquareRadius),(2*iSquareRadius,1*iSquareRadius),(2*iSquareRadius,-1*iSquareRadius)] # down, SSW, NNW, up, NNE, SSE... and no, SSW is not southby -_-iNumRings = 23lRepeatImageNames = buildDuplicatesList(sTownName)iImageWidth = 2*iNumRings*2*iSquareRadiusiImageHeight = 2*iNumRings*2*iSquareRadiusoImage = Image.new('RGBA', (iImageWidth, iImageHeight)) # initialize our image we will build # TODO: add padding?# initialize vars for loop - start pattern off with tile in the center (exact centered around 0 will be minus a radius in x and plus a radius in y)iImageIndex = 0iDirectionVectorsIndex = 0sFileName = lRepeatImageNames[iImageIndex] # first tile image will be the tile that is repeated most - and will continue for the number of timesoTileImage = Image.open(sFileName)lCurrentDirectionVector = lDirectionVectors[iDirectionVectorsIndex] # initial direction for each ring (down)oImage.paste(oTileImage, (iImageWidth / 2 - iSquareRadius,iImageHeight / 2 - iSquareRadius)) # tile in center of imageiImageIndex = iImageIndex + 1 # increment image indexfor i in range(1,iNumRings): # loop through desired ring numbersiNumTiles = i*6 # ring 1 hasiDirectionRepeatAmount = i # the number of times to repeat a direction before changing it also happens to be the ring numberlCurrentTileCenterCoordinates = getStartingCoordinatesOfRing(i, iSquareRadius) # starting coordinate of this ringiSameDirectionTimes = 0 # initialize timesiDirectionVectorsIndex = 0 # initialize direction indexlCurrentDirectionVector = lDirectionVectors[ iDirectionVectorsIndex % len(lDirectionVectors) ]print "RING " + str(i) + "------------------------------"for j in range(0,iNumTiles): # print tiles for this ringif iSameDirectionTimes == iDirectionRepeatAmount: # first determine direction for thisiSameDirectionTimes = 0 # reset direction countiDirectionVectorsIndex = iDirectionVectorsIndex + 1 # increment to new direction ( we walk around the rings clockwise as we 'paint' )lCurrentDirectionVector = lDirectionVectors[ iDirectionVectorsIndex % len(lDirectionVectors) ] # modulo gives us the proper index such that lDirectionVectors acts as a cirular listif iImageIndex == len(lRepeatImageNames): # we've exhausted all our tilesprint "No more tiles :( headin on out..."oImage.save("results/" + sTownName + '.png') # save the finished image in the folder with its tilesreturn # done processingsFileName = lRepeatImageNames[iImageIndex] # first tile image will be the tile that is repeated most - and will continue for the number of timesoTileImage = Image.open(sFileName) # open the image that corresponds to the duplicate dataprint "Current tile coord: (" + str(lCurrentTileCenterCoordinates[0]) + ", "+ str(lCurrentTileCenterCoordinates[1]) + ")"oImage.paste(oTileImage, (iImageWidth / 2 - iSquareRadius + lCurrentTileCenterCoordinates[0],iImageHeight / 2 - iSquareRadius + lCurrentTileCenterCoordinates[1])) # paste this tile at these coordinates into the image!print "Now moving " + str(lCurrentDirectionVector[0]) + " in X"print "and " + str(lCurrentDirectionVector[1]) + " in Y"lCurrentTileCenterCoordinates[0] = lCurrentTileCenterCoordinates[0] + lCurrentDirectionVector[0] # x direction to go from vectorlCurrentTileCenterCoordinates[1] = lCurrentTileCenterCoordinates[1] + lCurrentDirectionVector[1] # y direction to go from vectoriImageIndex = iImageIndex + 1 # increment image indexiSameDirectionTimes = iSameDirectionTimes + 1 # also increase direction 'times'
5. Data Analysis
Ok, I'll admit it, the goal of this project was much more heavily leaning towards the art & design component: for 'data analysis' I didn't really do much of anything except find the duplicates of each tile in each town - and how many times that tile appeared, because I needed exactly that info to generate the town designs.
Building off the code that is in the repository, some other interesting info would be to find:
- total number of unique tiles across the entire game - finding most unique and most repeated tile
- total number of colors
Though perhaps hunting around on the web this info is already freely avaliable.
To get the counts of each repeats, after all 16x16 tiles were generated, I got the idea to just find a python script which counts duplicate files by content (a hash comparing algorithm). I found a nice one by Andres Torres at Python Central: https://www.pythoncentral.io/finding-duplicate-files-with-python/. I refactored it so it could just be called as a function and return an array of each repeat file name.
What I mean by this is: let's say '1.svg' was the 'ground' tile, and it was found to have the same contents as 336 other tiles in Goldenrod City. Well, this array would include the string '1.png' as the first 336 elements, then a string with the next most repeated file name, and so on. The array that this function returns is the key looping array as we print our hexagonal style pattern in printer.py
.
Here's an example of identical tiles and how often they repeat for Goldenrod:
336 like goldenrod/348.png (ground)
230 like goldenrod/374.png (pink tiles)
62 like goldenrod/1409.png (tree top)
56 like goldenrod/823.png (water)
56 like goldenrod/1226.png (right corner house)
56 like goldenrod/201.png (left corner house)
53 like goldenrod/1353.png (fence)
51 like goldenrod/943.png (roof top right corner)
51 like goldenrod/162.png (roof top left corner)
48 like goldenrod/229.png (house normal wall)
46 like goldenrod/1186.png (roof center)
31 like goldenrod/1481.png (tree bottom)
26 like goldenrod/400.png (roof top edge)
26 like goldenrod/439.png (roof bottom edge)
25 like goldenrod/539.png (top edge ground)
25 like goldenrod/77.png (rock wall middle)
20 like goldenrod/1368.png (shore)
18 like goldenrod/1030.png (gold brick wall)
17 like goldenrod/505.png (brown dirt top edge)
15 like goldenrod/1387.png (ground with gray fence)
14 like goldenrod/159.png (etc... you've gotta be kidding if you think I will)
14 like goldenrod/158.png
14 like goldenrod/980.png
14 like goldenrod/198.png
14 like goldenrod/429.png
10 like goldenrod/1421.png
10 like goldenrod/610.png
9 like goldenrod/758.png
9 like goldenrod/238.png
9 like goldenrod/200.png
9 like goldenrod/403.png
9 like goldenrod/170.png
9 like goldenrod/217.png
9 like goldenrod/377.png
8 like goldenrod/920.png
8 like goldenrod/148.png
6 like goldenrod/107.png
6 like goldenrod/149.png
6 like goldenrod/512.png
5 like goldenrod/260.png
5 like goldenrod/261.png
5 like goldenrod/831.png
5 like goldenrod/836.png
4 like goldenrod/880.png
4 like goldenrod/249.png
4 like goldenrod/1031.png
4 like goldenrod/784.png
3 like goldenrod/570.png
3 like goldenrod/573.png
3 like goldenrod/548.png
3 like goldenrod/572.png
3 like goldenrod/248.png
3 like goldenrod/571.png
2 like goldenrod/389.png
2 like goldenrod/1343.png
2 like goldenrod/658.png
2 like goldenrod/1171.png
2 like goldenrod/659.png
2 like goldenrod/997.png
2 like goldenrod/161.png
2 like goldenrod/924.png
2 like goldenrod/1173.png
Total Duplicates: 1507
So our return array would contain 1507 string elements, each with the value of that repeated file's name.
6. Final Results
I repeated this process for all the towns of Kanto and Johto (for non-Pokemon nerds: the 'original' towns and cities of Pokemon, plus the '2nd generation' towns, a total of 20):
Azalea Town Johto
Blackthorn City Johto
Celadon City Kanto
Cerulean City Kanto
Cherrygrove City Johto
Cinnabar Island Kanto
Cianwood City Johto
Ecruteak City Johto
Fuchsia City Kanto
Goldenrod City Johto
Lavender Town Kanto
Mahogany Town Johto
New Bark Town Johto
Olivine City Johto
Pallet Town Kanto
Pewter City Kanto
Saffron City Kanto
Vermilion City Kanto
Violet City Johto
Viridian City Kanto
The repository has all the code, 16x16 pngs, svgs, and the final hex images in the results
folder. Check it out on GitHub.
I may revisit this project for more designs at some point, I think there are a lot of cool things that could be done with these tiles.
I call it The Pokemon Kaleidoscope Series!!!
Enjoy!
The Pokemon Kaleidoscope Series
Azalea Town (Johto):
Blackthorn City (Johto):
Celadon City (Kanto):
Cerulean City (Kanto):
Cherrygrove City (Johto):
Cinnabar Island (Kanto):
Cianwood City (Johto):
Ecruteak City (Johto):
Fuchsia City (Kanto):
Goldenrod City (Johto):
Lavender Town (Kanto):
Mahogany Town (Johto):
New Bark Town (Johto):
Olivine City (Johto):
Pallet Town (Kanto):
Pewter City (Kanto):
Saffron City (Kanto):
Vermilion City (Kanto):
Violet City (Johto):
Viridian City (Kanto):