Friday, March 16, 2007

UseAbility

Light Reading
 There are some books which inspire me to write code. Reading them makes me want to run to the nearest keyboard and try out some ideas. The first book that had this affect on me was Code Complete by Steve McConnell. The second book, which I'm reading now, is Refactoring by Martin Fowler.
 One of the things that I read recently is that the automated tests for refactoring focus on Unit tests, not Functional tests. The test that I've written so far are, I think Unit tests, but I haven't focused on the distinction between the two. Perhaps that one of the reasons I've been struggling with things like CreateBasicCharacter - I may be trying to implement a Functional test instead of a Unit test.
 Not to imply that I shouldn't have any Functional tests, but perhaps the acknowledging the difference will help me approach those tests more clearly.
 Checked email. Adjusted keyboard height. Symphony Fantastique (Berlioz) is playing. I'm ready to code ... something.

Abilities
 I'd like to implement Character abilities. Each ability should have a name, a way of determining if the attempted action was successful, and a set of successful/unsuccessful effects. We'll leave the last part for later.
 A simple example ability to start with is "Read scroll." This will require a successful check against Intelligence, say the character must have an Intelligence attribute of at least 11. My first test will be to generate a character at random, and have the character attempt this action. Based on the character's intelligence, the test should be able to predict (and check for) the result.

Premature Generalization?
 Easiest thing to do would be to call objCharacter.ReadBoook(). Well, calling objCharacter.UseAbility("ReadBook") is pretty close. This function should return success or failure. Of course, just like the chrematistics, they will have a strName as well as a lngID.
 I think that the function should return an output parameter for success or failure, and return a status code for "did you encounter any errors along the way". This is a poor man's try/catch in a language that doesn't support that syntax.

Stats
 Incidentally, the server statistics show a massive amount of activity as I debug (mostly because I'm going back to test so often). This is slighly annoying (as it obscures the other data I'm trying to gather like real visits), but it's also a good sign.

Test First
 I did it. I remember to write a failing test. Of course, the failure is because I haven't implemented UseAbility yet. Actually, I'll also need "AddAbility". My first test, though should have the character fail because the ability wasn't found.
 I'm copying/pasting some code for the array handling. No smell there, though.

Exciting Failure!
 Hey! For the first time, a test other than the one I was working on failed. I got an unexpected failure that, without the help of these tests, would have usually been an undetected bug.
 I'm sold on test-driven development.
 In fact, this bug shows that I missed a test - I should have tested a character without any statistics. Now, as it happens, that's not a very likely case. But a chacter without any abilities (e.g. just starting out) is much more likely. I can fix the code for both to work smoothly.
 Now, back to implementing the "UseAbility" function. For which, first, I'll need a FindAbility function.
 Copying/Pasting again. Now it's beginning to get a little whiffy. I think that the solution may be to extract a subclass of Abilities, and delegate the addition/finding/testing to that class. The main warning sign is that ABILITY_NOT_FOUND doesn't feel right as part of the Character class. But it's not necessary just yet. Or, I'm just postponing the inevitable.
 In fact, I may want to generalize the add/find functions of the collection. Or, possibly, use a Scripting.Dictionary or similar object. Not yet, though.
 
Test Order
 I'll also need a test to ensure that two abilities with the same name can't be added. Actually, that test would pass now (even before the UseAbility one I was trying to implement). I'll back up and add the test now.

Ability as object
 Well, that didn't take long. I need to create an Ability object (just like a statistic object) in order to include the relevant Statistic that needs to be tested. So, I'll be undoing some of the changes I made to the code I copied and pasted to support objects (instead of strings).
 While I'm at it, I suppose I really do want to have a CanonicalAbilities collection, too. Perhaps these Canonical objects will no longer be necessary, once I hook up to a database.
 File level copy/paste, find/replace, and I've got a CanonicalAbilities object. Quick test - everything looks okay.
 First thing, though, is to tie this new ability class to the Character class (which is still using "disconnected" strings instead of objects).
 New music - Phish - nah, Clapton.

