Friday, March 09, 2007

General Statistics 101

http://flyingsheep.com/dicetest/test.asp
Goals for today
 I'd eventually like to be able to roll a complete character and have the computer tell me which archetype those statistic would best fit (e.g. what type of character I should create, unless I choose to re-roll).

Task List
 1 Add a second statistic to my character.
 2 Generalize the statistic class.
 3 Add the remaining statistic to my character.
 4 Define Archetype minimum statistics
 5 Write and test comparison program.

Ritual
 First, I'll move my passing tests (that I don't plan on modifying today) into the successful tests page. I'll eventually need multiple categorized pages of successful tests, but not today.

1 Second Statistic
 That was easy. A little copy/paste in the validation function, but I'll clean it up later (Copy/Paste once is acceptable, I think. Copy/Paste seven times indicates the time to generalize).

2 Generalize Statistic.
 I'm a bit worried about my dependence on Intellisense, and stuff I've been reading suggests that it doesn't work with multiple nested classes. Still, I think that multiple nested classes is, in this case, the way to go. A Character should contain multiple Statistics.
 The reason for generalizing at this point is in anticipation of multiple different objects (Characters, Archetypes, Abilities) using the same Statistic objects.
 I'm a bit confused about how to proceed. Then I realize I don't have a failing test.  Ah, I write a test to walk through the statistics collection (which doesn't exist yet), and it fails.
 For the first, I need to disable error ignoring in RunGenericTest, because I need the additional diagnostic information. I re-enable it afterwards. I'm also reduced to throwing in some "Response.Write"s to help debug, although that smells a bit.
 Ok, So Strength is using the new Statistic class (which I simplified as a public Name/Value pair, although I expect it to get more complex soon). I'll finish converting Agility, and clean up unused references to the hardcoded names in the class.
 Getting "Object doesn't support this property or method " error. Need to beef up my objCharacter.DebugPrint function.
 Disabled Error Ignoring again. I may want to add that as a switch (now that I've find my new friend Execute). Something like blnTreatErrorsAsFatal.

3 Add the remaining statistic to my character.
 Found this http://www.thehaws.org/add_quiz.shtml cute little quiz to determine your real life Dungeons and Dragons Stats. The author was nice enough to leave in some default values for impatient people like me.
 I'm slightly annoyed that Strength has a different format (e.g. 3d6 + d100), but I'm going to ignore that. Game Designer's prerogative. Also turns out that there's no statistic for Agility. Hmm...
 Ah, the powers of generalization. Just adding one line per statistic should handle all the programming and testing. Six lines of code later, and my character is feeling much more multi-dimensional. Oh, I suppose I should test it.

This character is named Gunthar and is a Barbarian.
This character has 6 statistics.
Gunthar has a Strength of 11.
Gunthar has a Intelligence of 12.
Gunthar has a Wisdom of 6.
Gunthar has a Dexterity of  9.
Gunthar has a Constitution of 14.
Gunthar has a Charisma of 11.

 Sweet.

4 Define Archetype minimum statistics
 Some simple rules: Fighters - minimum Strength 9; Mages - minimum 9 Intelligence; Clerics - minimum 9 Wisdom; Thieves - min 9 Dexterity.
 Now I'm at a bit of a quandary. I don't want to compare objCharacter.Statistic("name").value to objArchetype.MinimumRequiredStatistic("name").value. I don't like doing comparisons by "name" - the danger of trying to compare "Strength" and "Strength" is too great (with my less than stellar spelling ability). I want canonical Statistics, with IDs and Named constants like STATISTIC_STRENGTH = 1. I want a global object that defines which statistics exist and exposes an enumerated list.
 However, I also want to be able to add new statistics later. I guess I want a FindStatisticByName("Strength") so that I can use them both as string aliases and constants.  Let's start down that path.
 CStatistic is sort of an instance of a statistic. This will also give me the first stuff to put into my StartUp pre-test function.
 Oops. Wrote a bunch of code without a failing test. I've got to get into the right habits. Two syntax errors. I'm coding too much between tests.
 OK, new plan for today. I'll write up some tests for this new Canonical Statistics object. I'm still not sure if it should be global, or should be passed around.
 Adding a DebugPrint function to this new object.
 My previous tests have been more like commands - CreateBasicCharacter. I exercise the functionality, but don't check for results (except for the errors being thrown). For this new class, I'm going to be specific (as there are specific things I want to check, such as disallowing the addition of duplicates). Nice - I can use the DebugPrint function to ensure that the object is the same after attempting to add a duplicate.
 New Assert Function - AssertNumbersMatch(). Funny how I needed AssertNumberInRange first. Works. I like how the code and the tests have a symbiotic relationship - each validates the other.
 Time to clean up, set up tasks for next time, and log off, with all tests passing.

Tuesday, March 06, 2007

Dice

You can follow my progress at  http://flyingsheep.com/dicetest/test.asp
 
Back to it
 After a brief enjoyable break to play some Guitar Hero II and BattleLore, I'm back. My next goal is to create a Character class, add an attribute, and set the attribute by rolling 3d6 (sound familiar?).

Delay
 Finding my RL friends on Live Journal. I'm probably a few years behind the curve here, but there's a tangled web, and it's difficult to resist the urge to tease out a few strands and bookmark them. RSS Feeds are the next step, but for today, bookmarks will have to do.
 Also spent some time working on the intellisense problem with ASP classes. No progress.

Gunthar the Barbarian
 Created a character. Tested to make sure name matches Gunthar and archetype (class) matches Barbarian. Is it okay to pass parameters to tests? I'm thinking I might have to include an array of parameters in the test function definition (strTestResult, blnTestPassed, [arrParameters?]). I'm also thinking that I should move my successful tests out of the main file that I'm working in (just to tidy up a bit).
 The freedom to move things around and not worry about breaking something (and, worse, not realizing its been broken until much later) is exhilarating!

Generic Functions
 I need a generic function AssertNumberInRange(lngNumber, lngRangeMin, lngRangeMax, strResult). I'll write one and then refactor some of the tests.

Debugging
 For the first time so far, it took more than a minute for me to find the error. I fell back into debugging. It's a frustrating sign that I'm trying to move too fast. I've got to get back to baby steps.
 I was trying to test both the new Assert..Range function and the Character stat rolling function. I got greedy.
 Used the Assert Range function to find a bug in the stat rolling function. Investigating. Ah ha! ObjDie doesn't return the value. It simply sets objDie.Value. Hmm... Maybe I want to change that. If I can't remember how it worked this morning, that's not intuitive design.

So, Tell Me About Yourself
 I like my classes to have a .DebugPrint() method that returns an HTML formatted string with various information (state, variables, etc.) about itself. This helps debugging, of course. It end up being a holdover from my pre-TDD days (e.g. yesterday), but I'm going to give it a try.

Test too complicated?
 I've got a single test called CreateBasicCharacter. It's probably not descriptive enough. It does a lot of things, too - checks the name, the archetype, the strength, etc.

Adding function CDie.RollMultipleDice(lngNumberOfDice)
 Oops - I got a stack error. I guess if RollMultipleDice calls Roll(), I can't simplify Roll() by calling RollMultipleDice(1). Ha! Adding some tests for Multiple Dice Rolling.

Ok Everything working
5 Tests running. Progress is being made. Time for dinner.

Sunday, March 04, 2007

First Steps: Dice

Warning
 I should warn you that this may be a little frustrating to read, especially if you already use TDD (Test Driven Development). It's akin to watching someone fumble around in a computer game when you know what the solution is. You may find yourself thinking, "Press the Red button twice, then the green button! It's so obvious!", or groaning when I start down an obvious dead end. My apologies in advance, and I am open to constructive criticism.
 One of my frustrations as a mathematician was knowing that if there were two possible paths to pursue, I invariably choose the dead end path first. (:

Starting Here, Starting Now
 Ok, so, some infrastructure work first. Setting up a project, connecting to the website, installing a visual FTP app, etc. Done, hello world page uploaded. Excellent.

Baby Steps
 According to TDD, I need to start with a failing test. This can include a test that fails because an object can't be created. I've created a first function called TestAll, that will eventually contain all the tests.

First Test - empty test (always passed)
 This is a test Test. Whoo hoo! It passed (: I do a little refactoring (already!), deciding that test functions should take two output parameters (strTestResult, blnTestPassed).
 (Yes, I still use Hungarian notation. I find it useful, although I probably simply haven't heard the right argument against it yet. I expect I'll give it up eventually.)

Error checking
 A decision must be made - do I disable fatal error checking (e.g. use On Error Resume Next) and catch errors and report them as failing tests? If so, does the test itself need to do this, or should the master function (TestAll) do this before and after each test?
 Without thinking too much about it, I'll go with making the TestAll function disable (and reenable) error checking, assuming that it doesn't reset when crossing function boundaries. I'll test that, obviously.

Execute (not Copy.Paste)
 There's a chunk of code in the TestAll function that clears errors, calls the function, checks for thrown errors, checks the results of the function, adds the results to the list, and cleans up. I'm a bit tempted to copy/paste this block of 14 lines, and just rename the function to be tested each time. Execute command to the rescue! I can create a generic function for this 14 lines, pass the name of the function in, and use Execute to run it. Cool beans, although I'm a bit wary that any use of Execute is a too-clever solution. Time will tell.

Refactoring again
 My first attempt at creating a RunGenericTest(strTestName, strAllTestResults) function doesn't work. Simple error (neglected to change a variable name), which reminds me that I need to specify Option Explicit to throw these errors.
 Compilation Errors still are fatal, as they should be. And embarrassing (as they should be).
 I'm already enjoying my RunGenericTest function, as I've decided to reformat the results a bit to number the tests. Had I chosen the copy/paste route, that's at least two places that I would have had to change (and would have increase the barrier to making that change). I now have two test - one empty passing test, and one failing test that throws a VB Runtime error (undeclared variable). Excellent. I don't need a failing VBScriptRuntimeError test, so I'll get rid of it.

Implementing RollDieGetValueBetween1And6
 I'm currently torn between keeping the class files separate (e.g. CDie.asp, CCharacter.asp, CMap.asp, etc.) and the convenience of intellisense (which only works on classes defined in that file. I'd like ideally to have the header information (function stubs) inline in my test file, and the implementation details separate.
 Alternatively, I could define the tests in the class files (where intellisense will work). Perhaps a class.TestAll() function? I'll go down that route. But how will the class file have access to the RunGenericTest function? I'm sure there's an answer, but not sure what it is just yet.
 For now, I can kludge together something like the following - copy/paste the contents of the CDie class into the testing page for convenience of working on them. Once I'm ready to move onto a different class, I'll "check in" the current inline version, and include the separate file. Not clean, but I think it'll work.
 I will probably eventually group functions by class, but I'm not sure yet if I will include them in the class.

Tests for random events
 It's a little difficult to write a test where the expected result is in a range. For example, I have two tests which simulate rolling a 6 and 10-sided die, respectively. In each case, I'm testing for results between 1 and 6 (or 10). But if I screw up and the 10-sided die is really only a 6 sided die, I'll never catch that bug.
 One answer would be to make the test check that each number in the range appears more or less the same amount of time. But even this test will occasionally fail (for example, if all 10 rolls are a "3", just by pure chance. Is it acceptable to have tests that pass 90% of the time? (: