Our first entry comes from Steve Gustafson. Before reading on, please check his application here. Now that you have played with it a bit, let's take a look at selected portions of the code. Let's start with his Application.cfc file:
<cfcomponent output="false">
<cfsetting showdebugoutput="NO">
<cfscript>
this.name="gusBlackJack";
this.sessionManagement = true;
this.sessionTimeOut = CreateTimeSpan(0,0,20,0);
this.scriptProtect = true;
</cfscript>
<cffunction name="onApplicationStart">
</cffunction>
<cffunction name="OnSessionStart">
<cfset session.playedCardList = ''>
</cffunction>
<cffunction name="OnRequestStart">
<cfargument name = "request" required="true"/>
</cffunction>
</cfcomponent>
I want to point out a few lines in particular. First off - notice how he turns off debugging. Why would you do this? ColdFusion debugging can be turned on at the server level, and if you forget to restrict debugging to a particular IP, then everyone in the world will see your debugging information. That's definitely not something you want. I'm a big fan of "Being Anal" in regards to stuff like this. You will notice I do the same in BlogCFC (although I use an Application.cfm file there instead). So - a minor point - but something to keep in mind.
In keeping with the "Being Anal" thread, he also specifies a session timeout and a scriptProtect. Specifying a sessiontimeout is a good idea since in CFMX 7, there was a bug where onSessionEnd wouldn't fire without it. (This has been fixed in 7.01.) ScriptProtect is a handy way to easily protect against cross-site scripting. It isn't perfect - but it is still a good idea to use if you aren't sure you are protecting against it in code yourself.
Now let's take a quick look at index.cfm. I'm only going to point out one thing here and it is more of a minor nit-pick than anything else. The following is from the top of the file, and is not the entire file itself:
<cfsetting showdebugoutput="No">
<!--- This section handles the ajax requests --->
<cfif isDefined('url.action')>
<cfset bjOBJ = createObject("component","blackjack")/>
<cfif url.action EQ 'hitMe'>
<cfset thisRslt = bjObj.dealCard()>
<cfoutput>#thisRslt#</cfoutput><cfabort>
<cfelseif url.action EQ 'newGame'>
<cfset thisRslt = bjObj.newGame()>
<cfoutput>#thisRslt#</cfoutput><cfabort>
</cfif>
</cfif>
<!--- end ajax section --->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Gus's CF BlackJack</title>
<link rel="stylesheet" type="text/css" href="styles.css" />
<script src="xmlhttp.js" type="text/javascript"></script>
<script language = "javascript">
var playerBank = 100;
var wager = 0;
var playerVal = 0;
var dealerVal = 0;
var hasAce = 0;
</script>
</head>
As you can see, he put the code to handle AJAX requests at the top of the file. To me, this doesn't belong here. I'd have a separate file just to handle those requests. Again, this is just my opinion and nothing more. I will, however, harp on something more. Notice how he creates the component for every request. This confused me a bit until I looked at his CFC:
<cfcomponent displayname="BlackJackByGus" hint="This component handles the backend for the Blackjack Application>">
<cffunction name="shuffle" hint="I select a random value" returntype="numeric" access="private" output="No">
<cfargument name="maxNum" type="numeric" required="True">
<cfreturn RandRange(1, arguments.maxNum)>
</cffunction>
<cffunction name="getSuit" hint="I select the suit to be dealt" returntype="string" output="No" access="private">
<cfset suit = shuffle(4)>
<cfif suit EQ 1>
<cfset strSuit = 'Spades'>
<cfelseif suit EQ 2>
<cfset strSuit = 'Hearts'>
<cfelseif suit EQ 3>
<cfset strSuit = 'Diamonds'>
<cfelse>
<cfset strSuit = 'Clubs'>
</cfif>
<cfreturn strSuit>
</cffunction>
<cffunction name="getCard" hint="I select the card to be dealt" returntype='numeric' output="No" access="private">
<cfset card = shuffle(13)>
<cfreturn card>
</cffunction>
<cffunction name="dealCard" hint="I select the card to be dealt" returntype="string" output="No" access="public">
<cfset cardNum = getCard()>
<cfset cardSuit = getSuit()>
<cfset thisCard = "#cardSuit#_#cardNum#">
<cfif ListFind(session.playedCardList, thisCard)>
<cfreturn dealCard()>
<cfelse>
<cfset session.playedCardList = listAppend(session.playedCardList,thisCard)>
<cfif cardNum LT 10>
<cfset cardValue = "#thisCard#|#cardNum#|#session.playedCardList#">
<cfelse>
<cfset cardValue = "#thisCard#|10|#session.playedCardList#">
</cfif>
<cfreturn cardValue>
</cfif>
</cffunction>
<cffunction name="newGame" hint="I begin a new game of blackjack" returntype="boolean" output="No" access="public">
<cfset session.playedCardList = ''>
</cffunction>
</cfcomponent>
What worried me was - how is he keeping track of what cards have been dealt. If you look at CFC, you will see that he references a session variable, playedCardList, to store the used cards. Typically, it is a bad idea to reference any outside scope in a CFC. How would I have done this differently?
First, I would have stored the playedCardList as a variable in the CFC itself. Secondly, I would have then stored the CFC in the session scope. This would kill two birds with one stone. First off - it makes my CFC a bit better in that it is no longer tied to the session scope. If I decide to switch to some other scope, it would be easier since I'm not having to change the CFC itself. Secondly, I would then need to check for the existence of the CFC before creating an instance. This will make the application run a bit quicker since we would only run createObject() once per session.
Another nit - nowhere in his CFC does he use the var scope, and every method (but shuffle) is missing var statements. In getSuit, for example, this line:
<cfset suit = shuffle(4)>
Should be:
<cfset var suit = shuffle(4)>
Similar changes are needed in his other methods. This is a serious problem. Not using the var scope can lead to some very hard to debug problems. It's times like this where I wish I could tell CF to be 'strict' and not let me create variables like that. (I know, I know, CF wasn't built for it. ;)
So, that's it. All in all, this is a nice submission. Good job, Steve!
Archived Comments
All that gets delt is ace of hearts! And I can't double down!
Odd, I'm not seeign that. And no double down was one of the rules. See the original contest rules for more details.
I didnt get my entry done in time, so I cant say much, but one of the 1st things I did was setup CSS rules to use the single .jpg card file, like this:
.card {
width:72px;
height:97px;
position:relative;
display:block;
}
/* Clubs */
#CA {background: url(cards.png) -2px -2px no-repeat;}
#C2 {background: url(cards.png) -75px -2px no-repeat;}
#C3 {background: url(cards.png) -148px -2px no-repeat;}
you get the idea. Then I would just put a div of class card, with an id of whichever card I wanted to display at the time.
<div class="card" id="CA" />
Anyone else do this?
Just a design thought =)
very kewl. The only thing is that Gus needs to hit the casino a little more often. In Blackjack you are dealt two cards in the brginning, not 1. this could be a FF problem though.
Ahah. This entry definitely seems a bit buggy in IE. I recommend folks try FIrefox, and I'll email the author.
It's not just IE. I've had the program not deal cards, or not register dealt card. I had 12 in one hand and hit for a King. My total after getting dealt the King was still 12.
I suspect there are number of locking issues and probably some asynchronous issues with his AJAX.
One of the biggest problems I've seen w/users experimenting with AJAX is the fact that it's asynchronous and not synchronous. Generally, things work for them in development, but things fall apart when the thing goes live.
If I have a function that says:
getRequestA();
getRequestB();
And they both make XmlHttpRequests to the server, there's not guarentee that A will process before B. You'd specifically need to invoke getRequestA() and then have getRequestB() run after getRequestA() finish it's request.
In response to Anthony: when I first loaded up the game I too was dealt two Aces of Hearts, and the dealer had a third one showing. When I took a hit it was a different card, so I know there are other cards in there if you look hard enough :)
[Note: I finished my first hand and started another, and this time I was dealt two Queens of Hearts and the dealer had a third QoH showing. Is anyone else seeing the same pattern?]
Its worth noting that getting duplicates isn't necessarily a logic flaw. I think most casinos play with multiple decks in the shoe, so maybe Steve went uber-realistic and did the same? (Multiple decks in the shoe is a feature that my BlackjackCFC supports, but for this contest I set it to use only one) Still, its strange that I keep getting duplicates on each deal.
Anthony said:
"All that gets delt is ace of hearts! And I can't double down!"
why would you want to double down on 2/12 against an ace anyway??? :)
(but yeah, like Ray said...specs said no double/split/surrender/etc. basic blackjack functionality)
When I play, the dealer and I both get two nine of diamonds. If I stay it is always a tie. If I take a hit I lose. Next hand, same cards.
mark:
that's the "bit buggy in IE" part that ray was referring to above. give it a try in Firefox if you've got it installed.
Architecture aside, I give it points for working like crazy in firefox. I lost all my chips quite quickly!
-Joe
Works great in firefox. Any reason why people are getting the same card over and over in IE?
YIKES!
I'm sitting here reading the comments and licking my wounds.
The reason the same card comes up in IE is because it is IE is caching the the AJAX http request. I have my IE browser set to always check for a new page, so I didn't catch the issue. The solution is to add a random query string to the AJAX request.
As for the asynchronous issue, the slower the connection the more likely this is to show up. It's a simple fix, that I'll put in place... eventually!
As for the Ray's comments:
Normally I would have put the code to handle the AJAX requests in a separate file. I put it at the top of the view page simply to make it easier for Ray to see what was happening.
As for using 'var' I generally agree with Ray.
I do however disagree with Ray regarding keeping the played card inside the CFC, and keeping the CFC in the session scope. There are pros and cons to putting CFC's in a memory scope. Yes, you can get a performance gain, however you lose the ability to fail-over when in a cluster using J2EE sessions. The way I handled the cfc and played card lists is a better solution in a clustered environment, which I typically work in.
I only bring this up so people can recognize there is no one right way. Sometimes what is a best practice in one situation is less best in another!
Thanks for the feedback everyone.
Gus
steve.
good job. i entered. and i cannot wait to hear what ray has to say :) i have never been formally trained in any sort of coding, graphic design, nothing. its all self taught, and hell, i never know if what im doing is good/bad. this will be a very good experience for the entrants.
constructive criticism does a body good.
/cringing until its my turn ;)
tw
"I do however disagree with Ray regarding keeping the played card inside the CFC, and keeping the CFC in the session scope. There are pros and cons to putting CFC's in a memory scope. Yes, you can get a performance gain, however you lose the ability to fail-over when in a cluster using J2EE sessions. The way I handled the cfc and played card lists is a better solution in a clustered environment, which I typically work in."
Not only that its a freaking real PITA to 'reset' your CFC instances in the session scope in a JRun cluster.
DK
Gus,
I don't know anything about the J2EE failover thing, but I would recommend you store the playCardList in the variables scope in either case. If you need to store the playCardList by itself in the session scope, you can still do that. Just create a setPlayCardList() function. After you create your CFC with each request, do bjOBJ.setPlayCardList(session.playCardList).
Patrick
Steve wrote:
> There are pros and cons to putting CFC's
> in a memory scope. Yes, you can get a
> performance gain, however you lose the
> ability to fail-over when in a cluster
> using J2EE sessions. The way I handled
> the cfc and played card lists is a better
> solution in a clustered environment,
> which I typically work in.
I don't know anything about clustering CF servers, so pardon me if this is a dumb question, but how is keeping the played card list in the session scope different from keeping the entire CFC in the session scope?
And if you can't store CFC instances in a shared memory scope, how do you handle really complex objects? Do you have to pass all state information into the CFC as arguments (or reference external scopes, which I don't like) instead of storing state inside the CFC?
Just wondering,
Seth
Seth,
The difference is that a CFMX/JRUN/J2EE cluster can share simple session variables like strings across the cluster, but not complex objects. This is a major limitation of the CFMX architecture, and one I hope will be addressed eventually.
So in answer to your second question, yes the state of an object needs to be preserved outside of the object to guarantee failover in cluster.
Gus
Steve, you could take a two pronged approach then. Store the card list in the session scope. Store the main CFC in the app scope. That way you don't have to recreate the core game logic. Then simply make it so you pass in the card data to your calls, or shoot, even make the app cfc continue to reference the session scope for the card data.
Or simply say, if i have a session.cardlist but no session.gamecfc, recreate the cfc and init with card data. That way if the user gets pushed to another box, you still just make the CFC once.
Sounds like a reasonable approach Ray.
My main reason for bringing up the clustering issue was that since the purpose of the contest is really education I wanted to illustrate that there can be many different 'right' ways to build an app.
Gus
No darnit, it's my blog and I'm right.
Oops... did I say that out loud? -blush-
> The difference is that a CFMX/JRUN/J2EE cluster
> can share simple session variables like strings
> across the cluster, but not complex objects.
> This is a major limitation of the CFMX
> architecture, and one I hope will be addressed
> eventually.
Major limitation is right! Is that a limitation of CF, or a limitation of the underlying clustering technologies?
A decent workaround is using the Flyweight design pattern, which is what Ray recommended. Flyweight objects store logic only, they do not maintain any internal state. Any state information needed by the object must be provided as method arguments.
An extensible approach would be to create a CFC called BlackjackContext or something. This CFC would encapsulate all of the state information for the game: what cards were dealt, how much money the player has, etc. This would be persisted in the session scope and passed into all methods in your Blackjack CFC.
This provides two benefits for you: first, it decouples your game from specifically accessing the session scope, which is good. Secondly, if you extend the game and add new state information (like a player's name) you don't have to go through the code and add this variable to all of your method calls. Just wrap it into the Context object and let the game retrieve it when necessary.
Bottom line, I learned one very important thing from your contest entry: I'm very glad I don't work in a clustered environment :)