Posted May 16, 2007 at
9:19 AM
Tags:
ColdFusion,
HTML / CSS,
Javascript / DHTML,
AJAX
Now that Ray Camden's Beginner ColdFusion Content has ended, I can discuss my unofficial entry. Seeing as I am not a beginner, I could not technically enter the contest, but I thought it would be a cool little project to have a go at, especially since I am not tremendously proficient with CFCs and object oriented programming (OOP).
Before I get into the details, I figured you might want to try the application. It is called My Shortie and can be launched here:
Launch My Shortie
Here is a screen shot of the application (don't worry, there is nothing Saucy about this application):
The labotomize button resets the Shortie's data. Also I am sure that is misspelling that word (labotomize) but I was too deep into the project to care by the time I found out :)
This little project proved much harder than I thought it was going to be. My first real hurdle was, How the heck do I interact with this entity (the Shortie) and how do I know how to affect the environmental properties in a way that is open to change? My first break through was the idea of an "Interaction".
An Interaction was a base component that defined what an interaction event with the Shortie would do; how does it affect the Shortie's energy? How long does it take to complete this interaction? How does it affect feelings of anger or love? Here is the base ColdFusion component that defines what an Interaction must define:
Launch code in new window » Download code as text file »
As you can see, each interaction MUST have the following properties that are Get()able:
- Anger
- Energy
- Happiness
- Hunger
- IsPositive
- Love
- TimeRequired
I decided that every interaction with the Shortie could be defined as some combination of these properties. Most of the properties (anger, energy, happiness, hunger, love) are simply numeric values that either add or subtract from the Shortie's properties. For instance, telling the Shortie, "I love you," should have a positive "Love" property since you tend to feel more loved when someone says they love you.
All interactions defined for the application must extend this base interaction. So for example, here is concrete ColdFusion component that defines the interaction for taking the Shortie to dinner (TakeToDinner.cfc):
Launch code in new window » Download code as text file »
- <cfcomponent
- extends="Interaction"
- output="false"
- hint="Take someone out to dinner.">
- <cffunction
- name="Init"
- access="public"
- returntype="any"
- output="false"
- hint="Returns an initialized interaction.">
- <cfset SUPER.Init() />
- <cfset VARIABLES.Instance.Name = "Take To Dinner" />
- <cfset VARIABLES.Instance.Key = "take_to_dinner" />
- <cfset VARIABLES.Instance.TimeRequired = CreateTimeSpan(
- 0,
- 3,
- 30,
- 0
- ) />
- <cfset VARIABLES.Instance.Love = 1 />
- <cfset VARIABLES.Instance.Anger = -1 />
- <cfset VARIABLES.Instance.Energy = 3 />
- <cfset VARIABLES.Instance.Happiness = 1 />
- <cfset VARIABLES.Instance.Hunger = -10 />
- <cfset VARIABLES.Instance.IsPositive = true />
- <cfreturn THIS />
- </cffunction>
- </cfcomponent>
As you can see, these concrete implementations of the Interaction base class just override the constructor (Init) method. All the Getter methods are defined by the base ColdFusion component. The only thing that is special about the concrete Interactions is that they have a special combination of the properties listed above. So, looking at the TakeToDinner.cfc, you can see that eating results in a -10 change in the Shortie's hunger (potentially) and requires 3.5 hours of time.
The idea behind the TimeRequired is that you can't simply fire interaction after interaction. Interactions with anyone take time. Sure, some interactions don't take very long, but for our simple application, we are going to assume that no interaction takes any less than one hour.
Once I started to flesh out more of the concrete interaction ColdFusion components, I felt really good about where this path was leading. This combination of properties, while simple, does feel quite natural. The next big hurdle was data persistence. The application has to be really simple and portable, so that means no database. But, since we cannot depend on purely APPLICATION-scoped values as this application must survive over time, the next best solution is data persistence via XML files.
But what are we persisting? Not just any old data - we are persisting how the Shortie feels at a given time. To me, this sounds like a Brain. So, I created a Brain.cfc ColdFusion component that has generic Get() and Set() methods. These Get() and Set() methods can take any kind of serializeable data (strings, numbers, dates, arrays, structs). These data points then get serialized and stored to an XML file via ColdFusion's WDDX functionality:
Launch code in new window » Download code as text file »
I decided to build the Brain.cfc as a separate component from the Shortie.cfc (up next) because I really wanted to stress the separation of concerns. This Shortie itself should not really have to know how to store data over time. And think about it in the real world; do we know how data gets stored? Sort of? Not really? We just send nerve impulses to our brain and our brain just kind of stores and retrieves it via some sort of crazy awesome black magic.
By building the Brain.cfc separately, it allowed me to unit test the brain functionality before I even started working on the Shortie.cfc. Also, by breaking the system down into smaller parts, it made it easier for me to think about.
Now that we have our interactions defined and our model for data persistence, it's time to actually build something to interact with. The Shortie.cfc ColdFusion component is our representation of the "Monster" (the original contest idea was Monster Maker). The Shortie.cfc was, by far, the most complicated of the involved components. Not so much because the code was crazy - more so because people are irrational and crazy and random and how the hell to you model that EVEN if you have good Interaction definitions?
The Shortie's Interact() method takes an instance of the Interaction.cfc ColdFusion component. It then grabs the properties out of that instance and updates its own internal state. To try to model the real world, this method if chock full of "randomness". Every now and then, it will totally flip the properties so that something that should have given you energy make you tired and something that should have made you feel loved will fill you with anger.
The other difficult thing about this component was that I didn't want it to be in real time. Meaning, like dog years, I wanted the Shortie's time to be smaller but proportional to real world time. For this application, one day of real world time is the equivalent of 120 days of Shortie time (one Shortie hour = 30 seconds real world time).
Aside from the time scale itself, incrementing time was hard to figure out. When you increment time, you are doing more than just changing a single variable; time wears on people - it makes them hungry, it sucks energy. And, if interactions themselves take time, then incrementing the Shortie time is going to be coupled with possibility of interactions.
Here is the Shortie.cfc code:
Launch code in new window » Download code as text file »
- <cfcomponent
- output="false"
- hint="This is your shortie; treat her well.">
- <cfset VARIABLES.Instance = StructNew() />
- <cfset VARIABLES.Instance.Brain = "" />
- <cfset VARIABLES.Instance.Anger = 0 />
- <cfset VARIABLES.Instance.Energy = 0 />
- <cfset VARIABLES.Instance.Hunger = 0 />
- <cfset VARIABLES.Instance.Happiness = 0 />
- <cfset VARIABLES.Instance.Love = 0 />
- <cfset VARIABLES.Instance.Time = Now() />
- <cfset VARIABLES.Instance.RealTime = Now() />
- <cfset VARIABLES.Instance.DayScale = 120 />
- <cfset VARIABLES.Instance.NextInteractionTime = Now() />
- <cffunction
- name="Init"
- access="public"
- returntype="any"
- output="false"
- hint="Returns an initialized shortie instance.">
- <cfargument
- name="Brain"
- type="any"
- required="true"
- />
- <cfset VARIABLES.Instance.Brain = ARGUMENTS.Brain />
- <cfset VARIABLES.LoadData() />
- <cfset THIS.AdjustTime() />
- <cfreturn THIS />
- </cffunction>
- <cffunction
- name="AdjustTime"
- access="public"
- returntype="void"
- output="false"
- hint="This adjusts the shortie time to make sure it scales to regular world time increments.">
- <cfset var LOCAL = StructNew() />
- <cfset LOCAL.Now = Now() />
- <cfset LOCAL.DayDiff = (
- (LOCAL.Now - VARIABLES.Instance.RealTime) *
- VARIABLES.Instance.DayScale
- ) />
- <cfset LOCAL.TargetTime = (
- VARIABLES.Instance.Time +
- LOCAL.DayDiff
- ) />
- <cfset VARIABLES.IncrementTime(
- (LOCAL.TargetTime - VARIABLES.Instance.Time)
- ) />
- <cfset VARIABLES.Instance.RealTime = LOCAL.Now />
- <cfreturn />
- </cffunction>
- <cffunction
- name="CommitData"
- access="private"
- returntype="void"
- output="false"
- hint="This commits short term memory to the brain.">
- <cfset VARIABLES.Instance.Brain.Set(
- Property = "Anger",
- Value = VARIABLES.Instance.Anger,
- CommitData = false
- ) />
- <cfset VARIABLES.Instance.Brain.Set(
- Property = "Energy",
- Value = VARIABLES.Instance.Energy,
- CommitData = false
- ) />
- <cfset VARIABLES.Instance.Brain.Set(
- Property = "Hunger",
- Value = VARIABLES.Instance.Hunger,
- CommitData = false
- ) />
- <cfset VARIABLES.Instance.Brain.Set(
- Property = "Happiness",
- Value = VARIABLES.Instance.Happiness,
- CommitData = false
- ) />
- <cfset VARIABLES.Instance.Brain.Set(
- Property = "Love",
- Value = VARIABLES.Instance.Love
- ) />
- <cfset VARIABLES.Instance.Brain.Set(
- Property = "Time",
- Value = VARIABLES.Instance.Time
- ) />
- <cfreturn />
- </cffunction>
- <cffunction
- name="GetNextInteractionTime"
- access="public"
- returntype="numeric"
- output="false"
- hint="Gets the time at which the Shortie can next be interacted with.">
- <cfreturn VARIABLES.Instance.NextInteractionTime />
- </cffunction>
- <cffunction
- name="GetProperties"
- access="public"
- returntype="struct"
- output="false"
- hint="Returns the 'mental' properties of the shortie.">
- <cfset var LOCAL = StructNew() />
- <cfset LOCAL.Anger = VARIABLES.Instance.Anger />
- <cfset LOCAL.Energy = VARIABLES.Instance.Energy />
- <cfset LOCAL.Hunger = VARIABLES.Instance.Hunger />
- <cfset LOCAL.Happiness = VARIABLES.Instance.Happiness />
- <cfset LOCAL.Love = VARIABLES.Instance.Love />
- <cfreturn LOCAL />
- </cffunction>
- <cffunction
- name="GetTime"
- access="public"
- returntype="string"
- output="false"
- hint="Returns the shortie time (internal time model).">
- <cfreturn (
- DateFormat(
- VARIABLES.Instance.Time,
- "mm/dd/yyyy "
- ) &
- TimeFormat(
- VARIABLES.Instance.Time,
- "hh:mm TT"
- )
- ) />
- </cffunction>
- <cffunction
- name="IncrementTime"
- access="private"
- returntype="void"
- output="false"
- hint="This increments time.">
- <cfargument
- name="TimeSpan"
- type="numeric"
- required="false"
- default="#CreateTimeSpan( 0, 1, 0, 0 )#"
- hint="The time to increase for this iteration (defaults to an hour)."
- />
- <cfset VARIABLES.Instance.Time = (
- VARIABLES.Instance.Time +
- ARGUMENTS.TimeSpan
- ) />
- <cfset VARIABLES.Instance.Energy = (
- VARIABLES.Instance.Energy -
- (
- 1 *
- ARGUMENTS.TimeSpan / CreateTimeSpan( 0, 4, 0, 0 )
- )
- ) />
- <cfset VARIABLES.Instance.Hunger = (
- VARIABLES.Instance.Hunger +
- (
- 1 *
- ARGUMENTS.TimeSpan / CreateTimeSpan( 0, 4, 0, 0 )
- )
- ) />
- <cfset VARIABLES.CommitData() />
- <cfreturn />
- </cffunction>
- <cffunction
- name="Interact"
- access="public"
- returntype="void"
- output="false"
- hint="This is how you can pass in an interaction. This will update the internal state.">
- <cfargument
- name="Interaction"
- type="any"
- required="true"
- hint="This is an interaction that must implement the interaction interface."
- />
- <cfset var LOCAL = StructNew() />
- <cfset THIS.AdjustTime() />
- <cfif (VARIABLES.Instance.NextInteractionTime GT VARIABLES.Instance.Time)>
- <cfreturn />
- </cfif>
- <cfset LOCAL.TimeRequired = ARGUMENTS.Interaction.GetTimeRequired() />
- <cfset VARIABLES.Instance.NextInteractionTime = (
- VARIABLES.Instance.Time +
- LOCAL.TimeRequired
- ) />
- <cfif (RandRange( 1, 30 ) EQ 15)>
- <cfset LOCAL.Multiplier = -1 />
- <cfelse>
- <cfset LOCAL.Multiplier = 1 />
- </cfif>
- <cfset LOCAL.Delta = StructNew() />
- <cfset LOCAL.Delta.Anger = (
- (
- ARGUMENTS.Interaction.GetAnger() *
- LOCAL.Multiplier
- ) +
- RandRange( -1, 1 )
- ) />
- <cfset LOCAL.Delta.Energy = (
- (
- ARGUMENTS.Interaction.GetEnergy() *
- LOCAL.Multiplier
- ) +
- RandRange( -1, 1 )
- ) />
- <cfset LOCAL.Delta.Hunger = (
- (
- ARGUMENTS.Interaction.GetHunger() *
- LOCAL.Multiplier
- ) +
- RandRange( -1, 1 )
- ) />
- <cfset LOCAL.Delta.Happiness = (
- (
- ARGUMENTS.Interaction.GetHappiness() *
- LOCAL.Multiplier
- ) +
- RandRange( -1, 1 )
- ) />
- <cfset LOCAL.Delta.Love = (
- (
- ARGUMENTS.Interaction.GetLove() *
- LOCAL.Multiplier
- ) +
- RandRange( -1, 1 )
- ) />
- <cfset LOCAL.Delta.IsPositive = ARGUMENTS.Interaction.GetIsPositive() />
- <cfset VARIABLES.Instance.Anger = (
- VARIABLES.Instance.Anger +
- LOCAL.Delta.Anger
- ) />
- <cfset VARIABLES.Instance.Energy = (
- VARIABLES.Instance.Energy +
- LOCAL.Delta.Energy
- ) />
- <cfset VARIABLES.Instance.Hunger = (
- VARIABLES.Instance.Hunger +
- LOCAL.Delta.Hunger
- ) />
- <cfset VARIABLES.Instance.Happiness = (
- VARIABLES.Instance.Happiness +
- LOCAL.Delta.Happiness
- ) />
- <cfset VARIABLES.Instance.Love = (
- VARIABLES.Instance.Love +
- LOCAL.Delta.Love
- ) />
- <cfset VARIABLES.CommitData() />
- <cfreturn />
- </cffunction>
- <cffunction
- name="LoadData"
- access="private"
- returntype="void"
- output="false"
- hint="Loads the property data from the brain (persisted data).">
- <cfset VARIABLES.Instance.Anger = Val(
- VARIABLES.Instance.Brain.Get( "Anger" )
- ) />
- <cfset VARIABLES.Instance.Energy = Val(
- VARIABLES.Instance.Brain.Get( "Energy" )
- ) />
- <cfset VARIABLES.Instance.Hunger = Val(
- VARIABLES.Instance.Brain.Get( "Hunger" )
- ) />
- <cfset VARIABLES.Instance.Happiness = Val(
- VARIABLES.Instance.Brain.Get( "Happiness" )
- ) />
- <cfset VARIABLES.Instance.Love = Val(
- VARIABLES.Instance.Brain.Get( "Love" )
- ) />
- <cfset VARIABLES.Instance.Time = Val(
- VARIABLES.Instance.Brain.Get( "Time" )
- ) />
- <cfif NOT VARIABLES.Instance.Time>
- <cfset VARIABLES.Instance.Time = VARIABLES.Instance.RealTime />
- </cfif>
- <cfreturn />
- </cffunction>
- <cffunction
- name="Labotomize"
- access="public"
- returntype="void"
- output="false"
- hint="Clears the brain data, thereby, resenting the properties.">
- <cfset VARIABLES.Instance.Brain.ClearData() />
- <cfset VARIABLES.LoadData() />
- <cfset VARIABLES.Instance.NextInteractionTime = Now() />
- <cfreturn />
- </cffunction>
- </cfcomponent>
Notice that the Shortie takes an instance of the Brain.cfc in its Init method. I think this is a good application of composing one object inside another. But again, I am learning this stuff as I go.
The one final hurdle to conquer was not letting interaction after interaction being fired. We know that interactions take time (one of their properties). Using this, the Shortie keeps track of a "Next Interaction Time". Once an interaction gets sent to the Shortie, the time required for the interaction is added to the Shortie's current time. Then, if any more interactions get passed in (Interact() method) before the Shortie's time is greater than the next interaction time, they are simply ignored.
The one final piece of the back end is the Application.cfc which ties it all together. The Application.cfc ColdFusion component here is very simple. All it does really is create an instance of the Brain.cfc and injects it into the instance of the Shortie.cfc and then caches it in the APPLICATION scope.
Launch code in new window » Download code as text file »
- <cfcomponent
- output="true"
- hint="Handles application definition and application level events.">
- <cfset THIS.ApplicationName = "KinkySolutions - My Shortie" />
- <cfset THIS.ApplicationTimeOut = CreateTimeSpan(
- 2,
- 0,
- 0,
- 0
- ) />
- <cfset THIS.SessionManagement = false />
- <cfset THIS.SetClientCookies = false />
- <cfsetting
- requesttimeout="20"
- showdebugoutput="false"
- />
- <cffunction
- name="OnApplicationStart"
- access="public"
- returntype="boolean"
- output="false"
- hint="Runs when application starts or is manually reset.">
- <cflock
- scope="APPLICATION"
- type="EXCLUSIVE"
- timeout="10">
- <cfset StructClear( APPLICATION ) />
- <cfset APPLICATION.UDFLib = CreateObject(
- "component",
- "UDFLib"
- ).Init()
- />