Building A City - Part III

image

Now you've got a runtime distribution running of the Micropolis demo. Doesn't do much does it? Sure you can spin around and navigate throughout the city. Perhaps you've changed the name of the city to load and checked out the other layouts. Otherwise, it's pretty bland and boring. Let's spend a little time building a new Python script to exercise two parts of the engine, terraforming and file access.

Catching Up

First let's catch up on what we need to get going here. To compile, please read Part I to get the required software installed and the C++ code compiled on Windows. Once you've done that, check out Part II where we get the additional files needed for runtime and launching the Python code. You can grab a binary release of the files if you don't want to build your own from here (note: You'll still need to install Python and the extensions listed in Part II to run the simulator).

Codebase Reflections

Just a side note about the codebase that I wanted to point out in order to try to clear up some confusion. The micropolis-activity-source package is the TCL/Tk version for x86 Linux (there's also a compiled version available here). This is the updated X11 version Don wrote and ported to use TCL/Tk for it's windowing system. This comes in C source form and can be compiled (with TCL/Tk) on a Linux system.

The C source was then cleaned up and "ported" to non-managed C++ and reborn into the MicropolisCore package, the focus of this series of blog entries. The code can be compiled on Linux but there's also Visual Studio 2005 project files for building on Windows. This version is ported from the original C code into a C++ class called Micropolis. That class is then processed by SWIG (as part of the Visual Studio compile) to generate export libraries and wrappers so the routines can be called by Python.

The "new" C++ code isn't complete but it is arguably better than the C code. The Micropolis project is fully devoid of any UI or Windowing libraries of any kind. At some point that means we can make the Micropolis project testable with unit tests (yeah, that'll come later). However it also means the new project is not just something you can plug in and run Micropolis as a full game on Windows. The TCL/Tk version is fully functional, this project isn't (yet). There are stubs for the calls and when you dig into the code you'll find that none of the routines that display screens or allow you to place tiles works yet. This is all coming so we'll grow into it.

Why Python? In lieu of TCL/Tk it's a godsend. At least it's a real object oriented language (even if it is a scripting language) and using SWIG allows you to expose C or C++ methods and classes to Python, which is what we're working with here. So why not take the original C code and run SWIG against it? Because the code is tied to the GUI toolkit and trying to get all that running on Windows is an exercise left for the bored. Sure, the Micropolis-mega class isn't the way I would have done it but the code is out there and able for blokes like me to break apart and make it, let's say, more testable and extensible. Stay tuned on that front.

More Tools

Yes, even more tools and downloads are needed. As we're going to be working (mostly) in Python we need a half decent editor. Notepad++ is fine for editing files, but you want some kind of debugging capabilities and perhaps some syntax checking or intellisense. I have a low tolerance for patience when it comes to tools. If I can't get something to work or figure out a tool within a few minutes I move on. It might be cruel but software has to be intuitive and make sense, just like a codebase. You should be able to work your way around a good codebase or a new tool without scratching your head saying "Huh?".

After combing the net I found a few reviews of Python IDEs. The pickings are not great but I settled on taking a look at Komodo, Wing IDE, and PyScripter.

Komodo was a bit of a mess. There was a free editor-only version which I tried, but gave up instantly with it. I switched gears to the professional version which I thought would help but it's like a bad episode of Hell's Kitchen. It bears no resemblance to any IDE I've worked with. Creating a "project" led to some folder that linked into the file system and there was no way to organize files (that I could see anyways). In the end, it seemed like nothing but bloatware so I gave up on that tool quick.

Wing IDE from Wingware is a pretty sophisticated set of tools and generally looks nice and performs well. If you're looking to do some serious Python work I highly recommend it. It has all the features you would want in an IDE and doesn't suffer from a confusing UI or bloated load times like Komodo was.

