i’m so full of ideas

not a fluke

February 17, 2010 5:48 am

I was holding my breath, wondering what the first full day of sales would be like. iTunes Connect had the daily report available exceptionally early today, so I didn’t have to wait long. The final total: $113.39.

Okay, wait. Just stop a minute. Um. Holy crap! Over a hundred dollars! In one 24-hour period!

Putting this into perspective. I’m sure this is a spike, induced because I’m forcing all the dedicated Hearts Solo players into the Hearts Net camp. It won’t stay that high forever. And my standard of living is such that I can’t survive on a hundred bucks a day. But if I had several projects like this, I could cut back on the contracting work that I do. Maybe even quit altogether eventually.

I’d planned to write more about the spat I seem to be having with some of the grumpy Hearts Solo players, but … no. I stopped reading the reviews, but my friend Steph didn’t. “Class action suit?” Really? Yeah, I’m sure you’ll get back every penny you all spent on my program. Seriously, this is beyond parody at this point. There is nothing I could say to that level of vitriol.

There’s a lot I can say to a hundred bucks a day, though. And here it is: lots and lots and lots of updates. I’m almost finished with the theme-changing feature. I’m just waiting for my best beta tester to have time to check it out. Then it’s headed to the App Store.

“Crook”

February 16, 2010 3:22 pm

Man! I’m not even reading the Hearts Solo reviews anymore. I did scan down the list of titles briefly, though. I used one of them as the title of this blog post: “Crook.”

You know what happened to this particular “crook” today? He made $54.12 on sales of Hearts Net. That’s for approximately the first 12 hours that the new Hearts Solo has been in the App Store.

That is not just “a good day” for me. That is five times greater than any other sales day I’ve had so far. This is, in fact, the first occasion where I have a glimmer of hope that the time I’ve spent on this game will be worth it. If you people keep buying $54.12 worth of Hearts Net every day, I will keep working on it until it is PERFECT. Internet multi-player, chat rooms, statistics, rules variations, Mac version, Windows version, Android version, and the most fiendish, clever robot players in the world. I’ll make you the best damned Hearts game you ever played. All for $54.12 per day.

By far, the biggest legitimate complaint I’ve gotten is that some people don’t like the color schemes, either for Hearts Net or Hearts Solo. Okay, I got the message! I’m going to release a new version of Hearts Net pretty soon, which will allow you to pick any one of the three themes that the game has had in its life. If you were fond of the plain old green-and-red theme, you will be able to get it back. If you’re willing to pay for Hearts Net, of course.

Now, a casual rundown of the various hate mail. A lot of people say I “tricked” them into downgrading to a crippled version of the program. I said exactly what was going to happen in the “what’s new” notes, and on my website. Where else would you have expected me to put this information?

Have you stopped to think what you’re complaining about here? You got Hearts Solo for free. FREE. For like eight months. And you were apparently so attached to it that you are now frothing at the mouth over its loss.

One of the people who angrily emailed me today said that he has played over 750 games. Apparently, this was supposed to make me feel bad? Because I’m taking this precious resource away from him? So he sent me this angry email, rather than just spending three bucks to get Hearts Net.

Folks, I tried to do this the “polite” way. Hearts Net has been out for over two weeks now. I have many indications that the heavy players are aware of it. It was completely, utterly ignored. Okay then, now we’re doing it my way. And on the first day, I made $54.12. Are you really going to claim I did the wrong thing here?

So, you guys just go right on shouting, as loud and long as you want. The feedback pages for App Store free apps are always a study in casual, drive-by negativity. I know a lot of you enjoy being pissed off over things like this. Okay then, you got your wish! But you are not my customers. My customers are the people who spent $54.12 on Hearts Net, on the first day that I crippled Hearts Solo. It is those people who I’m interested in. To the rest of you: Bon Voyage!

A word for my longtime RSS subscribers

5:00 am

According to my access logs, the RSS feed for this blog is the most popular thing on my website, by a large margin. I know next to nothing about RSS, so I can’t gauge how many readers I have. Could be two, could be a hundred.

Most of the articles I write here are about technical subjects. I assume most of you found one of them via web search, liked what you saw, and added my blog to your RSS reader. That’s great! I love having readers.

But now, based on the nerd rage I see in my inbox, I suspect I’m going to be writing a lot of articles that are aimed at users of my card games, rather than technical articles. I think these people would be more sympathetic to my plight if they understood it, so I’m going to be writing about that. This is liable to put you off, if you came here for my technical articles, causing you to unsubscribe from my RSS feed.

Instead of doing that, I encourage you to subscribe to a subfeed, in which I’ll post “the good stuff.” Pretty much all of my technical articles include the tag ‘programming’, so I’m going to use that one to for those articles from now on. Here’s the new URL to add to your RSS reader:

http://www.platinumball.net/blog/category/programming/feed/

Hello, iPhone hearts players

February 15, 2010 1:55 pm

I just got the “available for sale” email from Apple, so the new version of Hearts Solo should be available on the App Store pretty soon. The good news: it now has all the user interface improvements I added to Hearts Net, and also the improved robot players. The bad news: you probably won’t be able to finish an entire game.

Here’s a review I got on the iTunes App Store just a couple of days ago, for Hearts Solo:

I’ve now played 150 games against the ai players and it’s a great implementation! It can be a bit easy to shoot the moon (something apparently improved in the author’s hearts net) but still MUCH better than most other iphone hearts games. Highly recommended, great fun.

So, let’s review. This person has spent many, many hours playing the free version of my game. He is aware that there is a better, paid version of the game. He is even aware that the paid version fixes a criticism he has of the free version. Yet he is apparently not willing to spend just a couple of bucks buying Hearts Net.

I’m sorry folks, but this is not acceptable. I know the App Store gives you strong clues that all apps ought to be free, but I’m not going to play like that. If I can’t get paid for this kind of work, then I’m not going to do it at all.

By crippling Hearts Solo, I’m trying to send the message that it’s just for evaluation purposes. It has been downloaded over 30,000 times. So this is my appeal to all 30,000 of you: If you like it, spend some money on the paid version. If you don’t like it enough to pay for it, then this is where we part company.

iPhone and iPad SDK coexistence

February 14, 2010 7:14 am

Apple has released a new SDK specifically for iPad. It’s not yet ready for prime-time, so if you want to continue to develop for iPhone, you’ll need to use both the old and new SDKs for awhile. Curious about the logistics of this endeavor, I googled up this article. I dutifully followed the instructions and installed the beta 1 release in a separate dev directory. Yeah, I’m calling shenanigans. It did not work. I think it’s a bit irresponsible to post something like that without having tried it.

You can’t completely separate the two SDKs. According to the release notes, the one that is installed last is the one whose compilers will be used for both. I had other problems as well. Both the old and new device simulators were crashy. So I completely wiped both sets of dev tools and reinstalled the iPhone SDK.

Recently, Apple released beta 2 of the iPad SDK. They may have fixed the problems I experienced before, but I’m not taking any more chances. This time, I got myself an external Firewire hard drive. USB 2.0 would also work, but I prefer Firewire for a possibly frivolous reason: I have so many USB devices that it’s difficult to find a place to plug in a new one, but I have two Firewire ports on the back of my Cinema Display that are otherwise unused.

This next part is important. You’re going to need to boot off this drive, so its partition map scheme must be set to “GUID Partition Table.” If it isn’t, you should reinitialize it so that it is. On the Mac, you use the Disk Utility program for this. It doesn’t create GUID partition tables by default, so you have to press the button that says “Options…” and change it.

Next, use a program like Carbon Copy Cloner to make a copy of your primary hard drive onto the external drive. Finally, in System Preferences, pick “Startup Disk,” select your external hard drive, and reboot. Now install the new iPad SDK. It will be copied onto your external disk, leaving your primary disk alone. This way, the two SDKs won’t butt heads with each other.

