06 June 2013

Objective-C, Day 4

(Warning: more non-FP content)

It's time to get the board representation filled out with a real board. I've only got a couple of hours left before I have to pack up my computer to leave my Undisclosed Location, but let's see if I can get a little more done. Let's review what level 1 looks like:

Level layouts are taken from the original Macintosh Polar game created by Go Endo. These were originally MacOS resources of type 'STGE.' Let's see if we can decode them. Using ResEdit, the raw data for 'STGE' resource ID -16000 looks like:

0x0000 0x0000 0x0003 0x0001
0x0000 0x0000 0x0000 0x0000
0x0000 0x0000 0x0000 0x0000
0x0000 0x0000 0x0000 0x0000
0x0000 0x0000 0x0001 0x0000
0x0000 0x0000 0x0000 0x0000
0x0004 0x0000 0x0000 0x0001
0x0000 0x0006 0x0000 0x0002
0x0000 0x0005 0x0004 0x0005
0x0000 0x0000 0x0000 0x0000
0x0000 0x0000 0x0000 0x0000
0x0000 0x0001 0x0000 0x0000
0x0001 0x0000 0x0000 0x0001
0x0000 0x0000 0x0000 0x0000
0x0000 0x0000 0x0000 0x0000
0x0000 0x0000 0x0000 0x0000
0x0000 0x0000 0x0000 0x0005
0x0000 0x0000 0x0000 0x0002
0x0003 0x0000 0x0000 0x0001
0x0001 0x0000 0x0000 0x0000
0x0000 0x0001 0x0000 0x0000
0x0000 0x0000 0x0000 0x0000
0x0000 0x0000 0x0000 0x0000
0x0000 0x0000 0x0000 0x0000
0x0000 0x0000 0x0000

There are 99 16-bit values. My best guess is that this corresponds to the 24x4 grid (96 board positions) plus 3 extras for some kind of of header of footer data (maybe the total number of hearts is indicated, for example). There are 7 unique values, so it seems extremely likely that they correspond almost exactly to our eight different tile types, with zero representing a blank space. But the counts of each type don't _quite_ match up. The first board has 8 trees, 1 bomb, 2 hearts, 2 ice blocks, 2 mountains, 3 hearts, 1 house, and 1 penguin (there is always 1 penguin), while this 'STGE' resource has: 9 ones, 2 twos, 2 threes, 2 fours, 3 fives, and 1 six. The counts are very close, so this has to represent level 1, and the the 5 almost certainly represents a heart, but I'm not clearly seeing the layout. The first vertical column goes penguin, tree, tree, tree. I don't quite see a pattern that looks like that, but resources -1599 and -15996 give me a hint that the "extra" data is at the front: they contain 0x0007 and 0x0008 as their third values. Those don't appear anywhere else so they probably don't indicate tiles. So let's try rearranging resource -16000 without the first 6 bytes, remove redundant zeroes for clarity, and looking at the values aligned by groups of 24 instead of 4:

1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 4 0 0
1 0 6 0 2 0 5 4 5 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 0 0 0 2 3 0 0
1 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

There's the board. The left column is actually all trees -- when the board first appears, the penguin is hiding a tree. There are actually nine trees. So the encoding looks like so: empty space = 0, tree = 1, mountain = 2, home = 3, ice block = 4, heart = 5, and bomb = 6. The penguin doesn't have a value, but his starting position is probably represented by the first two values, (0,0), most likely encoded as row index, column index to correspond to row-major indexing, and there are 3 hearts to count down as the game is solved.

Can we validate this with the second board? Yes, it looks like there's a 4 indicating 4 hearts. In all the boards I've seen so far (the first four), the penguin is in the upper left. The fifth resource has 0, 1 for its first two values, so I'm guessing I can confirm the encoding of the penguin's position if and when I get to that stage.

And now to come up with a quick way to populate the board in my code. As I'm still a complete n00b in Objective-C, and on the general principle that there's no real need to make this any more clever or less obvious than it has to be, let's just use a string with the data from the resource. Let's add an init method to the interface for the model class:

- (id)initWithLevelIndex:(int)level_idx;

And here is some data, and an initializer method:

static const int num_polar_levels = 1;
static const int polar_data_len = 96;
static const int polar_data_num_tile_vals = 7; // 0-6 inclusive
static const int polar_data_max_tile_val = 6;
static const NSString *polar_levels[num_polar_levels] =
{
    @"100000000000000100000400" \
    @"106020545000000000100100" \
    @"100000000000000050002300" \
    @"110000100000000000000000"
};

- (id)initWithLevelIndex:(int)level_idx
{
    // Call our own basic initializer. This will 
    // result in redundant setting of board values,
    // but maybe I will clean that up later.
    self = [self init];

    // A simple lookup table to decode the original
    // Polar resource data as strings
    ArcticSlideTile 
        *polar_data_tile_map[polar_data_num_tile_vals] = {
        empty, tree, mountain, house,
        ice_block, heart, bomb };

    if ( level_idx > ( num_polar_levels - 1) )
    {
        NSLog( @"initWithLevelLayout: level %d out of range!\n",
               level_idx );
        self = nil;
    }
    else
    {
        const NSString* level_str = polar_levels[level_idx];
        unsigned int level_data_idx = 0;
        for ( unsigned int idx_y = 0;
             idx_y < board_height; idx_y++ )
        {
            for ( unsigned int idx_x = 0;
                 idx_x < board_width; idx_x++ )
            {
                NSRange range = NSMakeRange(level_data_idx, 1);
                const NSString * item_str =
                    [level_str substringWithRange: range];
                int polar_data_tile_val = [item_str intValue];
                if ( polar_data_tile_val >
                    polar_data_max_tile_val )
                {
                    NSLog(@"tile val %d out of range!\n",
                        polar_data_tile_val );
                    self = nil;
                }
                else
                {
                    board[idx_y][idx_x] =
                        polar_data_tile_map[polar_data_tile_val];
                    level_data_idx++;
                }
            }
        }
    }
    return self;
}

Hmmm, that seems overly complicated. There probably is a better, more idiomatic Objective-C way to use NSStrings for that, but I'm tempted to just write it with a basic C string. Using NSString objects in this context didn't even really help me catch bugs or avoid crashes, since I had forgotten to initialize a heart object and the result was hitting a nil object pointer at runtime and crashing, pretty much the same result as dereferencing a null pointer in straight C except with a better error logged. I'm a little disconcerted by Objective-C's inability to allocate objects on the stack, but that comes down to the aforementioned "thin veneer" over C. I don't really care about the overhead in using method dispatch in this simple piece of code operating on such a small amount of data, but I do care about simplicity. Just to compare, here's a more straightforward C implementation of that inner loop:

static const char *polar_levels[num_polar_levels] =
{
    "100000000000000100000400"
    "106020545000000000100100"
    "100000000000000050002300"
    "110000100000000000000000"
};

int polar_data_tile_val = level_str_p[level_data_idx] - '0';
if ( ( polar_data_tile_val < 0 ) || 
     ( polar_data_tile_val > 
       polar_data_max_tile_val ) )
{
    NSLog(@"polar data tile value %d out of range!\n",
          polar_data_tile_val );
    self = nil
}

Maybe the lesson here is "use Objective-C objects only when objects are a win."

I'm going to continue with this project, developing the code to handle the interactions of objects on the playing field and eventually the user interface. However, now that my week away from home is done, I might not be able to make progress very quickly. Stay tuned -- I'll post what I can, when I can. As always, if you have any comments or questions, I'm happy to have feedback.

No comments: