Wednesday, April 30, 2008

Day 90: Gemification 2, the revenge

Sinuses... stuffed! Laptop... dying! Must... complete... article... on... gems!

(Part 1)

So I've got a gem. 'gem install kuiper-0.0.1.gem' went well. I even added /var/lib/gems/1.8/bin/ to my $PATH so I could run the script. I started up a new terminal and did this:

roger@roger-laptop:~$ kuiper
roger@roger-laptop:~$

Gwuh? Now this was an unexpected result! Obviously it's finding the file, but running it had no effect. I tracked this down to a Ruby idiom we've all seen before:

if $0 == __FILE__
start_kuiper
end

But this doesn't work with gems! Rubygems doesn't actually install your script in /var/lib/gems/1.8/bin/, they install their own script there, which itself calls yours. Luckily, Rubygems names this script after the one it's calling, so you can modify the usual idiom as so:

if File.basename($0) ==
File.basename(__FILE__)

start_kuiper
end

Now when I ran the game, I got what I was expecting, which is to say a giant list of tracebacks because it couldn't find the resources it needed. (Put in a bug to fix that gracefully: #121) Locating your data files turns out to be relatively simple:

begin
require 'rubygems'
Gem.activate('kuiper',false)
rl.dirs << Gem.loaded_specs['kuiper'].
full_gem_path
rescue LoadError
# Do something else
end

The begin/rescue part in there serves two reasons: Obviously, it'll catch the case where Rubygems isn't loaded. Less obviously, Gem.activate will also throw a LoadError if it can't find its gem. The Gem.activate bit looks for a gem by the name you give it, and 'activates' it - so far as I can tell, this is the same as requiring it via a 'gem' command (which I'll show later). This step is necessary to populate Gem.loaded_specs with our gem's spec. Finally, full_gem_path is - like it sounds - a full path to where the gem installed. In my case, that's:

ls /var/lib/gems/1.8/gems/kuiper-0.0.1/
bin data images lib tests

These directories are all the stuff I told the spec to include (via spec.files) - which includes my data files.

Finally, a bit on interacting with other gems. I made my gem depend on the rubygame gem being installed - now how do I go about letting my program require it?

begin
require 'rubygems'
gem 'rubygame', '>=3.0.0'
rescue LoadError
# Nope, either no gems or no rubygame
end

require 'rubygame'

Like before, the begin/end block there will make sure I have both gems and rubygame installed. Here instead of using the Gem API directly, I use a convenience function to see if Rubygame is there. The require line is outside of the begin/end so that, if we don't have gems and/or don't have the rubygame gem, we can still run so long as the user has rubygame installed somewhere on their system.

And that, as they say, is that. I'm off to drown my troubles in pseudoephedrine!

Tuesday, April 29, 2008

Day 89: The Gemification

Buckle in, this is going to be a long one: I want Kuiper to be installable as a gem.

The biggest kick in the teeth along this road is that the Rubygem online book itself doesn't really tell you how to do it, only that you should "see the DeveloperGuide for the real meat". The problem in this case is that the Developer Guide does not, so far as I can tell, exist. Thus I give you this:

Roger's (comparatively) Pain-Free Gem Creation Tutorial (with a bit of Rakefile stuff thrown in)

As you might expect, it's weighted heavily toward my game's deployment needs. Still, it's somewhat useful.

The first step, because we want this to be an easily re-done process, is to create a Rakefile. This is pretty simple, because it's Ruby code itself. The first part of my Rakefile, I decided, would run the unit tests I love so very much. This does the trick:

require 'rake/runtest'

task :test do
Rake.run_tests 'tests/tests.rb'
end

"tests.rb" is a file that just requires all my other tests; rake knows to do the right thing.

Next, I made a rule to build my documentation:

require 'rake/rdoctask'

Rake::RDocTask.new do |rd|
rd.main = "lib/kuiper.rb"
rd.title = "Kuiper RDocs"
rd.rdoc_files.include("lib/**/*.rb")
end

Tasks like the test one are easy; I'd already known about run_tests. But what the frak is a Rake::RDocTask, and how did I know what to do with it? Here the Rake RDocs (coincidentally, the link above to 'Rakefile' takes you straight to the docs) were invaluable. Every special task is documented there with examples of how to use it.

Next, the big tomato. Creating the gem:

require 'rake'
require 'rake/gempackagetask'
require 'rubygems'

Rake::GemPackageTask.new(gem_spec) do |pkg|
pkg.need_tar_bz2 = true
pkg.need_zip = true
pkg.need_tar = true
end

You didn't really think it'd be that easy, did you?

Each gem relies on a 'spec' to tell it minor things like what to put in the gem and what that gem requires. That's where 'gem_spec' comes from in the example above. A reference to everything that goes in it can be found here and I relied on it extensively.

Here's what my spec looks like, broken down:

gem_spec = Gem::Specification.new do |s|

This creates the actual Specification object and starts a block to initialize the whole thing. We're going to do some heavy changes to that 's' variable!

s.name = "kuiper" # The name of our gem
s.author = "Roger Ostrander" # Me
s.email = "denor@users.sourceforge.net"
s.homepage = "http://llynmir.net/projects/kuiper/"
s.summary = "Top down 2D space action/rpg "+
"hybrid"
s.description = "Kuiper is a single player "+
"top-down 2D space RPG game, "+
"in the spirit of Escape "+
"Velocity and Tradewars."
# Do we have rdoc for this package?
s.has_rdoc = true

This is just some basic information about the game. Most of it's self-explanatory, I just want to say that 'summary' is what will show up as a result of the 'gem list' or 'gem search' commands. 'description' is basically a longer summary.

# Our version number is [major,minor,bug]
s.version = $KUIPER_VERSION.join(".")
# This specifically refers to the Rubygame
# gem. A .rpm or .deb will not, as of right
# now, do the trick.
s.add_dependency('rubygame', '>=3.0.0')

Version info - first comes ours (which I keep in a central place in one of my source files, to make it easy to change) and then those we rely on. Like the comment says, this specifically refers to other gems; when I installed my gem it complained because I hadn't installed the rubygame gem (even though Rubygame lives elsewhere on my system)

# The directory of the script to start the game
s.bindir = "bin"
# Scripts to install
s.executables << 'kuiper'

I'm making a game, not a library, so this is a little awkward. These lines basically tell the gem system that I'm installing a binary (in reality, the 'kuiper' part there is another ruby script that will start things up), and so to put that file in the gem bin directory. I then discovered that Ubuntu doesn't include this directory in the $PATH by default, for those wondering it's "/var/lib/gems/1.8/bin/"

s.require_paths = ["lib"]

This tells Rubygems where (else) it should look for files it's requiring. The standard is to put source files in "lib", so I've done that and indicated this fact.

s.rdoc_options << '--title' << 'Kuiper RDocs' <<
'--main' << 'lib/kuiper.rb'
s.test_file = 'tests/tests.rb'

This is me repeating myself; though we've already got rake tasks to do these, people can test gems on their own, and also get documentation for them (when I installed the gem I created, in fact, it automatically installed the documentation with it).

Finally,

s.files = FileList.new do |fl|
fl.include(
"{lib,ext,samples,doc,images,data}/**/*")
fl.exclude(/svn/)
end
end

These are all the files to be put into the gem! And with that, you can go to the command line and go 'rake gem'.

Tomorrow: Part 2

EDITED: Because blogger preview sucks at showing you what will get cut off by its borders.

Monday, April 28, 2008

Day 88

Weapon unit tests: COMPLETED!
Minibuilder: COMPLETED!

That's the real content for today. The rest will be a love letter to unit testing.

Yesterday when I made the game run on windows, I had to tweak the XML handling - apparently on whatever version of ruby I'm running over there, attributes you give to XML bits aren't automatically made into strings like they are on my linux box. So I changed it to explicitly use to_s.