It’s a shame that Apple is making us solve a software problem with hardware, but this is the sort of inconvenience you have to put up with if you want to live on the bleeding edge. I can’t even remember the last time I had to use the Startup Disk pref pane before this. It’s been years, surely. This reminds me of when I used to work at Be, and we had a new version of BeOS to install every couple of weeks.

iPhone cert FUD

February 12, 2010 9:24 am

I finally got around to installing Snow Leopard on my MacBook Pro. I don’t believe in putting a new operating system on top of an old one, so I started over from scratch, on a brand-new hard drive. Which explains why I waited so long. Installing everything from scratch is torture.

A couple of days ago, I could no longer postpone reinstalling all the dev certs and crud you need to make iPhone apps that you can put on real iPhone hardware. As I was installling all that stuff, I encountered this interesting text from Apple’s developer website, concerning development certificates:

It is critical that you save your private key somewhere safe in the event that you need to develop on multiple computers or decide to reinstall your system OS. Without your private key, you will be unable to sign binaries in Xcode and test your application on any Apple device. When a CSR is generated, the Keychain Access application creates a private key on your login keychain. This private key is tied to your user account and cannot be reproduced if lost due to an OS reinstall. If you plan to do development and testing on multiple systems, you will need to import your private key onto all of the systems you’ll be doing work on.

Well! From the sound of that, you would be forgiven for thinking that, if you don’t back up your private key, you’ll be completely dead in the water, never to write another iPhone app again. That’s certainly what I thought when I was setting this stuff up the first time, about a year ago. So I did just what they told me to, and I backed it all up. I was reading somebody else’s blog recently that claimed the text quoted above was literally true. Which is why I’m writing today.

Actually, no. If you don’t back up that stuff, you won’t be dead in the water. I found my cert backups from a year ago, and tried to use them, but it was just too painful. Nothing about Apple’s iPhone code signing process makes much sense. When you get stuck, it’s best to just blow everything away and start over.

Which is just what I did! I removed everything from my keychain that had anything to do with iPhone development, I threw away all my provisioning profiles, and I started over from scratch. It went a lot easier this time. I think I’ve finally been doing this long enough to understand a few of the concepts.

From the sound of it, I think the only time you’d really need to follow Apple’s backup device is if you plan to use two or more Macs for iPhone development simultaneously. I’ve never needed to do that, so that’s one less thing to worry about.

hearts progress

February 11, 2010 9:04 am

I’m about to release new versions of both my hearts games. I plan to add a link on the help tab that will show people the latest blog entries pertaining to my card games. I guess that means that I should have at least one blog entry to link to, which is why I am writing this. Heh!

As of this writing, Hearts Net has been in the app store for 12 days. So far I’ve made $64.11 on it. Not exactly getting rich, but I’m still twiddling the knobs. Yesterday was my most profitable day yet, so the situation could still pick up. There’s a lot of stuff I can try.

My best beta tester to date discovered that wifi games won’t work in environments with restrictive firewalls. (Thanks, Philip!) I know what it will take to fix it, which I plan to roll out in an update. If anybody else has run into this problem, please speak up. If it’s affecting a lot of people, that would make it a higher priority.

Personally, I think it’s more important to port the game to the Mac. Apple’s App Store policies have conspired to convince people that iPhone apps should be $0.99 or free, so that’s working against me. Mac users have a reputation for paying for software they use, so I think I’d rather be in that market.

Yes, I know that almost all of you are clamoring for configurable rule variations and better Hearts robots. I’ll get there, but I can’t do all this for free. If the Mac version turns out to be more profitable, then I’ll be more in the mood to do all the stuff you guys want me to.

willow weep for me

February 2, 2010 10:53 am

Last year I went through a whole bunch of different ideas for iPhone apps I might write. I dawdled for way too long, spent too much time working on apps that I ultimately scrapped, and generally wasn’t very efficient with my time. But I finally got something into the app store in June of last year. I made it free, because I wasn’t sure how it would go over. Taking people’s money is Serious Business.

I released a card game that I call Hearts Solo. It has been pretty popular. To date, it has been downloaded by 27,942 people. Of that number, 5,536 upgraded to version 1.1.0 when it was released. So I felt that I had adequately paved the way for a better version that I could charge real money for.

