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.
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;
} } }
package model {
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.