Zovirl Industries

Mark Ivey’s weblog

A Tiled Island Map

Smooth coastline!

For the island game I’m creating using App Engine, I need a tiled map. I wasn’t expecting this to be difficult, but the sheer number of different tiles required is daunting. If I limit roads to only entering a tile from the north, south, east, or west, there are still 15 ways for roads to cross a tile. Since I don’t want blocky, square islands with abrupt land/ocean transitions, I need a set of 46 coastline tiles. Start multiplying by different types of base tile (forests, fields, mountains, tiny villages, big cities, etc.) and the situation quickly gets out of hand.

Blocky coastline

The only sane solution appears to be transparent overlays which are composited on demand. I don’t see a way to do server-side compositing with App Engine (plus I don’t think it would scale well), so that leaves client-side compositing. I did a simple mock-up with transparent PNG images, using absolute positioning to stack them on top of each other. It seems workable. Traditionally, transparent PNG images weren’t supported well by IE6, but since only 10% of the visitors to my site are using IE6, I might just forget about supporting it.

So that’s the long-term plan. In the spirit of getting a working prototype as quickly as possible, though, I’m going to start with a really simple tileset where I can pre-generate all the combinations and just serve static images. Here are the 6 tiles I’m starting with:

Ocean

Land

Mountains

City

Field

Road



I’m not going to allow roads over mountains for now, so there are 50 possible tiles (16 road combinations * (city, field, land) + ocean + mountains). Pre-generating all the combinations is a snap using PIL.

#!/usr/bin/env python2.5

import os
from PIL import Image

def composite(images):
  """Composite a stack of images, [0] on top, [-1] on bottom."""
  top = images[0]
  for lower in images[1:]:
    top = Image.composite(top, lower, top)
  return top

def combinations(list):
  """Generate all combinations of items in list."""
  if list:
    for c in combinations(list[:-1]):
      yield c
      yield c + [list[-1]]
  else:
    yield []

def load(name):
  return Image.open(os.path.join('sources', '%s.png' % name))

def save(name, image):
  image.save(os.path.join('img', '%s.png' % name))

def make_roads(name, above, below):
  """Save tiles with roads.  Images in above will be composited above
  the roads.  Images in below will be composited below the roads."""
  roads = dict(w=load('road_overlay'),
               s=load('road_overlay').transpose(Image.ROTATE_90),
               e=load('road_overlay').transpose(Image.ROTATE_180),
               n=load('road_overlay').transpose(Image.ROTATE_270))
  above = [load(filename) for filename in above]
  below = [load(filename) for filename in below]
  for directions in combinations(sorted(roads.keys())):
    out = composite(above + [roads[x] for x in directions] + below)
    if directions:
      save('%s_road_%s' % (name, ''.join(directions)), out)
    else:
      save(name, out)

def main():
  save('mountains', load('mountains'))
  save('ocean', load('ocean'))
  make_roads('land', [], ['land'])
  make_roads('city', ['city_overlay'], ['land'])
  make_roads('field', [], ['field_overlay', 'land'])

if __name__ == '__main__':
  main()

PIL also comes in handy to paste together the first proof-of-concept map using the new tiles:

#!/usr/bin/env python2.5

import os
import sys
from PIL import Image

TILE_SIZE = 50  # Width/height of a tile in pixels.

in_file, out_file = sys.argv[1:3]
data = [line.split() for line in open(in_file).readlines()]

map = Image.new('RGB', (TILE_SIZE * len(data[0]), TILE_SIZE * len(data)))
for y, row in enumerate(data):
  for x, name in enumerate(row):
    image = Image.open(os.path.join('img', '%s.png' % name))
    map.paste(image, (x * TILE_SIZE, y * TILE_SIZE))
map.save(out_file)

And here it is: The first example map!

Next on the agenda: Getting Django running on App Engine and serving this example map using HTML.

Drive: a simple scrolling demo in pygame


A couple weekends ago I wanted to play around with some game ideas, to see if they were super-awesome or boring. I needed a simple framework to prototype them on, so I whipped one out using pygame. Then I sketched up some art. And made an installer.

And totally forgot to play around my original game ideas.

Damn. Maybe next time.

Anyway, here it is: a simple scrolling demo made with python and pygame. It has no real purpose (unless you want to do scrolling in pygame).

OSX
Windows
Source (Linux)

Delicious Python