That worked fine until today, when I finished up the weapon unit tests. I ran the suite (I have a script that runs all of them) and found out that it failed, which wasn't a surprise until I discovered that it wasn't the weapons failing at all! It was older code that had broken with the above change. Turns out nil.to_s is '', and '' gets read back in as an empty string, which for my purposes is vastly different than nil. I solved this by having it only set non-nil attributes in the XML to begin with, but the point is I never would have found it until it bit me on the ass in unpredictable ways if not for testing.

But finding obscure places where I've broken my code isn't their only purpose! After finishing the weapons tests, I went back to the minibuilder (also done today!) and fleshed out the functionality. Removing was enabled, and I found it worked pretty much how I expected and how the tests predicted it would, except in one case: I couldn't remove secondary weapons.

I went back to the test cases to find that I'd never written a test for that! I did so and delighted to see that it failed. Found the problem, patched it up. In this case I would probably have tracked down the bug without the tests just fine, but now I'll be alerted if it ever happens again.

P.S. I like you, unit testing, do you like me?

43 tests, 140 assertions, 0 failures, 0 errors

I'm not hearing a no.... :)

Sunday, April 27, 2008

Day 87

And now for something completely different!

Kuiper is working on Windows. I don't know what, exactly, got me started on the idea, but I began today idly trying to get Rubygame to build on Windows. I followed along with the wiki instructions and made some minor changes. The biggest problem arose when I discovered that asprintf wasn't supported. So now Rubygame will incorrectly convert unicode unto UTF-8 in the unlikely event that it's given a 513-byte unicode character to convert. (It won't break, however, because snprintf is supported). Also the code wasn't freeing the result of asprintf, so I may have actually plugged a memory leak by accident.

If you're compiling Rubygame 2.x, you're probably fine with the above instructions. But I was compiling the subversion 3.0 stuff, which includes Chipmunk, and the chipmunk build process isn't integrated with the rest of it. Building Chipmunk failed with, among a litany of other warnings:

make: *** No rule to make target '{.', needed by 'chipmunk.obj'. Stop.

All the searching in the world couldn't help me build it; in the end I found a prebuilt .so file (via this thread), which I plunked down into c:\ruby\lib\ruby\site_ruby\1.8\i386-msvcrt, and it seemed to work. Of course, if you go this route, you're probably not going to be able to use any of the chipmunk-based features of the new Rubygame, but I'm not so it wasn't a problem for me.

All this made me think that I need another metric for progress. I've got time spent, but that's pretty constant. I've been using lines of code, but all this time I only added one line of code to kuiper proper (if it can't find $HOME, it looks for $USERPROFILE, which is apparently the windows equivalent). It's killing my LOC/hour performance :)

Saturday, April 26, 2008

Day 86

Programming Tip of the Day: There is a difference between '=' and '=='.

If you take a look back to yesterday's entry you might notice something about the minibuilder. Specifically, there are two entries in it that look almost the same: "wpn_lazor" and "wpn_lazor:2279". What that second one means is that it's been duplicated from the original wpn_lazor. That's the intended behavior; ships' weapons have to be duplicates of the original because the weapons have information about how long until they can fire again, etc, built into them. There was a bug early on where one ship got the weapons of every ship in the sector (because they weren't duplicates) and would fire an insane amount of stuff at you while the others flew around dumbly. So it's necessary.

But that screenshot illustrates a bug as well! You should never see the original wpn_lazor - wpn_lazor:2279 should instead internally have an amount of '2'. This is because they don't compare equal, and so the engine doesn't know to stack them.

As I delved into the weapons systems to get a unified feel, I kept discovering more and more kludges. The :weapons accessor only gives you primary weapons. Ammunition added to a ship somehow ends up getting registered as a primary weapon! I'm amazed this stuff works at all; it seemed pretty straightforward at the time.

Thus, today's work was almost entirely on bug #119: Weapon Unit tests. To make sure, once and for all, that they're working the way I expect. It feels like I'm going backwards (weapons were, after all, the stuff of the last milestone) but it needs to be done if I want the minibuilder to work, and I need the minibuilder for my current milestone.

I also created bug #118, wherein we warn the scenario editor if they're trying to put the player's ship up for sale. Because it's a ship like any other, and scenario creators might think "Hey, people might want to buy their original ship." If they did that, they'd get a ship that was exactly as damaged as their original ship! It does give me an idea, however: When you buy a new ship, the old one can be put up for sale in its old condition - that way you can always trade back.

Friday, April 25, 2008

Day 85

Programming Tip of the Day: If your sprite isn't rendering, try actually adding it to the screen!

I got a new server yesterday to replace the one that used to house my trac and SVN repository. I installed Ubuntu on it and tried to upgrade, only to find that this is taking forever. Turns out a new version came out mere hours before my server arrived. Not only was the install I had out of date, but due to everyone hammering the servers, it'll take forever to get up to date!

This gave me extra time to think about the blog post I made. I sat down with an actual honest-to-gods pen and paper and drew what I wanted the mission editor to look like. Then I was like "Hey, I can do that! And it'd be hugely useful to other objects, too!"

Thus, I introduce #117: THE MINIBUILDER!

As terrifyingly powerful as the old build dialog, but bite-sized!

All the functionality isn't there yet. I was halfway through writing up the subclass that would deal specifically with adding/removing weapons from ships (they have special methods to do this) when I thought to myself: Haven't I done this before?

Yes, I have. I made special subclasses of the old builder to handle weapons for ships... and cargo for ships... and addons for ships. The way ships handle them is different from the way planets handle them is different from the way that you'd just throw them in a list. Wouldn't it be nice, I thought, to have a unified API and then just adapt anything that doesn't conform to it?

Thus I created an Adapter class! It has an 'add_item', a remove_item, an 'items', and a 'constraint' (the type that goes into the list). I only have to write a 'Adding weapons to ships' subclass once instead of for every GUI element that does so. How useful, I thought! Then I went online to make sure I was spelling the word 'Adapter' correctly and discovered that my invention already exists and is a pattern. I still felt proud, having independently discovered it.

My server is currently formatting a 500GB disk whose reliability has been called into question. Thus I'm formatting it with the most slow and careful of bad-block checks. I do not recommend this unless you have a great deal of time on your hands, it's taken nearly 24 hours now. Still, if the disk is dodgy, this will find out. And if this doesn't break it, nothing will.

Thursday, April 24, 2008

Day 84

Sitting at home, waiting for my server....

Today I finished the mission flag system, at least so far as the unit tests are concerned.

It occurred to me, though, that all I'm doing is putting off the real work. Granted, mission flags needed to be done at some point, but they didn't need to be done right now. I have enough conditions and actions to put a basic mission together, in fact, I have for a while. The part I don't want to tackle yet is the editor. Making domain objects is easy. Making editors, in fact, is pretty easy with all the refactoring I've done. The problem is that the mission editor is probably going to be unlike anything else I've coded so far. Well that's not true; I could code it exactly like the other components, but that's a very clunky way of editing. Otherwise the scenario-maker is going to have to go through 8 button presses: edit missions, new mission, new condition, pick a condition from a list, accept that condition, new action, pick an action from a list, etc. And that's just to /get/ the mission. No, I'll need a better editor.

A lot of things could use a better editor. Adding weapons or cargo to a planet's stores, for instance, would be a lot nicer as two side-by-side lists with '<' and '>' button between them rather than the list of items, press 'add', pick from a dialog thing that's going on now. In fact, I'm making a bug for it now. Hello, #115!

I also found a new image to be the titlescreen background, which used to be based on this bit of astronomy. Turns out nebulae are far more impressive-looking, even if they aren't what the game is named after. Since the icon and trac logo were based off of that original photo, I had to change them, too. The trac logo is a part of the nebula now, and the icon is a little spaceship.

When I was debating if I should start this blog, one of my primary worries was that I wouldn't have anything to say. Looking back at this and other posts seems to contradict that, but part of me can't shake the paranoia that I'll run out of topics and every update will be "I added 4 lines of code. Yay me."

Wednesday, April 23, 2008

Day 83

I'd like to introduce a feature to this blog that I use at work quite a bit. It's called the Programming Tip of the Day, and it's for when I've made bone-headed mistakes. In Ruby, if you have a method like this:

def check
self.doImportantStuffHere
end

And then later in that same class you have a method like this:

def check
end

The second one takes precedence. Thus today's Programming Tip of the Day: If nothing's happening, make sure the method where stuff happens isn't being overwritten.

Today's major task was getting the mission cycle complete; I have a unit test which awards the player a mission (by asking the mission if it's appropriate and, if so, telling it to give itself to the player) and then tells that same mission to check to see if it's done. Given that the sole condition for completion is that the player be in the starting sector, it is done. This same check will automatically do any attached actions which, in this case, complete the mission.

I started work on the next action/condition group, which is to set and check 'flags'. The player has a set of flags which are essentially a big dictionary of key (the :tag of the flag) value (a string or number) pairs. Their intent is to be used in multi-leg missions: If you want the player to go from A to B to C, you set a flag when they get to B and then when they get to C check it to see if they jumped through the extra hoop.

I'm also trying out the google syntax highlighter for my code snippets above. We'll see how that turns out.

EDIT: Poorly!



Tuesday, April 22, 2008

Day 82

My tags are, I think, going to be things like the ticket numbers of what I was working on and the milestone it's for.

I'm a bit worried about ticket proliferation. When I made my latest milestone, I created a few tickets for it. These were some domain objects, and a few conditions and actions I wanted. It now seems that conditions and actions are dead easy to create and test. If I made a ticket for every possible condition and action in the game, I'd have a ton of them.

Thus far my strategy has been to only create tickets for things I need to remember to do but can't do right now, or things I'm not done fixing at the end of a session so I know where I left off. It's worked so far, but I worry about not knowing when I made a fix/change because it wasn't in the bug tracker. For instance, assigning the player missions was never in the tracker but I got it done today. Hopefully the blog will help with this.

As mentioned, today's work was assigning missions to the player. The mission object itself is capable of determining if the player can have it (by seeing if all its conditions are met), and also capable of giving itself to the player. I feel somewhat bad about having this sort of stuff (what more seasoned professionals might call 'business logic') in the model classes. While it makes it easier to unit test (by far!), I can't get the image out of my head of someone taking a picture of me doing it and adding the LOLCat style caption: "OOP: Ur doin it wrong".

Other stuff I wanted to mention:

I've got a spreadsheet which tells me, among other things, how long I've been working on this. I've decided to name the blog entries after the day. My entry as of today looks like:

Hours worked: 1.25
Goal for today: 1
Total Hours Worked: 132
Lines of Code: 7465 (as counted by CLOC)

If this was livejournal, I'd make my current mood the results of my unit tests. It's not, but today only I think I'll do so.

Current mood: 37 tests, 98 assertions, 0 failures, 0 errors

KuiDevBlog.new

What's a Kuiper?
Kuiper is a single player top-down 2D space RPG game, in the spirit of Escape Velocity and Tradewars. It is created using the Rubygame library and is (unsurprisingly) written in Ruby. Also, It is heavily influenced by my now-defunct TraderMissions.

Why?
Because I want to write a game. I've been coding up games since I was about 8 years old, but the number of completed games I've finished is a very low number indeed. Even TraderMissions, my biggest solo effort until this one, remained incomplete. Finally, I decided that if I was going to get serious about this game development thing, I would have to actually finish a game.

I took a page from NaNoWriMo, an event I've participated (and blogged here!) in a number of times before. The magic way to get something done, it turns out, is a deadline. So I set one for myself: By December 31, 2008, 11:59 pm EST, I'll have released a game or at least submitted it to a number of sites for announcement (I can't control their publishing schedule, after all).

To that end, I'm doing the same thing now I did with NaNoWriMo - working on the game, every single day, one hour a day. Two hours on weekends. It's slow going, but it's progress, and each week I can see noticeable changes in the game. I'm creating this blog for the same reason I made NaNo blogs - public humiliation. It's a staple of NaNoWriMo that you announce your intention to create to as many people as you possibly can, so that fear of later embarrassment keeps you going when nothing else does.

Here goes nothing!