PyScripter was small but powerful and overall very nice. Both a project (file) explorer and a class explorer which was handy. Uncluttered interface and even detected when a new version was available (complete with a quick download, shutdown and restart) however it lacked any intellisense and would only launch the app the first time. First time it worked like a charm, any subsequent launch would produce an error that it couldn't find the gdk library (even though it launched fine the first time). Restart the app and it can launch it without error.

In the end I settled on the free version of Wing IDE (Wing IDE 101). First, it was free and that's a good thing. I don't do enough Python development to warrant the $129 price tag. There's a personal version for $30 but it doesn't give you much over the free editor version, so that's what I'm using. It has syntax highlighting and formatting (a must), can launch and debug a Python script, and even has intellisense. Unfortunately the class browser doesn't come with the free or even personal versions and I'm not that dedicated to the language to shell out for the professional version (not to mention the fact that I'm a cheap bastard) so it's the free version for us.

Whatever tool you use, launch it and let's go about cleaning up the initial script.

Starting Clean

We want to clean up the Python code a little and focus on the changes we'll make for this exercise. We'll build a new Python file (exercise-1.py) for this. Mainly we're just making things a little easier to maintain and read. In the code we mimic what the micropoliswindow.py file does (just with a few less lines). Now we're ready for our modifications.

Terraforming

Terraforming is the process where the game engine creates a new blank landscape. It's called when you start a new blank city. We'll do this in the startup of our engine by calling the GenerateNewCity function. Here's the original Python code called to start the Micropolis engine:

   1: def createEngine(self):
   2:  
   3:     engine = micropolis.Micropolis()
   4:     self.engine = engine
   5:     print "Created Micropolis simulator engine:", engine
   6:  
   7:     engine.ResourceDir = 'res'
   8:     engine.InitGame()
   9:  
  10:     # Load a city file.
  11:     cityFileName = 'cities/deadwood.cty'
  12:     print "Loading city file:", cityFileName
  13:     engine.loadFile(cityFileName)
  14:  
  15:     # Initialize the simulator engine.
  16:  
  17:     engine.Resume()
  18:     engine.setSpeed(2)
  19:     engine.setSkips(100)
  20:     engine.SetFunds(1000000000)
  21:     engine.autoGo = 0
  22:     engine.CityTax = 9
  23:  
  24:     tilewindow.TileDrawingArea.createEngine(self)

It loads the deadwood.cty file from the cities folder using the loadFile method of the engin. Here's the changes we'll make to generate the blank landscape:

   1: def createEngine(self):
   2:  
   3:     engine = micropolis.Micropolis()
   4:     self.engine = engine
   5:  
   6:     engine.ResourceDir = 'res'
   7:     engine.InitGame()
   8:     engine.GenerateNewCity()
   9:     
  10:     engine.Resume()
  11:     engine.setSpeed(1) 
  12:     engine.SetFunds(1000000000)
  13:     engine.autoGo = 0
  14:     engine.CityTax = 9
  15:     
  16:     tilewindow.TileDrawingArea.createEngine(self)

Rather than calling engine.loadFile, we'll call engine.GenerateNewCity. This is found in generate.cpp in the Micropolis project and exposed to us via SWIG (from the _micropolis.pyd file generated by the Visual Studio project). Launch the app ("Python.exe -i exercise-1.py" or from your IDE) and you'll get something like this:

image

Here's the source file exercise-1.py so far. Now that we have a new, blank city to work with we can make it more interactive. First, let's create a popup menu and add it it our window. Start by adding a call to a method we'll create called createPopupMenu by modifying the constructor of the MicropolisDrawingArea class:

   1: self.engine = engine
   2: self.createPopupMenu()
   3: tilewindow.TileDrawingArea.__init__(self, **args)
Now we'll need to create the method. This is going to create a gtkMenu object, add the menu option to generate a new city, and setup our bindings. Add this method to the MicropolisDrawingArea class:
   1: def createPopupMenu(self):
   2:     
   3:         # main popup menu
   4:         self.popup = gtk.Menu()
   5:  
   6:         # file/system menu
   7:         menu = gtk.MenuItem("File")
   8:         childMenu = gtk.Menu()
   9:  
  10:         menuItem = gtk.MenuItem("Generate City")
  11:         menuItem.connect("activate", self.GenerateNewCity)
  12:         childMenu.append(menuItem)
  13:  
  14:         menu.set_submenu(childMenu)
  15:         self.popup.append(menu)
The connect call will bind the activation of this menu item to a method called GenerateNewCity. Here's the method which calls the engines method of the same name:
   1: def GenerateNewCity(self, widget):
   2:     self.engine.GenerateNewCity()

Finally we need to invoke the popup menu. We'll do this from the right-click menu. The mouse handling is already dealt with in the tilewindow class (you can see this in tilewindow.py in the handleMousePress method) but we're going to intercept the call in our own class and pass it on if we don't handle it. Add this method to the MicropolisDrawingArea class:

   1: def handleButtonPress(
   2:     self,
   3:     widget,
   4:     event):
   5:  
   6:     if event.button == 3: # right-click
   7:         self.popup.show_all()
   8:         self.popup.popup(None, None, None, event.button, event.time)
   9:     else:
  10:         tilewindow.TileDrawingArea.handleButtonPress(self, widget, event)

Now when we run the app and right-click on the drawing surface, we can use our new popup menu:

image

And when clicked it runs through the various routines creating a new landscape each time. You can check out the C++ code in the generate.cpp file for details on how it works. You can grab the Python script exercise-2.py with this new functionality. Here are some variations the engine produces:

image image image

Now that we have an interface to let the user interact with the system, we can extend this. Two features of the system are loading existing cities (from the cities folder) and loading scenarios. Let's wire these up to the new interface.

Loading Cities

Cities are kept as binary files in the cities folder. The engine loads them up via the LoadCity method that takes in a filename for a parameter. So first we'll add some code to our menu to allow the user to select a Load City option:

   1: menuItem = gtk.MenuItem("Generate City")
   2: menuItem.connect("activate", self.GenerateNewCity)
   3: childMenu.append(menuItem)
   4:  
   5: # Start New Load City menu option
   6: menuItem = gtk.MenuItem("Load City...")
   7: menuItem.connect("activate", self.LoadCity)
   8: childMenu.append(menuItem)
   9: # End New Load City menu option
  10:  
  11: menu.set_submenu(childMenu)
  12: self.popup.append(menu)

Now let's write the LoadCity method. This is going to use the gtk.FileChooserDialog which let's us pick a file from a directory. The Python version doesn't use the standard Windows File Open look and feel so it might look weird when you run it, but it does the job.

In the new LoadCity method we'll make a few modifications like only allowing to load local files; we'll set the working folder to the cities folder; and we'll add a filter to show *.cty files. Here's the new code snippet to add:

   1: def LoadCity(self, widget):
   2:     dialog = gtk.FileChooserDialog("Open City..",
   3:                                    None,
   4:                                    gtk.FILE_CHOOSER_ACTION_OPEN,
   5:                                    (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK))
   6:     dialog.set_local_only(True)
   7:     dialog.set_select_multiple(False)
   8:     cityFolder = os.getcwd() + "\\cities"
   9:     dialog.set_current_folder(cityFolder)
  10:     
  11:     filter = gtk.FileFilter()
  12:     filter.set_name("All files")
  13:     filter.add_pattern("*")
  14:     dialog.add_filter(filter)
  15:     
  16:     filter = gtk.FileFilter()
  17:     filter.set_name("Micropolis City Files")
  18:     filter.add_pattern("*.cty")
  19:     dialog.add_filter(filter)
  20:     
  21:     response = dialog.run()
  22:     if response == gtk.RESPONSE_OK:
  23:         filename = dialog.get_filename()
  24:         self.engine.LoadCity(filename)
  25:     dialog.destroy()

Now run the app and select Open City from the popup menu and you'll see something like this:

image

Click on a double-click on a city file or select one and click "Open". The new city will load in your window and you can move around the new landscape.

You can find the code up to this point in the exercise-3.py file.

Loading Scenarios

There are 8 "custom" scenarios pre-built for Micropolis. Think of a scenario as a situation composed of a map file, location, funds, and a timeline. While cities have their own timeline stored with them, scenarios are loaded and set to a certain point in time along with a set of fixed funds. Scenarios include the 1906 earthquake of San Francisco; Hamburg, Germany during the height of World War II in 1944; and and futuristic Boston in the year 2010.

The scenarios are hard coded inside fileio.cpp and can't be changed without modifications to the C++ source. There is an engine method called LoadScenario that takes in a number (1-8) for the scenario number to load. Again, like the Load City option with scenarios we'll build 8 menu items and hook them up to a callback. In this case, we can use a single call back and pass in (from the creation of the menu item) the number of the scenario to load.

Here's the new menu code for the scenarios:

   1: playMenu = gtk.MenuItem("Play Scenario")
   2: subMenu = gtk.Menu()
   3:  
   4: menuItem = gtk.MenuItem("Dullsville")
   5: menuItem.connect("activate", self.PlayScenario, 1)
   6: subMenu.append(menuItem)
   7:  
   8: menuItem = gtk.MenuItem("San Francisco")
   9: menuItem.connect("activate", self.PlayScenario, 2)
  10: subMenu.append(menuItem)
  11:  
  12: menuItem = gtk.MenuItem("Hamburg")
  13: menuItem.connect("activate", self.PlayScenario, 3)
  14: subMenu.append(menuItem)
  15:  
  16: menuItem = gtk.MenuItem("Bern")
  17: menuItem.connect("activate", self.PlayScenario, 4)
  18: subMenu.append(menuItem)
  19:  
  20: menuItem = gtk.MenuItem("Tokyo")
  21: menuItem.connect("activate", self.PlayScenario, 5)
  22: subMenu.append(menuItem)
  23:  
  24: menuItem = gtk.MenuItem("Detroit")
  25: menuItem.connect("activate", self.PlayScenario, 6)
  26: subMenu.append(menuItem)
  27:  
  28: menuItem = gtk.MenuItem("Boston")
  29: menuItem.connect("activate", self.PlayScenario, 7)
  30: subMenu.append(menuItem)
  31:  
  32: menuItem = gtk.MenuItem("Rio de Janeiro")
  33: menuItem.connect("activate", self.PlayScenario, 8)
  34: subMenu.append(menuItem)
  35:  
  36: playMenu.set_submenu(subMenu)
  37: childMenu.append(playMenu)

And here's the PlayScenario method. Note we'll grab the scenario number as a parameter passed in and call the engine.LoadScenario method using it:

   1: def PlayScenario(self, widget, scenario):
   2:     self.engine.LoadScenario(scenario)

Now with the modifications we've done we can load up scenario files, load up cities, and generate a new blank terrain all driven from a popup menu.

image

Finally we'll just add the remaining menu items that represent the ones in the game. We'll stub these out for now to call a placeholder method. You can grab the final exercise-final.py file from here. Just drop it in your distribution folder and you're ready to go.

Things you can do to extend these ideas:

  • Modify the fileio.cpp code to read in an XML file or something and retrieve the scenario information from there rather than having it hard coded in the source. This will allow you to add new scenarios without rebuilding the system.
  • Alternately build the loading of the scenarios in Python instead so it can be modified.

That's it for file access and getting started in the guts of the engine. If you have any questions so far, feel free to email me with them. Next up we'll continue to extend the UI and talk to more parts of the Metropolis engine.

This is a series of posts exploring and extending the Micropolis code. You can view the full list of posts here.

Published Monday, January 14, 2008 9:44 AM by Bil Simser

Comments

# re: Building A City - Part III

Sunday, February 17, 2008 2:15 PM by P-Dawg

Is there more coming? C'mon! I'm dying over here!!! = )

p