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 Image
import numpy
import os
# definitions
def createTiles(sTownName):
oImage = Image.open(sTownName + "/" + sTownName + ".png") # big town png
oRGBAImage = oImage.convert('RGBA') # covert to RGBA values
iWidth = oImage.size[0]
iHeight = oImage.size[1]
data = numpy.asarray(oRGBAImage)
count = 0
for j in range(16,iHeight,16): # y down
for k in range(16,iWidth,16): # x across
row = 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 at
if 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 math
from duplicates import buildDuplicatesList # refactored duplicate function
from PIL import Image
iSquareRadius = 8 # square radius of 16x16 tiles is 8
# given int, returns array representing x, y coordinates
def getStartingCoordinatesOfRing(iRingNumber, iSquareRadius):
return [2*iRingNumber*iSquareRadius, iRingNumber*iSquareRadius]
def printHex(sTownName):
# initialize vars
lDirectionVectors = [(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 = 23
lRepeatImageNames = buildDuplicatesList(sTownName)
iImageWidth = 2*iNumRings*2*iSquareRadius
iImageHeight = 2*iNumRings*2*iSquareRadius
oImage = 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 = 0
iDirectionVectorsIndex = 0
sFileName = lRepeatImageNames[iImageIndex] # first tile image will be the tile that is repeated most - and will continue for the number of times
oTileImage = 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 image
iImageIndex = iImageIndex + 1 # increment image index
for i in range(1,iNumRings): # loop through desired ring numbers
iNumTiles = i*6 # ring 1 has
iDirectionRepeatAmount = i # the number of times to repeat a direction before changing it also happens to be the ring number
lCurrentTileCenterCoordinates = getStartingCoordinatesOfRing(i, iSquareRadius) # starting coordinate of this ring
iSameDirectionTimes = 0 # initialize times
iDirectionVectorsIndex = 0 # initialize direction index
lCurrentDirectionVector = lDirectionVectors[ iDirectionVectorsIndex % len(lDirectionVectors) ]
print "RING " + str(i) + "------------------------------"
for j in range(0,iNumTiles): # print tiles for this ring
if iSameDirectionTimes == iDirectionRepeatAmount: # first determine direction for this
iSameDirectionTimes = 0 # reset direction count
iDirectionVectorsIndex = 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 list
if iImageIndex == len(lRepeatImageNames): # we've exhausted all our tiles
print "No more tiles :( headin on out..."
oImage.save("results/" + sTownName + '.png') # save the finished image in the folder with its tiles
return # done processing
sFileName = lRepeatImageNames[iImageIndex] # first tile image will be the tile that is repeated most - and will continue for the number of times
oTileImage = Image.open(sFileName) # open the image that corresponds to the duplicate data
print "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 vector
lCurrentTileCenterCoordinates[1] = lCurrentTileCenterCoordinates[1] + lCurrentDirectionVector[1] # y direction to go from vector
iImageIndex = iImageIndex + 1 # increment image index
iSameDirectionTimes = 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):