Once again, I took way too long with the follow-up. Adding network play turned out to be pretty close to the hardest programming challenge I’ve ever attempted. It sounds easy enough, especially since I’ve done so much network programming in the past, so I can’t explain it. My best guess is that I didn’t choose a very good internal structure for the code. The practical upshot is that it took me about six months to finish.

I put Hearts Net on the App Store on Friday the 29th at $3.99. Two people got the game with promo codes I handed out. A third person bought it with actual money, but I happen to know that it was because she doesn’t know how promo codes work and screwed up, because she emailed me about it. So I lowered the price to $1.99, and the next day I got six sales, none with promo codes. So I figured, hey, things are looking up. The next day, five sales. Yeah, this is not looking good. So I made the game free, hoping that will help get the word out, and I can start charging later.

Yesterday was the first day that the game was free. Total downloads: 1,717. My best day before this was when I released version 1.1.0 of Hearts Solo. It was downloaded 590 times on July 31.

SO. Here’s what this says to me. There is an awareness of my game among the people who might want to play it, and a fair amount of demand. Quite a few of those people are playing it. I gave them a free version to start, so they know what they’re getting into. But they’re not willing to pay even $1.99 for it?

Come on. Really? Not even $1.99? You’re breaking my heart here.

iPhone: UIImage rotation and scaling

January 31, 2010 8:03 pm

Hello to my three remaining blog subscribers! Long time no see!

For my first post back after my long hiatus, I’m going to revisit the single most popular entry I’ve written to date, which was about UIImage rotation. I’ve used that code a great deal since I first wrote it. I’ve modified it several times. It’s pretty near perfect at this point.

The original code I modified for that earlier post was for dealing with photos taken with the iPhone camera. I stripped out a lot of that stuff, because I was only interested in rotating images that were embedded in the program. And then, wouldn’t you know it, I got a contract job that required me to deal with iPhone camera images as well. So I had to revisit the subject.

The original code from blog.logichigh.com had the rotation and scaling all lumped together in one chaotic function. I split it out into separate rotation and scaling methods, so they can be used independently. It also makes for easier code maintenance.

The code contains [UIImage rotate:], which works the same as the last time around, but it has been streamlined a bit internally. It will rotate any UIImage to any orientation, with or without mirroring. Then we have two scaling methods, the simplest being [UIImage scaleWithMaxSize:]. You provide it with a float value, which is the largest width and/or height that you want the output image to have. If the input image is already smaller than that, it won’t be scaled.

Finally, the all-singing, all-dancing method for massaging photos from the iPhone camera: [UIImage rotateAndScaleFromCameraWithMaxSize:]. It examines the EXIF data in the image and, if needed, rotates it to the proper orientation. Then it scales the image to the maximum width and/or height supplied to the method.

This code has been thoroughly tested in several real-world iPhone projects. There are no known bugs. If you find one, I’d love to hear about it, so I can fix it.

Here’s the header file, WBImage.h:

// WBImage.h -- extra UIImage methods // by allen brunson  march 29 2009 #ifndef WBIMAGE_H #define WBIMAGE_H #import <UIKit/UIKit.h> @interface UIImage (WBImage) // rotate UIImage to any angle -(UIImage*)rotate:(UIImageOrientation)orient; // rotate and scale image from iphone camera -(UIImage*)rotateAndScaleFromCameraWithMaxSize:(CGFloat)maxSize; // scale this image to a given maximum width and height -(UIImage*)scaleWithMaxSize:(CGFloat)maxSize; -(UIImage*)scaleWithMaxSize:(CGFloat)maxSize  quality:(CGInterpolationQuality)quality; @end #endif  // WBIMAGE_H

And here’s the implementation file, WBImage.mm:

// WBImage.mm -- extra UIImage methods // by allen brunson  march 29 2009 #include "WBImage.h" static inline CGFloat degreesToRadians(CGFloat degrees) {     return M_PI * (degrees / 180.0); } static inline CGSize swapWidthAndHeight(CGSize size) {     CGFloat  swap = size.width;     size.width  = size.height;     size.height = swap;     return size; } @implementation UIImage (WBImage) // rotate an image to any 90-degree orientation, with or without mirroring. // original code by kevin lohman, heavily modified by yours truly. // http://blog.logichigh.com/2008/06/05/uiimage-fix/ -(UIImage*)rotate:(UIImageOrientation)orient {     CGRect             bnds = CGRectZero;     UIImage*           copy = nil;     CGContextRef       ctxt = nil;     CGRect             rect = CGRectZero;     CGAffineTransform  tran = CGAffineTransformIdentity;     bnds.size = self.size;     rect.size = self.size;     switch (orient)     {         case UIImageOrientationUp:         return self;         case UIImageOrientationUpMirrored:         tran = CGAffineTransformMakeTranslation(rect.size.width, 0.0);         tran = CGAffineTransformScale(tran, -1.0, 1.0);         break;         case UIImageOrientationDown:         tran = CGAffineTransformMakeTranslation(rect.size.width,          rect.size.height);         tran = CGAffineTransformRotate(tran, degreesToRadians(180.0));         break;         case UIImageOrientationDownMirrored:         tran = CGAffineTransformMakeTranslation(0.0, rect.size.height);         tran = CGAffineTransformScale(tran, 1.0, -1.0);         break;         case UIImageOrientationLeft:         bnds.size = swapWidthAndHeight(bnds.size);         tran = CGAffineTransformMakeTranslation(0.0, rect.size.width);         tran = CGAffineTransformRotate(tran, degreesToRadians(-90.0));         break;         case UIImageOrientationLeftMirrored:         bnds.size = swapWidthAndHeight(bnds.size);         tran = CGAffineTransformMakeTranslation(rect.size.height,          rect.size.width);         tran = CGAffineTransformScale(tran, -1.0, 1.0);         tran = CGAffineTransformRotate(tran, degreesToRadians(-90.0));         break;         case UIImageOrientationRight:         bnds.size = swapWidthAndHeight(bnds.size);         tran = CGAffineTransformMakeTranslation(rect.size.height, 0.0);         tran = CGAffineTransformRotate(tran, degreesToRadians(90.0));         break;         case UIImageOrientationRightMirrored:         bnds.size = swapWidthAndHeight(bnds.size);         tran = CGAffineTransformMakeScale(-1.0, 1.0);         tran = CGAffineTransformRotate(tran, degreesToRadians(90.0));         break;         default:         // orientation value supplied is invalid         assert(false);         return nil;     }     UIGraphicsBeginImageContext(bnds.size);     ctxt = UIGraphicsGetCurrentContext();     switch (orient)     {         case UIImageOrientationLeft:         case UIImageOrientationLeftMirrored:         case UIImageOrientationRight:         case UIImageOrientationRightMirrored:         CGContextScaleCTM(ctxt, -1.0, 1.0);         CGContextTranslateCTM(ctxt, -rect.size.height, 0.0);         break;         default:         CGContextScaleCTM(ctxt, 1.0, -1.0);         CGContextTranslateCTM(ctxt, 0.0, -rect.size.height);         break;     }     CGContextConcatCTM(ctxt, tran);     CGContextDrawImage(ctxt, rect, self.CGImage);     copy = UIGraphicsGetImageFromCurrentImageContext();     UIGraphicsEndImageContext();     return copy; } -(UIImage*)rotateAndScaleFromCameraWithMaxSize:(CGFloat)maxSize {     UIImage*  imag = self;     imag = [imag rotate:imag.imageOrientation];     imag = [imag scaleWithMaxSize:maxSize];     return imag; } -(UIImage*)scaleWithMaxSize:(CGFloat)maxSize {     return [self scaleWithMaxSize:maxSize quality:kCGInterpolationHigh]; } -(UIImage*)scaleWithMaxSize:(CGFloat)maxSize  quality:(CGInterpolationQuality)quality {     CGRect        bnds = CGRectZero;     UIImage*      copy = nil;     CGContextRef  ctxt = nil;     CGRect        orig = CGRectZero;     CGFloat       rtio = 0.0;     CGFloat       scal = 1.0;     bnds.size = self.size;     orig.size = self.size;     rtio = orig.size.width / orig.size.height;     if ((orig.size.width <= maxSize) && (orig.size.height <= maxSize))     {         return self;     }     if (rtio > 1.0)     {         bnds.size.width  = maxSize;         bnds.size.height = maxSize / rtio;     }     else     {         bnds.size.width  = maxSize * rtio;         bnds.size.height = maxSize;     }     UIGraphicsBeginImageContext(bnds.size);     ctxt = UIGraphicsGetCurrentContext();     scal = bnds.size.width / orig.size.width;     CGContextSetInterpolationQuality(ctxt, quality);     CGContextScaleCTM(ctxt, scal, -scal);     CGContextTranslateCTM(ctxt, 0.0, -orig.size.height);     CGContextDrawImage(ctxt, orig, self.CGImage);     copy = UIGraphicsGetImageFromCurrentImageContext();     UIGraphicsEndImageContext();     return copy; } @end