Organizing bookmarks is too hard. Does Backcountry Maps go under Maps or Backpacking? Does Hot Library Smut go under Libraries or Pornography? And how am I supposed to keep my bookmarks synched across 5 computers? I’m a busy guy, I don’t have time to figure this stuff out.

That’s why I’ve wanted to use del.icio.us for a long time. I was really excited when they announced their import tool. A few minutes later I was staring at 1000 poorly tagged bookmarks. Oops, should’ve cleaned them up before importing. After failing to find any way to batch process the mess, I dejectedly went back to my hierarchy.

I checked again today and there’s still no “Delete All” (the FAQ helpfully suggests canceling & remaking my account). Time to do it myself. I looked at the API docs (oh, I see, I’m not supposed to flood the server…) and grabbed pydelicious. Fired up ipython and had my bookmarks deleted in no time. Sharing all of my bookmarks turned out to be simple too: Since pydelicious doesn’t seem to understand private bookmarks [1] it shares everything. Changing my mind and deciding that I really should have tagged all my new bookmarks “import” (oops, again) was similarly easy. Hooray!

[1] It is unfortunate that the del.icio.us team doesn’t provide up-to-date libraries in a few languages. In a lot of ways I think that officially maintained libraries are an ideal answer to the SOAP vs. REST arguments

steal the mouse back from SDL

I hate it when a poorly-behaved SDL app (usually mine) grabs the mouse, crashes, and doesn’t give the mouse back. Great, now I’m stuck in X with no mouse. Here’s a handy python script to get it back:

#!/usr/bin/env python
# get the mouse back after an SDL app crashes
import pygame
pygame.init()
pygame.mouse.set_visible(False)
pygame.event.set_grab(True)
pygame.quit()

XCode & Python Resources

Some resources for using XCode with Python. A few introductory sites plus the ones I find most useful on a day-to-day basis:

XCode & Python Hints

Here are a few hints about how I set up XCode for Python development.

Run your program from XCode

Some versions of PyObjC have a bug so when you make new projects, XCode doesn’t know how to run them. Make a new custom executable & point it build/Development/myProgram.app.

No, don’t quit!

Having the keyboard shortcut to quit XCode be the same shortcut as quitting your application (cmd-Q) is a recipe for annoyance when you accidently hit it twice. Instead, go into XCode’s preferences and change the key binding to quit XCode to something like cmd-option-Q.

Faster builds

You can shave a second or two off the build-test-fix cycle if you set up a dummy “No Build” target. The Development build target will run every time but behind the scenes it isn’t copying files, it is just making aliases (py2app’s –alias option). Set up a new shell script target which runs /usr/bin/true and you can save the effort.

You have no debugger, but at least use PDB

If you set the USE_PDB environment variable, PyObjC will dump you at a PDB prompt when there is an unhandled exception. You can set this on the “Arguments” tab of the custom executable.

XCode & Python

Introduction

XCode is the IDE that Apple ships with OS X. Although primarily targeted towards C++, Objective C, and Java it also plays well with Python. It is excellent for writing OS X applications in Python thanks to good integration with Apple’s Interface Builder and py2app (the OS X distutils packager).

If you want to write OSX applications using python, XCode is the tool you want. This is thanks to solid integration with Apple’s Cocoa application framework and Interface Builder.

Debugger

First, the bad news: no debugger. XCode has a debugger but it doesn’t work with Python. You’ll have to use another tool like PDB instead.

Environment

Except for the debugger, the rest of the environment is nice. The UI feels like a proper OSX app, with all the standard keyboard shortcuts and controls, so you’ll feel right at home.

The UI is very flexible, and can accommodate a single fullscreen window or multiple separate windows for each file.

Editor

Standard features here. Syntax highlighting, split panes, menus to jump straight to a class or function. Content assist exists, but leaves something to be desired. I have yet to see a content assist for python that is able to keep up with the dynamic nature of Python classes, however.

Source Control

XCode knows about Subversion, Perforce, and CVS. File comparisons are done using Apple’s File Merge tool so you get nice side-by-side views.

Finding Stuff

“Find in Project” lets you search your entire project. Bookmarks let you quickly jump to common sections of code

Making OSX Apps

XCode is a great environment for Cocoa development. PyObjC provides the Cocoa bindings for Python, and once it is installed XCode knows how to make a new Python project. The distutils setup.py script is handled for you and you can create your .app bundle by clicking Build

Drag-and-Drop your interface…

