Many months ago, I wrote a simple version of Hangman as a desktop Adobe AIR application. It was built using HTML and jQuery and supported a one thousand word database of word choices. This week I looked into porting the application to the Blackberry Playbook. This is what I came up with.

First - I had assumed the port would be simple. The game itself is trivial so I wasn't expecting much trouble. Then something occurred to me. My desktop application listened for key presses to handle letter guesses. As far as I know, that type of setup doesn't make sense on a mobile device. Sure you can get 'keyboard' input, but from what I know it can only be done by using a form field of some sort. To get around this I decided to simply use buttons. Here is an example:

As you can see, the buttons look rather nice. I don't have a physical Playbook yet (no one does outside of RIM I guess), but it looks like it would work well. I'm not happy with "Z" hanging there by itself and would love some suggestions on how it could be improved. I built out this "keyboard" by hand:

<s:TileGroup id="tileLayoutView" requestedColumnCount="5" click="letterButtonClicked(event)"> <s:Button id="btnA" label="A" /> <s:Button id="btnB" label="B" /> <s:Button id="btnC" label="C" /> (I deleted some lines here.) <s:Button id="btnX" label="X" /> <s:Button id="btnY" label="Y" /> <s:Button id="btnZ" label="Z" /> </s:TileGroup>

After a suggestion by Joe Rinehart I tried using a Datagroup, but ran into issues updating the UI controls on the inside. (I'll happily go into details about the issues i ran into if anyone wants to hear more.)

Outside of that the other big change was how the application was laid out. I don't have CSS, but frankly Flex's layout controls are a lot easier for me to work with. Not to offend anyone's delicate sensibilities but laying stuff out in a Flex application reminds me of tables. Using CSS reminds me more of visiting a sadistic dentist. Sorry. Anyway, let me share the code. Here is the initial view which simply sets up the database connection:

<?xml version="1.0" encoding="utf-8"?> <s:MobileApplication xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" firstView="views.HangmanHome" initialize="init()"> <fx:Declarations> <!-- Place non-visual elements (e.g., services, value objects) here --> </fx:Declarations>

<fx:Script> <![CDATA[ protected var dbConnection:SQLConnection = new SQLConnection;

private function init():void { var dbFile:File = File.applicationDirectory.resolvePath("install/words.db"); dbConnection.open(dbFile); navigator.firstViewData = {dbCon:dbConnection}; }

]]> </fx:Script>

</s:MobileApplication>

Here is the main view where of the logic occurs. Note that one feature I did not port was updating the game history. I figured that was pretty trivial and I'd add it later if I wanted.

<?xml version="1.0" encoding="utf-8"?> <s:View xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" title="Hangman" xmlns:mx="library://ns.adobe.com/flex/mx" viewActivate="init(event)"> <fx:Declarations> <!-- Place non-visual elements (e.g., services, value objects) here --> </fx:Declarations>

<fx:Style> @namespace s "library://ns.adobe.com/flex/spark"; @namespace mx "library://ns.adobe.com/flex/mx";

#blankWord { font-size:70px; font-weight: bold; }

#gameStatus { font-weight: bold; } </fx:Style>

<fx:Script> <![CDATA[ import model.Game;

import mx.collections.ArrayCollection; import mx.core.IVisualElement;

import spark.components.BorderContainer;

private var game:Game;

[Embed (source="images/Hangman-1.png" )] public static const H1:Class; [Embed (source="images/Hangman-2.png" )] public static const H2:Class; [Embed (source="images/Hangman-3.png" )] public static const H3:Class; [Embed (source="images/Hangman-4.png" )] public static const H4:Class; [Embed (source="images/Hangman-5.png" )] public static const H5:Class; [Embed (source="images/Hangman-6.png" )] public static const H6:Class; [Embed (source="images/Hangman-7.png" )] public static const H7:Class; [Embed (source="images/Hangman-8.png" )] public static const H8:Class; [Embed (source="images/Hangman-9.png" )] public static const H9:Class; [Embed (source="images/Hangman-10.png" )] public static const H10:Class;

private function init(event:Event):void { trace('init in view called'); initGame(); }

private function letterButtonClicked( event : Event ):void { if(!game.isGameOver()) { var button : Button = event.target as Button; if(button) { trace("clicked: " + button.label); game.pickLetter(button.label); button.enabled = false; drawWord(); drawHangman(); if(game.isGameOver()) { handleGameOver(); } } } }

private function initGame():void {

gameStatus.text = ""; newGameButton.visible = false; for(var i:int=65; i<91; i++) { var s:String = String.fromCharCode(i); trace(s); this["btn"+s].enabled=true; }

game = new Game();

//begin by picking a word game.setChosenWord(pickRandomWord());

//draws the blank/letters drawWord();

//draws the hangman drawHangman(); }

private function drawHangman():void { var misses:int = game.getMisses(); if(misses == 0) hangmanImage.source = null; else { hangmanImage.source = HangmanHome["H"+(misses+1)]; } }

private function drawWord():void { blankWord.text = game.drawWord(); }

private function handleGameOver():void { if(game.playerWon()) { gameStatus.text = "Congratulations, you won the game!"; } else { gameStatus.text = "Sorry, but you lost the game!"; } newGameButton.visible = true; }

private function pickRandomWord():String { var sql:SQLStatement = new SQLStatement(); sql.text = "select word from words order by random() limit 1"; sql.sqlConnection = data.dbCon; sql.execute(); var sqlResult:SQLResult = sql.getResult(); trace('random word is '+sqlResult.data[0].word); return sqlResult.data[0].word.toUpperCase();

} ]]> </fx:Script> <s:actionContent> <s:Button height="100%" label="Exit" click="NativeApplication.nativeApplication.exit()" /> </s:actionContent>

<s:layout> <s:HorizontalLayout paddingTop="10" paddingLeft="10" paddingRight="10" gap="10"/> </s:layout>

<s:TileGroup id="tileLayoutView" requestedColumnCount="5" click="letterButtonClicked(event)"> <s:Button id="btnA" label="A" /> <s:Button id="btnB" label="B" /> <s:Button id="btnC" label="C" /> <s:Button id="btnD" label="D" /> <s:Button id="btnE" label="E" /> <s:Button id="btnF" label="F" /> <s:Button id="btnG" label="G" /> <s:Button id="btnH" label="H" /> <s:Button id="btnI" label="I" /> <s:Button id="btnJ" label="J" /> <s:Button id="btnK" label="K" /> <s:Button id="btnL" label="L" /> <s:Button id="btnM" label="M" /> <s:Button id="btnN" label="N" /> <s:Button id="btnO" label="O" /> <s:Button id="btnP" label="P" /> <s:Button id="btnQ" label="Q" /> <s:Button id="btnR" label="R" /> <s:Button id="btnS" label="S" /> <s:Button id="btnT" label="T" /> <s:Button id="btnU" label="U" /> <s:Button id="btnV" label="V" /> <s:Button id="btnW" label="W" /> <s:Button id="btnX" label="X" /> <s:Button id="btnY" label="Y" /> <s:Button id="btnZ" label="Z" /> </s:TileGroup>

<!-- block 2 is a v block, top row is pic + instruction block, row 2 is word --> <s:VGroup width="67%" height="100%">

<s:HGroup width="100%" height="80%" horizontalAlign="center">

<!-- hangman goes here --> <s:Image id="hangmanImage" width="50%" />

<!-- nstructions, status,e tc --> <s:VGroup id="rightGroup" width="50%" paddingLeft="5" paddingRight="5" gap="10">

<s:Label width="100%" text="To begin, simply type the letter you would like to guess. Right answers will help reveal the mystery word. Wrong answers will lead to your untimely demise!" /> <s:Label id="gameStatus" width="100%" /> <s:Button id="newGameButton" label="New Game" visible="false" click="initGame()" /> </s:VGroup>

</s:HGroup>

<s:Label id="blankWord" width="100%" />

</s:VGroup>

</s:View>

And finally - the simple Game object I created.

package model {

public class Game {

private var _chosenWord:String; private var _usedLetters:Object = new Object(); private var _misses:int = 0;

public function Game() { }

public function drawWord():String { var s:String = ""; for(var i:int=0; i<_chosenWord.length; i++) { var thisLetter:String = _chosenWord.substr(i, 1); if(_usedLetters[thisLetter]) s+= thisLetter; else s+= "-"; } return s; }

public function getMisses():int { return _misses; }

public function isGameOver():Boolean { if(_misses == 9 || drawWord() == _chosenWord) return true; return false; }

public function pickLetter(s:String):void { _usedLetters[s] = 1; if(_chosenWord.indexOf(s) == -1) _misses++; }

public function playerWon():Boolean { return (isGameOver() && drawWord() == _chosenWord); }

public function setChosenWord(s:String):void { _chosenWord = s; }

}

}

And a final screen shot so you can see the result you don't want to see:

I've attached a zip of the project to the blogentry.

Download attached file.