Ability attributes
 Ok, so each ability has a name (I feel like I'm not writing enough tests), but it also needs a StatisticID to go along with it. It does seem a little odd that a Statistic wouldn't know its own StatisticID. Hmm... I may want to do database stuff next to see if that clears up some confusion.
 
Set up/Tear Down
 This is no good. I've changed one of my tests to use a global variable, moved that test to the top of the line with comment "set-up/test", and added a "tear down" comment where I free up that global object. Phew. I'll add creating real methods to my list of things to do.
 I need a test to prove that I can't add the same statistic twice to the same character.
 Ok, while doing that test, I found a more basic problem - I can't find a statistic once I've added it. I create a more simple test to isolate just this problem. Now that this test is failing, I can investigate.

Found it
 I found the problem. VBScript Classes don't support (amoung other things) class members that are also constants. In this case the STATISTIC_NOT_FOUND was using the default 0 (which is also the position of the first object in the array) instead of -1.
 I get around this by exposing a public member, and setting the value once in the initialize statement. I should make the member private and then just expose an accessor, but I like the intellisense that the first approach provides. I can always clean it up later...
 As part of this debugging, I had to disable other tests. I should incorporate that into the program and expose it in the UI (something like a checkbox for each test or something). This would go nicely with putting all my separate test calls into a loop.

Finally ...
 I'm ready to implement the UseAbility Function.
 Yuck. That was much more difficult than it should have been. I could, if I didn't want to check for any errors, simply code it in 3 lines:
UseAbility(strAbilityName)
 Set objAbility = m_Abilities(FindAbilityByName(strAbilityName))
 Set objStatistic = m_Statistics( FindStatisticByName( FindStatisticByIndex( objAbility.StatisticID ) ) )
 blnSuccess = (objAbility.SuccessThreshold <= objStatistic.Value)

 But I'm not comfortable with that. I want to make sure that each intermediate function returns a valid answer. Perhaps I'm too defensive. Well, let's see if it works.
 Yup, I ended up adding some tests for intermediate functions, which end up producing different return codes for failure:
Const RETURN_CODE_ABILITY_NOT_FOUND = -3
Const RETURN_CODE_ABILITY_STATISTIC_NOT_FOUND = -2
Const RETURN_CODE_ABILITY_STATISTIC_NAME_NOT_FOUND = -1
 I've also added 4 tests:
"testCharacterWithoutStatsCanNotReadBook"
"testCharacterWithoutAbilitiesCanNotReadBook"
"testCharacterWithLowIntCanNotReadBook"
"testCharacterWithHighIntCanReadBook"

 Well, that took longer than expected, but I'm pretty happy with the results. I've got a long list of things to start on next time, to, including:
Create setup/teardown functions
SaveTo/LoadFrom database

See you next time.

Monday, March 12, 2007

Internal Tweaks

Some Internal Tweaks tonight.
 Update RunGenericTest to offer the option of not using On Error Resume Next
 Update RunGenericTest to allow for parameters to be passed (arrParameters array?)

You Eye
 For the first time, I'm going to add a little logic to the test page itself. First, I'll update the GenericTest function to allow for errors to be treated as fatal. Then I'll update the page to expose this option (link, I suppose). This is akin to turning off "Show Friendly Errors" in IE.
 Tonight's coding is accompanied by Tears for Fears: the hurting.
 Hmm... First time that I can recall actually trying to introduce runtime errors into my code. If I fail at producing buggy code what have I really done? (:
 Ok, I can now toggle easily between error-catching and error-ignoring. I also didn't really need to use the Execute command.

Update RunGenericTest to allow for parameters to be passed
 This will be another UI feature - allow the user to enter parameters for the tests. Am I breaking some sort of natural law here? It seems I might get carried away and no longer use the tests as they were intended. Oh well, full steam ahead.
 Of course, I'll need to spend some code to validate the inputs, which might not be code under test.
 Good, good, all is working. I should have included some more ambitious goals, but I knew I only had a short amount of time. I'll play around a bit with the output, maybe in a table format.
 Ok, it looks pretty good now. That's enough for tonight.