Blackberry version of Hangman

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>

&lt;fx:Script&gt;
&lt;![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};
}
		
]]&gt;
&lt;/fx:Script&gt;

</s:MobileApplication> </code>

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>

&lt;fx:Style&gt;
@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;
}
&lt;/fx:Style&gt;

&lt;fx:Script&gt;
&lt;![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&lt;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();
	
}
]]&gt;
&lt;/fx:Script&gt;
&lt;s:actionContent&gt;
	&lt;s:Button height="100%" label="Exit" click="NativeApplication.nativeApplication.exit()" /&gt;
&lt;/s:actionContent&gt;

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

&lt;!-- block 2 is a v block, top row is pic + instruction block, row 2 is word --&gt;
&lt;s:VGroup width="67%" height="100%"&gt;
	
	&lt;s:HGroup width="100%" height="80%" horizontalAlign="center"&gt;

		&lt;!-- hangman goes here --&gt;
		&lt;s:Image id="hangmanImage"  width="50%" /&gt;
		
		&lt;!-- nstructions, status,e tc --&gt;
		&lt;s:VGroup id="rightGroup" width="50%" paddingLeft="5" paddingRight="5" gap="10"&gt;
			
			&lt;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!" /&gt;
			&lt;s:Label id="gameStatus" width="100%" /&gt;
			&lt;s:Button id="newGameButton" label="New Game" visible="false" click="initGame()" /&gt;
		&lt;/s:VGroup&gt;

	&lt;/s:HGroup&gt;

	&lt;s:Label id="blankWord" width="100%" /&gt;	

&lt;/s:VGroup&gt;

</s:View> </code>

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.

Raymond Camden's Picture

About Raymond Camden

Raymond is a developer advocate. He focuses on JavaScript, serverless and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support.

Lafayette, LA https://www.raymondcamden.com

Comments