C# Text Adventure: Lesson 26

An Item class, and revising Hero

If you compare GameObjs.cs as it existed at the end of the previous lesson (#25) with its state now (see downloadable source code at end of this post), you'll find numerous differences.

There is the addition of an Item class, and the Hero class has been significantly expended, including the addition of an inventory, a place for the hero to keep his stuff.

I tweaked structure CombatResults to include a member to hold the highest damage the combatant dished out in its most recent combat. And I revised MobileKiller.CombatRunner() to calculate this value for each of two combatants and store it in their member of type CombatResults.

To understand how Hero is developing, you'll not only want to view the updated GameObjs.cs source code, but also the testGameObjs11.cs code to see how it sets up some Item objects and adds them to the hero's inventory.

You can download the source files for this lesson by clicking here.

Return to Lesson 25 |

Return to Index

C# Text Adventure: Lesson 25

Deriving the Hero class

In this lesson, I've derived class Hero from Moba. I've overloaded MobileKiller's CombatRunner() method, I've added an additional field to structure CombatResults, and I've updated MobileKiller so that each combatant's best (highest) Attack Roll result from the combat gets stored in the combatant's CombatResults member.

So that the hero can grow more powerful over time, I've added three integer members: mana, prowess, and vigor. I've overridden the Hero class' Attack_Roll() and Defense_Roll() methods, so that instead of an upper bound of 20 applying to the roll generated, the upper bound is instead (20+Prowess).

The test program, testgo9.exe, demonstrates invocation of a method — GetAttackRolls() — I added to Moba that allows code to pass an integer parameter and that will then return an integer array with parameter elements, each containing a value generated by the instance's Attack_Roll() method.

You can download the source files for this lesson by clicking here.

Return to Lesson 24 | Proceed to Lesson 26

Return to Index

C# Text Adventure: Lesson 24

Adding class Room to GameObjs.cs

For this lesson, I've updated GameObjs.cs with a Room class whose members are all private and readonly. We only set them via the constructor. We do, however, allow their values to be retrieved by a getter.

A Room consists of a Title and Description of type string, an ID of type Int, and six members are of type Exit, a structure that holds exit information. I provide two Exit constructors. The second one takes a bool value and creates an Exit instance whose LeadsTo property is set to -1 and whose IsVisible property is set to false. We use that constructor to construct an instance of what I call nilExit in testGameObjs7.cs. The nilExit can then be stored in any of the room's six Exit properties for which no exit should exist (see testGameObjs7.cs and/or compile and run testgo7.exe to see this).

The first Exit constructor takes nine parameters that are used to instantiate a Room object.

If you peruse the code, you'll find that the Exit structure contains information on the type of exit (or portal) we're dealing with (regular doorway, bulkhead on a ship, hole in the ground), the ID of the room to which the Exit links, a byte value coding any effects extant upon the room, and an IsVisible property (normally set to true, but might be false due to magic, blindness, etc.)

I've added a couple of methods to ConsoleInputOutput.cs that have been useful to me in the past and for which I can foresee a use in this game:

Listing

    public static int StringContainsThisManyNonSpaceChars(string s)
    {
        int cnt = 0;

        foreach (char c in s)
        {
            if (c != ' ')
            {
                cnt++;
            }
        }
        return cnt;
    }

    public static bool IsNonAlphaNumChar(char c)
    {
        if (Char.IsDigit(c)) { return false; }
        if (Char.IsLetter(c)) { return false; }
        return true;
    }

I recommend you download the source code files using the link below. Study the code and compile and run the test program.

You can download the source files for this lesson by clicking here.

Return to Lesson 23 | Proceed to Lesson 25

Return to Index

C# Text Adventure: Lesson 23

Zooming out a little

So far, we've been developing Heroes in a rather MUD (Multi-User Dungeon)-like vein. However, this doesn't mean we won't use a considerable amount of procedural coding when we begin implementing quests, etc. Not all combat will be random encounters with mobiles.

Nevertheless, a significant portion of the game code is, and will be, object-oriented. Since we've been spending so much time on developing class Moba (and most recently, on class MobileKiller), let's zoom out a little and talk about the broader game implementation.

We're going to define areas — zones containing a set number of rooms. And we're going to derive a sealed class from Moba and name it Hero. The hero instance will have a CurrentRoom property that holds a Room instance and informs Seesh Arp what he sees, whether or not there is an exit to the south, etc.

And that room property might have members that can either be set to null or to mobiles (instances of Moba). Funny, huh, how the hero instance can contain the very mobile with which Seesh Arp interacts?

We'll do quite a bit of development of Hero, because it represents the player-of-the-game's alter-ego, the in-game protagonist, Seesh Arp. Our instance of this special class will be instantiated in Main() of heroes.cs and will last throughout the entirety of the program's execution (except in the Main Menu, of course, which is the last watering hole for the user before casting off into the gameworld).

You may find it very helpful to download either or both of the freeware programs ExamDiff and TextCompare, and use them at the end of each of these lessons to get a better idea what exactly has changed in the various source code files. For example, it would be instrumental, for this lesson, to use one of these tools to visually see the differences between the GameObjs.cs source code file at the end of the previous lesson with the same source code file at the end of this lesson.

For example, TextCompare, when you use the "New and Changed Lines" option for comparison, shows exactly the changes I made to GameObjs.cs for this lesson, and even gives the line numbers. But ExamDiff goes one better. It lets me copy/paste those lines. In the following listing, rather than show the totality of GameObjs.cs, I show only what has been added to GameObjs.cs during this lesson.:

Additions to GameObjs.cs for this lesson:

    private CombatResults combat_results;

    public virtual string ReportCombatResults()
    {
        string rep = "\n\n " + this.A_Descriptor + " reports:\n";
        rep += "\tTotal successful attacks: ";
        rep += this.MobileCombatResults.CombHits.ToString() + " over " + this.MobileCombatResults.Rounds.ToString() + " rounds.\n";
        rep += "\tTotal damage inflicted on foe(s): " + this.MobileCombatResults.CombTotalDmg.ToString() + "\n";
        rep += "\tAverage damage inflicted: " + this.MobileCombatResults.CombAverageDmg.ToString() + "\n";
        rep += "\tHit Percentage: " + (100 * this.MobileCombatResults.CombHitPercentage).ToString() + "%\n";

        return rep;
    }

    public CombatResults MobileCombatResults
    {
        get { return combat_results; }
        set { combat_results = value; }
    }

public struct CombatResults
{
    public int rounds;

    public int comb_hits;
    public int comb_attacks;
    public int comb_totaldmg;
    public float comb_avedmg;
    public float comb_hitpercentage;

    public CombatResults(int _rounds, int _tot_dmg, int _hits)
    {
        rounds = _rounds;
        comb_attacks = rounds;
        comb_hits = _hits;
        comb_totaldmg = _tot_dmg;
        comb_avedmg = (float)comb_totaldmg / (float)rounds;
        comb_hitpercentage = (float)comb_hits / (float)rounds;
    }
    public int CombHits
    {
        get { return comb_hits; }
    }

    public int CombTotalDmg
    {
        get { return comb_totaldmg; }
    }

    public float CombAverageDmg
    {
        get { return comb_avedmg; }
    }

    public float CombHitPercentage
    {
        get { return comb_hitpercentage; }
    }

    public int Rounds
    {
        get { return rounds; }
    }
}

        int rounds = 0; //# times through while loop during combat
        int comb1hits = 0; //# times that combatant1 successfully hits combatant2
        int comb2hits = 0; //# times that combatant2 successfully hits combatant1

        enough code to show proof-of-concept
        */
            rounds++;

                comb1hits++;
                comb2hits++;
        CombatResults cr = new CombatResults(rounds, combatant1TotalDmg, comb1hits);
        combatant1.MobileCombatResults = cr;
        cr = new CombatResults(rounds, combatant2TotalDmg, comb2hits);
        combatant2.MobileCombatResults = cr;

In a nutshell, we now have a CombatRunner() that not only determines the victor when two mobiles fight, but also tracks the total damage inflicted by each mobile, how many attacks each mobile landed on its foes, the hit percentage of each mobile, and the average damage each mobile inflicted on his foe over the courses of the combat.

All of that, and we then store this data in both the victor and defeated mobile, as a member variable of type CombatResults. That member's name is MobileCombatResults. We even added a ReportCombatResults() method to our Moba class, as demonstrated in our testgo6.exe test application.

Here is testgo6.exe in action:

You can download the source files for this lesson by clicking here.

Return to Lesson 22 | Proceed to Lesson 24

Return to Index

C# Text Adventure: Lesson 22

Approximating a functional CombatRunner()

The text of this lesson will be brief. Basically, I made some tweaks to Moba and then expanded the CombatRunner() method into a first approximation of a functional combat runner. The testgo5.exe that can be built from this lesson's progress demonstrates this new functionality. If you want to build the testgo5.exe test application yourself, you can download the build folder here.

Here's a screen shot of the test app in action:

By the way, you can specify an integer between 250 and 10,000 as a command-line argument when running testgo5.exe in the command prompt, and the value you specify will translate into the number of milliseconds of delay between each 'step' in the CombatRunner()'s processing of the combat.

For example:

testgo5 2000

at the command line, for a 2-second delay.

You can download the source files for this lesson by clicking here.

Return to Lesson 21 | Proceed to Lesson 23

Return to Index

C# Text Adventure: Lesson 20

Towards the Development of a Combat Runner class

Okay, the first thing we need to do in this lesson is tweak the code in the setter for MaxHitPoints in class Moba:

    public int MaxHitPoints
    {
        get { return ((5 * Constitution) + Level); }
        set { maxhitpoints = value; }
    }

Now go up to each constructor in class Moba (both the default and the overloaded one) and add these two lines of code:

        MaxHitPoints = (5 * Constitution) + Level;
        HitPoints = MaxHitPoints;

Now let's add a couple of new methods to class Moba:

    public int RecalculateMaxHitPoints()
    {
        int result = ((5 * Constitution) + Level);
        MaxHitPoints = result;
        return result;
    }

    public string SetDescriptor(string _new_desc)
    {
        A_Descriptor = _new_desc;
        return _new_desc;
    }

We'll use these two methods in our Menu Item #4 example in testGameObjs3.cs. Switch to that source code file now, and add method exampleFour():

    private static void exampleFour()
    {
        Moba m = new Moba();
        Moba dragon = new Moba();
        dragon.A_Descriptor = dragon.SetDescriptor("A Disdainful Dragon");
        dragon.Constitution = 20;
        dragon.Level = 100;
        dragon.HitPoints = dragon.RecalculateMaxHitPoints();

        ConsoleIO.conShow("\\pExample #4:\\d", "\\ge");
        m.A_Descriptor = "He Who Is About To Die";
        ConsoleIO.conShow("\\pUsing Moba instance '", "\\wh");
        ConsoleIO.conShow("m", "\\ye");
        ConsoleIO.conShow("', instantiated with default constructor.\\n", "\\wh");
        ConsoleIO.conShow("\\pUsing Moba instance '");
        ConsoleIO.conShow("dragon", "\\ma");
        ConsoleIO.conShow("', modified by ", "\\wh");
        ConsoleIO.conShow("SetDescriptor()", "\\cy");
        ConsoleIO.conShow(" and\\n", "\\wh");
        ConsoleIO.conShow("\\pRecalculateMaxHitPoints()", "\\cy");
        ConsoleIO.conShow("\\d", "\\wh");
        ConsoleIO.conShow("\\p" + m.A_Descriptor, "\\ye");
        ConsoleIO.conShow(" has " + m.HitPoints.ToString() + " hit points.\\n", "\\wh");
        ConsoleIO.conShow("\\p" + m.A_Descriptor, "\\ye");
        ConsoleIO.conShow(" attacks ", "\\wh");
        ConsoleIO.conShow(dragon.A_Descriptor, "\\ma");
        ConsoleIO.conShow(" that has " + dragon.HitPoints.ToString() + " hit points.\\n", "\\wh");
        ConsoleIO.conShow("\\p" + m.A_Descriptor, "\\ye");
        ConsoleIO.conShow(" slightly injures ", "\\wh");
        ConsoleIO.conShow(dragon.A_Descriptor, "\\ma");
        ConsoleIO.conShow(".\\n");
        dragon = MobileKiller.InjureMobile(ref dragon, 3);
        ConsoleIO.conShow("\\p" + dragon.A_Descriptor, "\\ma");
        ConsoleIO.conShow(" has " + dragon.HitPoints.ToString() + " hit points.\\n", "\\wh");
        ConsoleIO.conShow("\\p" + dragon.A_Descriptor, "\\ma");
        ConsoleIO.conShow(" claws ", "\\wh");
        ConsoleIO.conShow(m.A_Descriptor, "\\ye");
        ConsoleIO.conShow("!\\n", "\\wh");
        Moba dead = MobileKiller.KillMobile(ref m);
        ConsoleIO.conShow("\\p" + dead.A_Descriptor, "\\ye");

        if (dead.HitPoints == 0)
        {
            ConsoleIO.conShow(" is dead! (hit points = " + dead.HitPoints.ToString() + ")\\n", "\\wh");
        }
        else
        {
            ConsoleIO.conShow(" still has this many hit points: " + dead.HitPoints.ToString() + "...\\n", "\\wh");
        }

        ConsoleIO.conShow("\\p");
    }

Next, go to the switch statement and modify case 4 so that it calls the method we just created:

            switch (choice)
            {
                case 1:
                    exampleOne();
                    break;
                case 2:
                    exampleTwo();
                    break;
                case 3:
                    exampleThree();
                    break;
                case 4:
                    exampleFour();
                    break;
                case 5:
                    exitTestApp();
                    break;
            }

Modify the StringBuilder assignment that composes our Menu title as follows:

            StringBuilder sb = new StringBuilder("\n\n");
            sb.Append(" (1) Moba instance instantiated with default constructor \n (2) Moba instance instantiated via MobaArgs object\n");
            sb.Append(" (3) Moba instance with critical success/failure flags set\n (4) Fighting a dragon \n (5) Exit Test App...\n");

Our Example #4 code uses code that we haven't added yet. Go into GameObjs.cs and add the following class and its methods:

public static class MobileKiller
{
    public static Moba KillMobile(ref Moba _m)
    {
        _m.HitPoints = 0;
        return _m;
    }

    public static Moba InjureMobile(ref Moba _m, int injury)
    {
        _m.HitPoints -= injury;
        return _m;
    }
}

Now save GameObjs.cs and testGameObjs3.cs. Switch to the command prompt and build. Run testgo3.exe and choose Menu Item #4:

So, what have we accomplished so far in this lesson? Well, we now have a way to cause a Moba instance to recalculate its MaxHitPoints. This is useful because the default Moba constructor sets Constitution to 1. In the case of example 4, I didn't want the hassle of having to specify each and every member variables' value, so I used the default constructor to create my dragon instance. Then I changed the mobile's Constitution value to 20. At that point in the code, though, the dragon's MaxHitPoints still reflect a Constitution value of 1. So I called the RecalculateMaxHitPoints() method. Handy!

Note also the use of SetDescriptor() in our Example #4 code. I didn't want a mighty dragon's A_Descriptor to be an empty string, after all!

Class MobileKiller is the third class to be added to GameObjs.cs. That source code file now contains Moba, MobaArgs, and MobileKiller classes.

You can download the source files for this lesson by clicking here.

Return to Lesson 19 | Proceed to Lesson 21

Return to Index

C# Text Adventure: Lesson 19

Beginning to think about Combat

Beginning to think about combat. Hmm. What data do we need in order for two mobile actors (instances of class Moba) to engage one another in combat?

We'd need to know their current lifeforce ("HitPoints" in our implementation). They'd need some way to determine if their attacks succeed or fail. Well, we already have the attack_roll() method that can generate a random integer between 1 and 20, inclusive. How, then, to determine, whether that result is good enough to get through the defending mobile actor's defenses?

Ah! We should add a defense_roll() method to class Moba. And we'll make it (and go back and retroactively make attack_roll()) virtual, in case we want a child class to override it at some point.

So here are those two methods:

    public virtual int attack_roll()
    {
        Random random = new Random(Guid.NewGuid().GetHashCode());
        return random.Next(1, 21) + Level;
    }

    public virtual int defense_roll()
    {
        Random random = new Random(Guid.NewGuid().GetHashCode());
        return random.Next(1, 21) + Level;
    }

Now, should a result of 1 on the d20 roll be considered a failure, regardless of the the value of variable Level that is added to it? Such that a Level 30 mobile fighting a Level 1 commoner fails to hurt the commoner, even when the Level 30 mobile's total is higher than the defender's?

Example:

Level 30 mobile: d20+Level = 1 + 30 = 31
Level 01 mobile: d20+Level = 5 + 1 = 6

If a d20 result of 1 is a critical failure, then the 31 rolled by the aggressor above fails to harm the Level 01 mobile, even though the defender's defense roll — 5 — is much lower than the aggressor's attack roll.

Likewise, should a d20 result of 20 be considered a critical success, a success so good that it gets through the opponent's defenses, regardless of their defense roll?

Example:

Commoner attacks Lv30 mobile: d20+1 = 20+1 = 21
Lv30 mobile defends with: d20+30 = 1+30 = 31

If a 1 is a critical failure, then the Commoner in the example above does injure the Lv30 mobile, even though the Commoner's total normally would be insufficient..

The correct answer: sometimes. For certain mobiles, perhaps for those of type Swordmaster, a d20=1 result might not indicate a critical failure, even though for less skillful individuals it would certainly indicate failure. Likewise, for some mobiles, perhaps those of type HouseFly, a d20=20 result might not indicate a critical success, even though for many mobiles it would so indicate.

Given the reasoning delineated above, why not add a couple of boolean members to class Moba to act as flags? Okay, so we do that:

And then we'll need to add getters and setters for those:

In our default constructor, we'll set both these flags to false:

However, to update the overloaded constructor that takes a MobaArgs instance as a parameter, we will first need to update the MobaArgs class. I've circled additions to the code of class MobaArgs:

Then, back in class Moba, I've updated the member variables section with our two booleans:

And here are the updated default and overloaded constructors:

After adding code to testGameObjs3.cs to implement the 3rd menu item (remember that the previous lesson used up menu items 1 and 2), I saved changes to testGameObjs3.cs and GameObjs.cs and built the project.

Now do you see why I created the MobaArgs helper class? Already we have a dozen member variables to set for each instance of Moba, and we'll doubtless add more in future lessons. MobaArgs gives us a way to build up an object containing all the needed parameters for Moba over several lines of code:

        MobaArgs mobargs = new MobaArgs();
        mobargs.MobaNonAbilityArgs(5000, "A creature", 1, 3);
        mobargs.MobaAbilityArgs(8, 10, 9, 12, 7, 6);
        mobargs.MobaCriticalArgs(false, false);

        Moba m = new Moba(mobargs);

We now can specify whether any particular instance of Moba has one or both critical flags set to true. And we have added a defense_roll() method. We have everything we need for basic combat, so we'll progress in that direction in the next lesson.

If you prefer, you can download a zip file containing the updated GameObjs.cs and testGameObjs3.cs source code files. Just extract them into your heroes directory.

You can download the source files for this lesson by clicking here. The archive of source code files and response files needed for building testgo3.exe (our third test app for game objects) can be downloaded by clicking here. It includes code in GameObjs.cs for menu item #4 discussed in the next lesson.

Return to Lesson 18 | Proceed to Lesson 20

Return to Index

C# Text Adventure: Lesson 18

Further GameObjs.cs development

Welcome to Lesson 18.

The first thing you should do is move testGameObjs2.cs and testGameObjs2.rsp to subdirectory testcode. Keep a copy of our second GameObjs.dll testing app, testgo2.exe. Put it wherever you want, but save it in case we wanna see the distribution of d20 attack rolls later.

Have you noticed the voluminous text that scrolls past when you run your build.bat file in the command prompt? Do this: open the response file for each of your source code files and add the /nologo switch. Here are the listings, respectively, for heroes.rsp, ConsoleInputOutput.rsp, ConsoleColors.rsp, GameObjs.rsp, and testGameObjs3.rsp (whose *.cs source code file you'll create — or download — below):

/t:exe /nologo /r:ConsoleInputOutput.dll /out:heroes.exe heroes.cs

/t:library /nologo /r:ConsoleColors.dll /out:ConsoleInputOutput.dll ConsoleInputOutput.cs

/t:library /nologo /out:ConsoleColors.dll ConsoleColors.cs

/t:library /nologo /out:GameObjs.dll GameObjs.cs

/t:exe /nologo /r:GameObjs.dll /out:testgo3.exe /r:ConsoleInputOutput.dll testGameObjs3.cs

Save those five modified response files, then build again. Much less text. Nicer:

Delete method conShowMultiColor4Words() from ConsoleInputOutput.cs. It's a kludge. If we need its functionality, we'll use a series of conShow() method invocations to get it. Save the source code file.

Next, create a new test app's source code file, and name it testGameObjs3.cs; here is the skeleton code for that file, with the first two menu items taken up by examples making use of our new Moba class (whose code listing appears further down this article).

If you prefer, download the testGameObjs3.cs source file for this lesson here, and extract it into your heroes directory:

testGameObjs3.cs code listing:

using System;
using System.Reflection;
using System.Text;

public sealed class TestClass
{
    public static void Main()
    {
        ConsoleIO.conShow("", "\\wh");
        Console.TreatControlCAsInput = true; //prevents ending if user presses Ctrl-C
        while (true)
        {
            ConsoleIO.conShow("\\n", "\\ge");
            ConsoleIO.conShow("\\pTest App Menu:");
            ConsoleIO.conShow("", "\\wh");

            StringBuilder sb = new StringBuilder("\n\n");
            sb.Append(" (1) Moba instance instantiated with default constructor \n (2) Moba instance instantiated via MobaArgs object\n (3) Example Code 3\n (4) Example Code 4\n (5) Exit Test App...\n");
            ConsoleIO.conShow("", "\\wh");
            ConsoleIO.conShow(sb.ToString());
            ConsoleIO.conShow("\\n");

            int choice = ConsoleIO.getNumberSelection(5);
            ConsoleIO.conShow("\n\n", "\\wh");

            switch (choice)
            {
                case 1:
                    exampleOne();
                    break;
                case 2:
                    exampleTwo();
                    break;
                case 3:

                    break;
                case 4:

                    break;
                case 5:
                    exitTestApp();
                    break;
            }
        }
    }

    private static void exampleOne()
    {
        Moba m = new Moba();
        ConsoleIO.conShow("\\pShowing Moba instance 'm', instantiated with default,\\n");
        ConsoleIO.conShow("\\pparameterless constructor:\\d");
        ConsoleIO.conShow("\\pCode:\tMoba m = new Moba();\\d");
        ConsoleIO.conShow("\\pm.A_Descriptor = '" + m.A_Descriptor + "'\tm.Age_Days = '" + m.Age_Days.ToString() + "'\tm.Level = '" + m.Level.ToString() + "'\\n");
        ConsoleIO.conShow("\\pm.MaxHitPoints = '" + m.MaxHitPoints.ToString() + "'\tHitPoints = '" + m.HitPoints.ToString() + "'\t\t");
        ConsoleIO.conShow("m.MoveRate = '" + m.MoveRate.ToString() + "\\n");
        ConsoleIO.conShow("\\pm.Strength = '" + m.Strength.ToString() + "'\tm.Constitution = '" + m.Constitution.ToString() + "'\t");
        ConsoleIO.conShow("m.Dexterity = '" + m.Dexterity.ToString() + "'\\n");
        ConsoleIO.conShow("\\pm.Wisdom = '" + m.Wisdom.ToString() + "'\t\tm.Intelligence = '" + m.Intelligence.ToString() + "'\tm.Charisma = '" + m.Charisma.ToString() + "'\\n");
        ConsoleIO.conShow("\\d", "\\wh");

        /*         if (!MethodChecker.HasMethod(c, "attack_roll"))
                {
                    ConsoleIO.conShow("Too bad " + c.A_Descriptor + " doesn't get an attack roll...\n");
                }
         */
    }

    private static void exampleTwo()
    {
        MobaArgs mobargs = new MobaArgs();
        mobargs.MobaNonAbilityArgs(5000, "A creature", 1, 3);
        mobargs.MobaAbilityArgs(8, 10, 9, 12, 7, 6);

        Moba m = new Moba(mobargs);
        ConsoleIO.conShow("\\pShowing Moba instance 'm', instantiated with instance\\n");
        ConsoleIO.conShow("\\pof MobaArgs:\\d");
        ConsoleIO.conShow("\\pCode:\tMobaArgs mobargs = new MobaArgs();\\n");
        ConsoleIO.conShow("\\p\tmobargs.MobaNonAbilityArgs(5000, \"A creature\", 1, 3);\\n");
        ConsoleIO.conShow("\\p\tmobargs.MobaAbilityArgs(8, 10, 9, 12, 7, 6);\\n");
        ConsoleIO.conShow("\\p\tMoba m = new Moba(mobargs);");
        ConsoleIO.conShow("\\d");
        ConsoleIO.conShow("\\pm.A_Descriptor = '" + m.A_Descriptor + "'\tm.Age_Days = '" + m.Age_Days.ToString() + "'\tm.Level = '" + m.Level.ToString() + "'\\n");
        ConsoleIO.conShow("\\pm.MaxHitPoints = '" + m.MaxHitPoints.ToString() + "'\tHitPoints = '" + m.HitPoints.ToString() + "'\t\t");
        ConsoleIO.conShow("m.MoveRate = '" + m.MoveRate.ToString() + "\\n");
        ConsoleIO.conShow("\\pm.Strength = '" + m.Strength.ToString() + "'\tm.Constitution = '" + m.Constitution.ToString() + "'\t");
        ConsoleIO.conShow("m.Dexterity = '" + m.Dexterity.ToString() + "'\\n");
        ConsoleIO.conShow("\\pm.Wisdom = '" + m.Wisdom.ToString() + "'\t\tm.Intelligence = '" + m.Intelligence.ToString() + "'\tm.Charisma = '" + m.Charisma.ToString() + "'\\n");
        ConsoleIO.conShow("\\d", "\\wh");
    }

    private static void exitTestApp()
    {
        ConsoleIO.conShow("\\pExit test app? ");

        int result = ConsoleIO.areYouSure();

        if (result == 1)
        {
            ConsoleIO.conShow("\\n", "\\wh");
            ConsoleIO.conShow("\\pBye-bye...");
            ConsoleIO.conShow("\\n");

            System.Threading.Thread.Sleep(750);
            Environment.Exit(0);
        }
    }
}

public static class MethodChecker
{
    public static bool HasMethod(this object objectToCheck, string methodName)
    {
        try
        {
            var type = objectToCheck.GetType();
            return type.GetMethod(methodName) != null;
        }
        catch (AmbiguousMatchException)
        {
            // ambiguous means there is more than one result,
            // which means: a method with that name does exist
            return true;
        }
    }
}

Did you notice the static class MethodChecker in the code above? It has a method, HasMethod(), which uses reflection to tell us whether or not the object we pass as a parameter contains a method with the name we've passed in as a string. Will be useful as we continue, though we don't actually use it in this lesson.

Here's an annotated screen shot of testGameObjs.cs in Notepad++:

We showed the contents of our test app's response file in a previous listing. Go into your build.bat file and change line 4 so that it references the new response file:

Now, in my experience, creating hierarchies of parent-child classes quickly becomes mind-numbing and leads to headaches. Let's simplify our code a bit by getting ready of class Creature, HarmlessCreature, DangerousCreature, and Humanoid. Replace Creature with a class which we'll call Moba (short for mobile actor in our game):

Here's the listing (alternatively, download the GameObjs.cs source code file here, and extract it into your project directory):

public class Moba
{
    public const byte MAX_BYTE = 255;
    public const byte MAX_CONSTITUTION = 20;
    private string a_descriptor;

    private byte strength;
    private byte constitution;
    private byte dexterity;
    private byte wisdom;
    private byte intelligence;
    private byte charisma;
    private byte level;
    private byte moverate;

    private int age_days;
    private int hitpoints;
    private int maxhitpoints;

    public Moba()
    {
        A_Descriptor = string.Empty;
        Age_Days = 5 * 365;
        Strength = 1;
        Constitution = 1;
        Dexterity = 1;
        Wisdom = 1;
        Intelligence = 1;
        Charisma = 1;
        Level = 1;
        MoveRate = 1;
    }

    public Moba(MobaArgs ma)
    {
        Age_Days = ma.ArgAge;
        A_Descriptor = ma.ArgDesc;
        Level = ma.ArgLevel;
        MoveRate = ma.ArgMoveRate;
        Charisma = ma.ArgCharisma;
        Constitution = ma.ArgConstitution;
        Dexterity = ma.ArgDexterity;
        Intelligence = ma.ArgIntelligence;
        Strength = ma.ArgStrength;
        Wisdom = ma.ArgWisdom;
    }

    public virtual string MakeSound()
    {
        string p = A_Descriptor + " snorts.";
        return p;
    }

    public int attack_roll()
    {
        Random random = new Random(Guid.NewGuid().GetHashCode());
        return random.Next(1, 21) + Level;
    }

    public string A_Descriptor
    {
        get { return a_descriptor; }
        set { a_descriptor = value; }
    }

    public int Age_Days
    {
        get { return age_days; }
        set { age_days = value; }
    }

    public byte Strength
    {
        get { return strength; }
        set { strength = value; }
    }

    public byte Constitution
    {
        get { return constitution; }
        set
        {
            constitution = value;
            if (value > MAX_CONSTITUTION)
            {
                constitution = MAX_CONSTITUTION;
            }
        }
    }

    public byte Dexterity
    {
        get { return dexterity; }
        set { dexterity = value; }
    }

    public byte Wisdom
    {
        get { return wisdom; }
        set { wisdom = value; }
    }

    public byte Intelligence
    {
        get { return intelligence; }
        set { intelligence = value; }
    }

    public byte Charisma
    {
        get { return charisma; }
        set { charisma = value; }
    }

    public byte Level
    {
        get { return level; }
        set { level = value; }
    }

    public byte MoveRate
    {
        get { return moverate; }
        set
        {
            moverate = value;
            if (value < 0) { moverate = 0; }
            if (value > MAX_BYTE) { moverate = MAX_BYTE; }
        }
    }

    public int HitPoints
    {
        get { return hitpoints; }
        set
        {
            hitpoints = value;
            if (value > MaxHitPoints)
            {
                hitpoints = MaxHitPoints;
            }
        }
    }

    public int MaxHitPoints
    {
        get { return ((5 * Constitution) + Level); }
        set { maxhitpoints = (5 * Constitution) + Level; }
    }
}

If you are electing to copy/paste rather than download source code files, then also copy/paste the following class, placing it after class Moba in source code file GameObjs.cs:

public class MobaArgs
{
    private int _argAge;
    private string _argDesc;
    private byte _argLevel;
    private byte _argMoveRate;
    private byte _argCharisma;
    private byte _argConstitution;
    private byte _argDexterity;
    private byte _argIntelligence;
    private byte _argStrength;
    private byte _argWisdom;

    public void MobaNonAbilityArgs(int _age, string _desc, byte _level, byte _moverate)
    {
        ArgAge = _age;
        ArgDesc = _desc;
        ArgLevel = _level;
        ArgMoveRate = _moverate;
    }

    public void MobaAbilityArgs(byte _cha, byte _con, byte _dex, byte _intel, byte _str, byte _wis)
    {
        ArgCharisma = _cha;
        ArgConstitution = _con;
        ArgDexterity = _dex;
        ArgIntelligence = _intel;
        ArgStrength = _str;
        ArgWisdom = _wis;
    }

    public int ArgAge
    {
        get { return _argAge; }
        set { _argAge = value; }
    }

    public string ArgDesc
    {
        get { return _argDesc; }
        set { _argDesc = value; }
    }

    public byte ArgLevel
    {
        get { return _argLevel; }
        set { _argLevel = value; }
    }

    public byte ArgMoveRate
    {
        get { return _argMoveRate; }
        set { _argMoveRate = value; }
    }

    public byte ArgCharisma
    {
        get { return _argCharisma; }
        set { _argCharisma = value; }
    }

    public byte ArgConstitution
    {
        get { return _argConstitution; }
        set { _argConstitution = value; }
    }

    public byte ArgDexterity
    {
        get { return _argDexterity; }
        set { _argDexterity = value; }
    }

    public byte ArgIntelligence
    {
        get { return _argIntelligence; }
        set { _argIntelligence = value; }
    }

    public byte ArgStrength
    {
        get { return _argStrength; }
        set { _argStrength = value; }
    }

    public byte ArgWisdom
    {
        get { return _argWisdom; }
        set { _argWisdom = value; }
    }
}

Build and run testgo3.exe, selecting the first menu item. TADA!

And here is the output if you select menu item 2:

Take some time and study the code in testGameObjs3.cs and GameObjs.cs. Note that class MobaArgs is basically just a helper class for Moba, making it easier to specify all ten data points needed to declare an instance of Moba.

Thus far, we have the option of declaring a Moba instance using the default constructor or the overloaded constructor that takes an instance of MobaArgs as a parameter.

In the next lesson, we will begin to think about how two instances of our mobile actor (i.e., of our Moba class) can engage in combat with one another.

We'll continue to add to testGameObjs3.cs as we test our GameObjs.cs code.

You can download the source files for this lesson by clicking here.

Return to Lesson 17 | Proceed to Lesson 19

Return to Index

C# Text Adventure: Lesson 17

Adding more classes to GameObjs.cs

First things first: move testGameObjs1.cs and testGameObjs1.rsp to subdirectory testcode.

Next, create testGameObjs2.cs per the following listing:

using System;

public static class TestClass
{
    public static void Main() { }
}

Create a response file, testGameObjs2.rsp, to tell the command-line compiler how to compile it into an executable. Here's the content that goes in that file:

/t:exe /r:GameObjs.dll /out:testgo2.exe /r:ConsoleInputOutput.dll testGameObjs2.cs

Finally, modify build.bat, replacing line 4 so that it now references our new test app's response file:

csc.exe @ConsoleColors.rsp
csc.exe @ConsoleInputOutput.rsp
csc.exe @GameObjs.rsp
csc.exe @testGameObjs2.rsp
csc.exe @heroes.rsp

Good. We're all set up to add more classes to GameObjs.cs and test them using testGameObjs2.cs.

Let's modify our parent Creature class a bit:

public class Creature
{
    private string a_descriptor;
    private int age_days;

    public Creature()
    {
        a_descriptor = "A nondescript creature";
        age_days = 3650;
    }

    public virtual string MakeSound()
    {
        string p = A_Descriptor + " snorts.";
        return p;
    }

    public string A_Descriptor
    {
        get { return a_descriptor; }
        set { a_descriptor = value; }
    }

    public int Age_Days
    {
        get { return age_days; }
        set { age_days = value; }
    }
}

And now, let's add a couple of classes that derive from class Creature. We'll name them HarmlessCreature and DangerousCreature.

Here are their listings:

public class HarmlessCreature : Creature
{
    public HarmlessCreature()
    {
        A_Descriptor = "A weak-looking creature";
    }

    public override string MakeSound()
    {
        string p = A_Descriptor + " whimpers.";
        return p;
    }
}
public class DangerousCreature : Creature
{
    public DangerousCreature()
    {
        A_Descriptor = "A feral-looking creature";
    }

    public override string MakeSound()
    {
        string p = A_Descriptor + " emits a chilling, predatorial sound.";
        return p;
    }

    public int attack_roll()
    {
        Random random = new Random(Guid.NewGuid().GetHashCode());
        return random.Next(1, 21);
    }
}

Remember method conShowMultiColorLine() from the previous lesson. We're gonna rename it to conShowMultiColor4Words(). Here's it's code listing:

    public static void conShowMultiColor4Words(string word1, string col1, string word2, string col2, string word3, string col3, string word4, string col4, bool keepLast2Together)
    {
        if (word1 == null) { return; }
        if (word2 == null) { return; }
        if (word3 == null) { return; }
        if (word4 == null) { return; }
        conShow("\\p" + word1 + " ", col1);
        conShow(word2 + " ", col2);
        conShow(word3, col3);
        if (keepLast2Together)
        {
            conShow(word4, col4);
        }
        else
        {
            conShow(" ");
            conShow(word4, col4);
        }

        conShow("", "\\wh");
    }

After modifying our parent class Creature and creating those two child classes in GameObjs.cs, and after modifying the method in ConsoleInputOutput.cs, save the files.

Let's take a look at the child classes. HarmlessCreature inherits from Creature and in its constructor modifies the value that the base class assigns to A_Descriptor, making the new descriptor more apropos to a weak, harmless creature. And it overrides the MakeSound() method so that the harmless creature sorta sounds harmless too.

DangerousCreature inherits a descriptor and a default age from its parent class, just as HarmlessCreature does. DangerousCreature's constructor set the descriptor to something a bit more sinister. It overrides the MakeSound() method so that the noise a dangerous creature produces sounds dangerous.

And class DangerousCreature adds this method called attack_roll(). Hmm, interesting...

Now let's add some code in our testGameObjs2.cs source file, and put these creatures through their paces. I chose to make a bit more involved test app this time. I borrowed menuing code from heroes.cs. The entire testGameObjs2.cs file is over 130 lines long. The easiest thing is to download the completed testGameObjs2.cs file by clicking here; and then, move it into your heroes directory. However, should you wish to copy/paste and then fiddle with formatting stuff in Notepad++, use the listing below:

using System;
using System.Text;

public sealed class TestClass
{
    public static void Main()
    {
        ConsoleIO.conShow("", "\\wh");
        Console.TreatControlCAsInput = true; //prevents ending if user presses Ctrl-C
        while (true)
        {
            StringBuilder sb = new StringBuilder("\n\n");
            sb.Append(" (1) HarmlessCreature demo\n (2) DangerousCreature demo\n (3) Parent Class demo\n (4) Test attack_roll()\n (5) Exit Test App...\n");
            ConsoleIO.conShow("", "\\wh");
            ConsoleIO.conShow(sb.ToString());
            ConsoleIO.conShow("\\d");

            int choice = ConsoleIO.getNumberSelection(5);
            ConsoleIO.conShow("\n\n", "\\wh");

            switch (choice)
            {
                case 1:
                    harmlessCreature();
                    break;
                case 2:
                    dangerousCreature();
                    break;
                case 3:
                    parentClassCreature();
                    break;
                case 4:
                    testAttackRoll();
                    break;
                case 5:
                    exitTestApp();
                    break;
            }
        }
    }

    private static void testAttackRoll()
    {
        DangerousCreature d = new DangerousCreature();
        ConsoleIO.conShow("\\d");

        int result = 0;
        int cnt = 0;
        int[] nums = new int[20];

        ConsoleIO.conShow("\\pNow let's make 1,000 rolls of a twenty-sided die (a 'dodecahedron')\\n");
        ConsoleIO.conShow("\\pand view the distribution of results. Press a key to continue...\\d");

        Console.ReadKey();

        for (int i = 0; i < 1000; i++)
        {
            result = d.attack_roll();

            int local_cnt = nums[result - 1];
            nums[result - 1] = local_cnt + 1;
        }

        ConsoleIO.conShow("\\pDistribution of 1-20 results over 1,000 d20 rolls:\\d");

        cnt = 0;

        for (int i = 0; i < 20; i++)
        {
            ConsoleIO.conShow("\\pResult of \"", "\\wh");
            ConsoleIO.conShow((i + 1).ToString(), "\\ye");
            ConsoleIO.conShow("\": " + nums[i].ToString() + " times {total results in range 1-" + (i + 1).ToString() + " ", "\\wh");
            cnt += (int)nums[i];
            ConsoleIO.conShow(cnt.ToString() + " ", "\\ge");
            double dd = (cnt * 100) / 1000;
            ConsoleIO.conShow("~ " + dd.ToString() + "%", "\\gr");
            ConsoleIO.conShow("}\\n", "\\wh");
        }

        ConsoleIO.conShow("", "\\wh");
    }

    private static void exitTestApp()
    {
        ConsoleIO.conShow("\\pExit test app? ");

        int result = ConsoleIO.areYouSure();

        if (result == 1)
        {
            ConsoleIO.conShow("\\n", "\\wh");
            ConsoleIO.conShow("\\pBye-bye...");
            ConsoleIO.conShow("\\n");

            System.Threading.Thread.Sleep(1000);
            Environment.Exit(0);
        }
    }

    private static void harmlessCreature()
    {
        HarmlessCreature h = new HarmlessCreature();

        ConsoleIO.conShow("\\d");
        ConsoleIO.conShow("\\pIn the distance, you see " + h.A_Descriptor.ToLower() + "...\\n");
        ConsoleIO.conShow("\\p" + h.MakeSound());
    }

    private static void dangerousCreature()
    {
        DangerousCreature d = new DangerousCreature();

        ConsoleIO.conShow("\\d");

        ConsoleIO.conShow("\\pNearby sits " + d.A_Descriptor.ToLower() + ".\\n");
        if (d is DangerousCreature)
        {
            ConsoleIO.conShowMultiColor4Words("It", "\\wh", "looks", "\\wh", "dangerous", "\\ma", "!", "\\wh", true);
            ConsoleIO.conShow("\\n", "\\wh");
            ConsoleIO.conShowMultiColor4Words("It's", "\\ge", "teeth", "\\lc", "look", "\\cy", "razor-sharp!", "\\ye", false);
        }
        ConsoleIO.conShow("\\n");
        ConsoleIO.conShow("\\p" + d.MakeSound());
        ConsoleIO.conShow("\\d");
    }

    private static void parentClassCreature()
    {
        Creature c = new Creature();
        ConsoleIO.conShow("\\pBefore you sits " + c.A_Descriptor + ".\\n");
        ConsoleIO.conShow("\\p" + c.MakeSound());
    }

}

The first three menu items in the test app demonstrate the behavior of instances of our parent class and its two children. The fourth menu item demonstrates the power of the attack_roll() method that is a member of the DangerousCreature class. This method is quite powerful. It generates a random integer between 1 and 20, inclusive. We will use this and similarly randomly generated numbers to implement a combat system (not much) later in our game's development.

You can download the source files for this lesson by clicking here.

Return to Lesson 16 | Proceed to Lesson 18

Return to Index

C# Text Adventure: Lesson 16

Putting Meat on the Bones of GameObjs.cs - NOT!

The title? Yeah, it's a bit laughable to say we're "putting meat on the bones" of GameObjs.cs, since we're barely putting in enough code to test the DLL we build. You'll see, as we construct a base class called Creature, that it's extremely basic. That's because we'll build up complexity in classes that inherit from it later on in our development.

We'll begin with a very simple class called Creature in GameObjs.cs. Here is the updated code listing for that source file:

using System;

public class Creature
{
    private readonly string a_descriptor;
    private readonly int age_days;

    public Creature()
    {
        a_descriptor = "A nondescript creature";
        age_days = 3650;
    }

    public string A_Descriptor
    {
        get { return a_descriptor; }
    }

    public int Age_Days
    {
        get { return age_days; }
    }

}

And here is the code listing for testGameObjs1.cs:

using System;

class TestClass
{
    public static void Main()
    {
        Creature c = new Creature();
        ConsoleIO.conShow("\\d");
        ConsoleIO.conShow("\\p" + c.A_Descriptor + " sits here watching you." + "\\d", "\\wh");
        ConsoleIO.conShow("", "\\wh");
    }
}

Build the project by running build.bat at the command prompt. Then run testgo1.exe. Here is the output:

Now let's add a method to ConsoleInputOutput.cs that allows us to print a multi-colored line to the Console with a single method invocation. Admittedly, we can already accomplish this with multiple conShow invocations.

Here's the listing for the new method:

    public static void conShowMultiColorText(string word1, string col1, string word2, string col2, string word3, string col3, string word4, string col4)
    {
        if (word1 != null) { conShow("\\p" + word1 + " ", col1); }
        if (word2 != null) { conShow(word2 + " ", col2); }
        if (word3 != null) { conShow(word3 + " ", col3); }
        if (word4 != null) { conShow(word4, col4); }
        conShow("", "\\wh");
    }

Modify testGameObjs1.cs as follows:

using System;

class TestClass
{
    public static void Main()
    {
        Creature c = new Creature();
        ConsoleIO.conShow("\\d");
        ConsoleIO.conShow("\\p" + c.A_Descriptor + " sits here watching you." + "\\d", "\\wh");
        ConsoleIO.conShow("\\n", "\\wh");

        ConsoleIO.conShowMultiColorText("You're", "\\ge", "a", "\\ma", "crazy", "\\ye", "programmer!", "gr");
        ConsoleIO.conShow("\\d");
    }
}

Save both files, then switch to the command prompt and rebuild the project. Run testgo1.exe. Here's the output:

In the next lesson we'll derive two classes from CreatureHarmlessCreature and Dangerous Creature.

See you in Lesson 17...

You can download the source files for this lesson by clicking here.

Return to Lesson 15 | Proceed to Lesson 17

Return to Index