iPhone: keyboard trouble

August 28, 2009 3:07 am
It is possible to type into two or more of these fields at once without dismissing the keyboard

It is possible to type into two or more of these fields without dismissing the keyboard

A couple of months ago, a user of my card game reported a bug. He said that when he tried to change the names of all the robot players at once, only one of them actually changed. I tried to duplicate the problem, but couldn’t. For me, all three robot player names changed. I asked him for more details, but like many non-technical users, he wasn’t able to articulate the problem very well. He said he kept fiddling and fiddling and eventually got all three names changed. I had made a real effort to duplicate the bug and failed. So I wrote this one off as user error.

Over the next few weeks, the same bug got reported three more times. I was still not able to reproduce it. I asked for more specific instructions on how to tickle the bug, but none of this batch of people took the time to send a second email. I can’t blame them. Most people view it as a frivolous time-wasting diversion, useful for whiling away a few minutes while in line at the grocery or something.

Fortunately for me, the fifth report came from a fellow programmer. He provided very explicit instructions, and I was finally able to reproduce it. Now that I understand what was happening, it’s no small wonder that it evaded me for so long. The problem was that my mental model of how the iPhone accepts text input was wrong.

I had assumed the sequence of events was always like this:

• User touches an editable text field
• Keyboard rolls up into view
• User types some text
• User presses the return key on the keyboard
• Program forces the keyboard to disappear
• Program processes the new text

But guess what, here is another possible sequence of events:

• User touches an editable text input field
• Keyboard rolls up into view
• User types some text
User touches a second text input field
User edits the second input field
• User presses the return key on the keyboard
• Program forces the keyboard to disappear
• Program processes the new text

A third possible outcome is that the user navigates away from the current view to a different one, and therefore never presses the return key.

This sounds complicated, but it turns out that the fix is pretty easy. You simply have to treat “user pressed the return key” and “user finished editing” as two separate events, rather than conflating them as one.

The assumption here it that your users are typing into UITextField objects. The fix I’m proposing is for UITextField delegates, i.e., whatever object implements the UITextFieldDelegate protocol. Here’s an example implementation of one of those methods:

-(BOOL)textFieldShouldReturn:(UITextField*)textField {     [textField resignFirstResponder];     return TRUE; }

This is the delegate method that gets called when the return key is pressed. I used to process the newly-edited text field contents in here. That was a mistake which led to the bug I’m talking about. In a typical implementation of this method, the only thing you want to do is make the keyboard go away, which is what the [textField resignFirstResponder] line does.

Here’s the method you should implement to deal with new text field contents:

-(void)textFieldDidEndEditing:(UITextField*)textField {     // deal with new text field contents here }

I can’t show a typical implementation, because only you can decide what you should be doing with the new contents of the text field. But as far as I can tell, this method is always called when the user is finished editing the text field, regardless of why that happened. It could be because the return key was pressed, or because the user switched to a different text field, or because she switched away from the parent view altogether, or perhaps other reasons I’m not aware of.