Interface Builder lets you set up your widgets. Helpful guides pop up to help you position your widgets according to the Aqua guidelines.

…and then Drag-and-Connect your buttons

Hooking up buttons to actions in your Python objects is just a matter of ctrl-dragging from the button to the object.

Conclusion

If you want a Python IDE for your Mac, XCode is a good choice. If you also want to develop OSX applications in Python, XCode is a very good choice.

Super Happy Dev House

Attended my first Super Happy Dev House on Saturday night and had a great time. The house was packed with energetic people devoted to getting stuff done. Even though most of the people were strangers working on their own projects, it was really motivating to be surrounded by so much activity.

My project was getting boost.python working on my iBook. I wasn’t able to stay all night and didn’t reach my goal*, but I did learn about iPython from the people sitting next to me. Someone (not sure who) had designed a great poster for the event and they had printed up stickers & pins so there was even some schwag. I’m looking forward to the next one.

* My problem was I was trying to use the boost packages from fink. Turns out they delete a bunch of the files you need to use bjam. The answer? Download the source directly from boost.org (not sure why I didn’t think of that right away, actually). Mission accomplished.

Perplex City

Last month, TR bought a Perplex City starter kit for my wife and I. What a cool game! There are so many levels you can play at. You can solve puzzle cards by yourself. You can join up with others online and solve cards together (including some really hard cards requiring group efforts). The city itself has a lot of depth, with record companies, pharmaceutical companies, schools, blogs, and newspapers. Some of these “companies” run ads in London newspapers, and puzzles from the game have shown up in newspapers and magazines. Finally, you can hunt for the stolen cube and try to get the $200K reward.

Many of the puzzles make fun programming challenges. I’ve been doing them in python, since that is such a great language for rapid programming. Sweet Dreams (#85) makes an obvious candidate:

#!/usr/bin/env python

a = 1
b = 1
while a < 400000:
    c = a + b
    a = b
    b = c
    print c

My wife solved Rickety Old Bridge (#106) much faster than I was able to write the program (although the program found that there are two similar solutions, not just one). This was a good one to learn generators with:

#!/usr/bin/env python

class bridge:
    people = { "kurt":2, "scarlett":5, "violet":1, "sente":10 }
    states = [ {"near":people.keys(), "far":[], "moves":[]} ]

    def move(self, source, dest):
        """ yield all new states formed by moving
            a person from source -> dest """
        for state in self.states:
            for person in state[source]:
                yield {source:  [s for s in state[source] if s != person],
                       dest:    state[dest] + [person],
                       "moves": state["moves"] + [person] }

    def to_far(self):
        self.states = [s for s in self.move("near", "far")]

    def to_near(self):
        self.states = [s for s in self.move("far", "near")]

    def score(self, state):
        times = [self.people[name] for name in state["moves"]]
        total_time = max(times[0], times[1]) + times[2] + \\
                     max(times[3], times[4]) + times[5] + \\
                     max(times[6], times[7])
        return total_time, state["moves"]

    def print_scores(self):
        scores = [self.score(s) for s in self.states]
        scores.sort()
        scores.reverse()
        for s in scores:
            print s[0], s[1]

    def run(self):
        self.to_far()
        self.to_far()
        self.to_near()

        self.to_far()
        self.to_far()
        self.to_near()

        self.to_far()
        self.to_far()

        self.print_scores()

if __name__ == "__main__":
    b = bridge()
    b.run()

The program I came up with for solving Magic Square (98) takes a long time to run but eventually starts finding candidate solutions about 2.2 million squares into its search:

#!/usr/bin/env python

def comb(nums, n):
    if n == 0: yield [], nums
    else:
        for i in range(len(nums)):
            for tail, remaining in comb( nums[:i] + nums[i+1:], n-1):
                yield [nums[i]] + tail, remaining

def rows(nums):
    if not nums: yield []
    else:
        for row, remaining in comb(nums, 4):
            if sum(row) == 34:
                for other_rows in rows(remaining):
                    yield [row] + other_rows

def check(grid):
    magic = True
    for i in range(4):
        if sum([row[i] for row in grid]) != 34:
            magic = False
    if sum([grid[i][i] for i in range(4)]) != 34 or \\
       sum([grid[i][3-i] for i in range(4)]) != 34:
        magic = False
    return magic

count = 0
for grid in rows(range(1, 17)):
    count += 1
    if count % 100000 == 0:
        print count / 1000, "K"
    if check(grid):
        print grid