Follow-Up on Designing a Game

So I designed and created a small game to demonstrate several techniques.  First, I demonstrated how to contain the scope of a project to ensure it gets out the door.  While this was only a tiny project, my goal was to create a working game over the span of a weekend or possibly a week (It took me about 24 man-hours of labor in total over a span of 5 or 6 days).  Technically, I did not track my hours and I did not estimate the development time of this project.  For any projects taking more than a week, estimates should also be included.  Also, if you review the design specifications and match it with the actual game, you’ll notice a few missing features.  Notably, there are not armor units, it’s legal to move past enemy units and the “next phase” indicator is not very visually appealing.  Chalk it up to time constraints.  I wanted to get this out the door so I could blog about it.  If this were a real game, I’d have to spend much more time polishing it to make it more visually appealing.

About the Game

One important thing I forgot to do in my last blog post was explain how to play the game.  It’s somewhat trivial to figure out, but for those who have never played any Avalon military board games, it goes like this:

1. The game phase controls what can be done on the board, starting with the allied move phase.
2. When in the move phase, you can move your units.  Each unit that has been moved will have their movement allocation decremented to zero.  So you can’t move the unit again for the current turn.
3. Once you are satisfied with your unit moves or you have no more units to move, you can click the “next” button to advance to the allied attack phase.
4. During the allied attack phase you can attack any enemy unit that is next to one of your allied units.  If you are not next to any enemy units, then you must skip the attack phase.  Each unit can only attack once for this turn.
5. Once you are satisfied with this phase, you can click the “next” button to advance to the next phase, which is the axis movement phase.
6. Now it’s the computer’s turn to move its units.  The computer will move units around the board and automatically advance the phase to the axis attack phase.
7. When the computer is in the axis attack phase it will attack any allied units that are next to one of its own units.  Then it will advance the phase to the allied movement phase.
8. Continue to step 2 until an end condition is met.

Game End Condition

The game ends when either all 4 cities are captured or all units on the allied or axis side are eliminated.

Weaknesses in the Current Game

If you play this game a few times you’ll notice that it’s about as difficult to play as tic-tac-toe.  It becomes predictable and it’s easy to formulate a strategy to beat the computer almost every time.  One problem is that the dice roll is biased toward the defense.  This is due to the fact that all units have an offense of one and a defense of two.  If you look at the ComputeBattleResult method of the GameClass object, you’ll notice that it’s just hard-coded at the moment.  I did that because there is only one type of unit on the board.  To expand this game it will be necessary to use the unit index parameters to lookup the offense and defense of the two units and use a lookup table (or a formula) to compute the odds.

The second problem is that the enemy only has one strategy.  Divide its units into 4 groups of units and march them to each of the four cities.  Attacking allied units along the way.  If you manage to destroy the 3 units heading to one particular city, the enemy will not dispatch additional units to reinforce the hole in its strategy.

Options for Fixing the Game Play

One option to fix the game play is to introduce a variety of different units.  Many of the methods that assume there is only one type of unit will need to be altered to make sure the defense and offense are accounted for.  New units should maintain the single movement allocation and range of one to keep this under control for now.

A second option is to add a mask to the play field.  This could be done by creating black hexagon images that can be written over the board positions not explored by the allied units.  Obscuring what has not been seen yet.  This will only have minimal impact for those who play the game more than once because they don’t need to re-explore a map they already have memorized.

An alternative or addition to the second option is to create a mask of areas not currently visible by allied units.  This can be accomplished by dimming the hex areas where allied units cannot see and not rendering any enemy units under the dimmed areas.  The background will be visible but enemy units not in visual range will not be seen.  Using this technique in combination with alternate enemy strategies has the potential to make the game more challenging.

Other Possible Enhancements

1. Allow stackable units.  The top unit could contain an icon with the word “stack” in it to indicate that it is a stack of different units.  Offense and defense numbers should be totaled for the top unit.  Complications arise from the fact that units must now be stacked and un-stacked.  Moving a stack needs to be accounted for.  Attacking an enemy unit should account for the entire stack.  I would propose doing the stack bookkeeping in the C# code and replacing all the units in the stack with a special unit in the JavaScript.  There is also the issue of attack.  This should be changed to provide a partial kill instead of all or nothing.  Offense and defense number would need to be decremented as the attack is occurring and units in the stack need to be destroyed if the defense level is too low to support them.

2. Combine attack and movement phases together.  There really is no reason to have a separate attack phase.  If the allied unit is moved into an enemy unit, then it would be considered an attack.  There is one possible issue:  If the range factor is used and a unit has an attack range of 2 or more, then there needs to be a control to determine if that unit is attacking an enemy.

3. Enhance enemy strategy.  The enemy AI can be expanded to provide 2 or more different strategies.  Even a un-sophisticated algorithm could use a random number at the beginning of the game to choose a strategy and then use that throughout the game.  Each strategy can be designed and tested one by one and then randomized when each strategy is completed for the final version.  Also, each strategy can have some intelligence built in.  For instance, in the current strategy of marching to each city, if a group of units is destroyed, then one unit from each other group can replace them.  if the enemy has too few units to hold all cities, then the AI can switch strategy to attempt to hold one city or just battle the allied units in the hope of destroying all allied units.

4. Allow longer movement allowances.  This is tricky because now we’ll need to implement the ability to block units so they can’t just slide past allied units (and vice versa). 

5. Add terrain effects.  One weakness of the map board is that it only has flat grassy fields with a  movement factor of 1.  Adding roads with double movement factors can make the game more interesting.  Also, a river with bridges.  Units can be blocked by rivers and must use a bridge.  Rivers can be used to form natural boundaries and choke points.  This will complicate the destination problem of the AI because now a path finding algorithm must be implemented (like the A* algorithm).  Mountains can be used as a 1/3 movement factor and forests a 1/2 movement factor, allowing for more choke points.

6. Weather factors.  Most physical board games have a random weather factor that changes for each turn.  Weather factors can modify movement (like snow, all movement cut in half) and offense effectiveness or visibility.

7. If a visibility mask is used, then scouts should be added to the game.  These would be fast units with long visibility, but possibly a zero defense and offense.

8 . Random map boards.  Civilization does this very well.  A completely random map can be generated at the beginning of the game.  This would improve the enhancement of obscuring the map until explored, since there is no way to be certain what terrain is under the obscured areas.  This also increases the challenge of the game for repeat players.  Every game is unique.

Conclusion

I think I’ve indicated how fast the scope of this game can expand to occupy all available free time of any programmer.  I’m sure anybody can think up other features to add to this game that would make it more interesting to play.  The best way to tackle a project like this is to prioritize the desired enhancements and focus on one enhancement at a time.  This will keep the game playable while each enhancement is added rather than attempting to do it all and working endlessly for months on a game that doesn’t work (not to mention attempting to debug it all at once).

 

More SVG

In this blog post I’m going to talk about SVG again.  One of the great advantages that SVG has over Silverlight is that it is built into the browser.  That means that you can use it anywhere in the browser window.  Plug-ins are restricted to the box where they are plugged in.  Click on the following link for a sample of what I did off the top of my head in a couple of hours:

SVG Demo


First, you’ll notice that I painted some yellow circles behind the text.  The text is not part of SVG, but part of the web page HTML itself.  I just set the z-index to a negative number to force it behind the text.  Second, you can see some annotation text and red text that points to “stuff” just to show that images can be created and plotted anywhere.  Unfortunately, there is a pixel difference between different browsers (probably in the font style), so your images might not line up with text in all browsers. 

The last demonstration is a bar chart that I threw together just to demonstrate something useful that can be plotted on the screen.  The gray grid is nothing more than horizontal and vertical lines painted first, then the 3d looking boxes are painted over using rectangles and polygons (see picture below):

As indicated in the text above this chart, I designed one bar and put the code in a method call that I call repeatedly with different parameters.  I just feed the method with the height of the red, yellow then green boxes.  In addition I fed the horizontal and vertical positions. 

The Code

First, I’m going to just spit this out from the code-behind page and ignore all conventions regarding business layer, presentation layer, etc.  This is just a demonstration to show feasibility and ease of development.  So I made up a “Print” method that I use over and over to spit the html out to the browser:

public static class UI
{
    public static void Print(string psText)
    {
        HttpContext.Current.Response.Write(psText);
    }


….

There is also a header and footer method inside the UI class:

public static void Header()
{
    string lsHTML;

    HttpContext.Current.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1));
    HttpContext.Current.Response.Cache.SetLastModified(DateTime.UtcNow.AddDays(0));
    HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.NoCache);


    lsHTML = @”
        <!DOCTYPE html>
        <head>
        <meta http-equiv=’Content-type’ content=’text/html;charset=UTF-8′ /> 
        </head>
        <body>
        “
;

    Print(lsHTML);
}


public static void Footer()
{
    Print(“</body>“);
    Print(“</html>“);
}


} // end of UI class

Now, it’s time to move on to the meat (I made it tiny font to fit the page, sorry if you wear glasses like I do):

using System;
using System.Collections.Generic;


namespace SVGDemo
{
    public partial class svg_demo1 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            Render();
        }
        private void Render()
        {
            UI.Header();


            UI.Print(@”
                <svg xmlns=’http://www.w3.org/2000/svg’ version=’1.1′ width=’1000′ height=’1000′>
                “
);

            SVGBarChart laSVGBarChart = new SVGBarChart(570, 300);
            laSVGBarChart.AddDataPoints(“5,50,40 10,30,80 15,25,95 25,10,75 30,5,15 30,25,15“);
            laSVGBarChart.Render();
           
            UI.Print(@”
                </svg>”
);

            UI.Footer();
        }
    }


    public class SVGBarChart
    {
        public int BarChartLeft = 570;
        public int BarChartBottom = 300;
        public List<SVGBarChartItem> Items = new List<SVGBarChartItem>();


        public SVGBarChart(int piLeft, int piBottom)
        {
            BarChartLeft = piLeft;
            BarChartBottom = piBottom;
        }
        public void AddDataPoints(string psDataPoints)
        {
            // expecting “x,y,z x,y,z”
            string[] laDataPoints = psDataPoints.Split(‘ ‘);

            for (int i = 0; i < laDataPoints.Length; i++)
            {
                string[] laSections = laDataPoints[i].Split(‘,’);

                Items.Add(new SVGBarChartItem(laSections[0].ToInt(), laSections[1].ToInt(),
                    laSections[2].ToInt()));
            }
        }
        public void Render()
        {
            DrawGrid(BarChartLeft – 50, BarChartBottom – 160, Items.Count * 50 + BarChartLeft,

                BarChartBottom + 20);

            for (int i = Items.Count – 1; i > -1; i–)
            {
                Items[i].RenderBar(i * 50 + BarChartLeft, BarChartBottom);
            }
        }
        private void DrawGrid(int piLeft, int piTop, int piRight, int piBottom)
        {
            // horizontal lines
            for (int i = 0; i < (piBottom – piTop) / 10 + 1; i++)
            {
                UI.Print(“<line x1=’” + piLeft + “‘ y1=’” + (piTop + i * 10) + “‘ x2=’” + piRight + “‘ y2=’

                    + (piTop + i * 10) + “‘ stroke=’#aaaaaa’ />“);
            }

            // vertical lines
            for (int i = 0; i < (piRight – piLeft) / 10 + 1; i++)
            {
                UI.Print(“<line x1=’” + (piLeft + i * 10) + “‘ y1=’” + piTop + “‘ x2=’” + (piLeft + i * 10)

                    + “‘ y2=’” + piBottom + “‘ stroke=’#aaaaaa’ />“);
            }
        }
    }


    public class SVGBarChartItem
    {
        int RedHeight;
        int YellowHeight;
        int GreenHeight;


        public SVGBarChartItem(int piRedHeight, int piYellowHeight, int piGreenHeight)
        {
            RedHeight = piRedHeight;
            YellowHeight = piYellowHeight;
            GreenHeight = piGreenHeight;
        }
        public void RenderBar(int piLeft, int piBottom)
        {
            // draw one bar
            int liWidth = 30;
            int liHorizontalOffset = 10;
            int liVerticalOffset = 6;
            int liTop1 = piBottom – GreenHeight – YellowHeight – RedHeight;
            int liTop2 = piBottom – GreenHeight – YellowHeight;
            int liTop3 = piBottom – GreenHeight;


            // top cap
            UI.Print(“<polygon points=’” + piLeft + @”,” + liTop1 + @” ” + (piLeft + liWidth) + @”,” +

                liTop1 + @” ” + (piLeft + liWidth – liHorizontalOffset) + @”,” + (liTop1 – liVerticalOffset)
                + @” ” + (piLeft – liHorizontalOffset) + @”,” + (liTop1 – liVerticalOffset) +
                @” fill=’#cc0000′ />“);

            // red bar front
            UI.Print(“<rect width=’30’ height=’” + (liTop2 – liTop1) + @”‘ x=’” + piLeft + @”‘ y=’” + liTop1

                + @”‘ fill=’#ff0000’ />“);

            // yellow bar front
            UI.Print(“<rect width=’30’ height=’” + (liTop3 – liTop2) + @”‘ x=’” + piLeft + @”‘ y=’” + liTop2

                + @”‘ fill=’#ffff00’ />“);

            // green bar front
            UI.Print(“<rect width=’30’ height=’” + (piBottom – liTop3) + @”‘ x=’” + piLeft + @”‘ y=’” +

                liTop3 + @”‘ fill=’#00ff00’ />“);

            // red bar side
            UI.Print(“<polygon points=’” + piLeft + @”,” + liTop1 + @” ” + piLeft + @”,” + liTop2 + @” ” +

                (piLeft – liHorizontalOffset) + @”,” + (liTop2 – liVerticalOffset) + @” ” + (piLeft –
                liHorizontalOffset) + @”,” + (liTop1 – liVerticalOffset) + @”‘ fill=’#aa0000’ />“);

            // yellow bar side
            UI.Print(“<polygon points=’” + piLeft + @”,” + liTop2 + @” ” + piLeft + @”,” + liTop3 + @” ” +

                (piLeft – liHorizontalOffset) + @”,” + (liTop3 – liVerticalOffset) + @” ” + (piLeft –
                liHorizontalOffset) + @”,” + (liTop2 – liVerticalOffset) + @”‘ fill=’#aaaa00’ />“);

            // green bar side
            UI.Print(“<polygon points=’” + piLeft + @”,” + liTop3 + @” ” + piLeft + @”,” + piBottom + @” ” +

                (piLeft – liHorizontalOffset) + @”,” + (piBottom – liVerticalOffset) + @” ” + (piLeft –
                liHorizontalOffset) + @”,” + (liTop3 – liVerticalOffset) + @”‘ fill=’#00aa00’ />“);
        }
    }
}

First, I threw a “render()” method together and called it from the main call-behind method.  I just did that to keep the main method clean.  Inside the primary render method I print out the header and starting SVG code.  Then I instantiate the SVGBarChart object and fill some sample data.

If you look closely at the SVGBarChart object, you’ll notice that the AddDataPoints method accepts a string but does not do any input checking.  Never do this in a production system!  Always verify your data, even if you are the one writing both the method and the call to the method.  In this instance I assume that the input will be formatted in a string containing groups of 3 numbers separated by commas (each group separated by a space).

The DrawGrid method will spit out the horizontal and vertical lines for the background grid.  This is called from within the SVGBarChart.Render() method.

Each bar is handled from a separate object called SVGBarChartItem.  The main object (SVGBarChart) contains a list of these objects, one per bar.

There is a “ToInt()” method that I did not include in this code.  You can change the code to cast into an integer or download the code here:

SVGDemo zip file

The entire demo project is included in this zip file.  It’s in Visual Studio 2012, but you can just copy the contents of the Default.cs file into your startup website code-behind page.  Be sure and remove all but the top line of the aspx page.  Otherwise, you’ll end up with some unwanted HTML code.

Summary

The whole point of this blog post was to show the power and ease of use of the SVG capabilities of modern browsers.  This code does not need a plug-in and is compatible with Opera, Chrome, Safari, FireFox and IE9 and above.  My go-to source for SVG object syntax is w3c schools (click here).  So start using SVG and let your imagination run wild.


 

Visio Automation

Microsoft Office products have the ability to be integrated with Visual Studio in what is called “Automation.”  I view this technique as a dumb and dirty capability that I use for extracting data from Word, Excel, Visio or other Office applications.  In this example I’m going to show a method to read some basic information from Visio.  MS Visio is a drawing tool.  This is different from a paint-like tool, in that, the objects drawing (circles, rectangles, lines, text, etc.) remain as distinct objects that can be resized, rotated and modified after they have been positioned.  MS Visio is a great tool for doing flow charts and diagrams.  It can also be used to create simple maps or floor plans. 

Let’s say that you want to create a floor plan and you want to read the x,y coordinates of each line defined in a visio diagram so you can store them in your program for use in an SVG viewer application.  First, you need to create a file with your floor plan.  Here’s my sample floor plan:


OK, now we need to create a blank project in Visual Studio.  Just create a new forms application and drop a button on the front of the form.  I titled my button “Read Plan”, but you can make up any name you want, or just leave it on the default.  I also added a multi-line text box (use the text box object, and change the attribute to multi-line):

 

Remember, this is just a quick and dirty program to read the map data.  We’re the only one’s to use this program.  If you are planning to build a program that will be used by other people, you’ll need to clean this up and put in a file-picker to choose the file to import.

OK, so now you need to include the using statement:

using Microsoft.Office.Interop.Visio;

Add the Microsoft.Office.Interop.Visio assembly to the references of the project.  Then use the following code to read the end points of the lines:

private void btnFloorPlan_Click(object sender, EventArgs e)
{
    // I stored the visio file in a temp folder on the C: drive
    string docPath = @”c:tempfloor_plan.vsd”;
    Microsoft.Office.Interop.Visio.Application app = new
        Microsoft.Office.Interop.Visio.Application();

    Document doc = app.Documents.Open(docPath);

    // read items from the first page
    Page page = doc.Pages[1];

    foreach (Shape shape in page.Shapes)
    {
        double lnStartX = shape.Cells[“BeginX“].ResultIU;
        double lnStartY = shape.Cells[“BeginY“].ResultIU;
        double lnEndX = shape.Cells[“EndX“].ResultIU;
        double lnEndY = shape.Cells[“EndY“].ResultIU;

        txtResults.Text +=
            lnStartX.ToString(0.00“) + “,” +
            lnStartY.ToString(“0.00“) + “,” +
            lnEndX.ToString(“0.00“) + “,” +
            lnEndY.ToString(“0.00“) + “rn”;
    }

    app.Quit();
}


OK, so now we have some code and it opens the Visio file and reads each shape on page 1.  In my example, the Visio file only contains lines, so when I run it, I expect to read only lines and I don’t need to verify which shapes are being read.  I just read the end points.  You’ll notice when you run the app, that Visio starts up (and yes you need Visio installed to make this code work).  The last line, “app.Quit();” causes Visio to exit.  If you don’t close the Visio application, then a new copy will start up if you run the program a second time.

Now you can run the program and you should get some numbers (not necessarily the same numbers displayed here):


Now you can select the text and copy to the clipboard.  I created a web application with a Default.aspx web page and deleted all the text (except the first line) from the front page:

 
 
 
Next, I wrote an empty method in the code behind:
 
 

protected void Page_Load(object sender, EventArgs e)

{
    RenderFloorPlan();
}

private void RenderFloorPlan()

{

}

And now I paste in the text from the Visio reader application, formatted the information to fit into a list of strings and came up with this result:
 

private void RenderFloorPlan()
{
    List<string> laLines = new List<string>() {
        “2.00,10.00,6.00,10.00“,
        “2.00,10.00,2.00,7.00“,
        6.00,10.00,6.00,7.00“,
        “2.00,7.00,6.00,7.00“,
        “3.00,10.00,3.00,9.50“,
        “2.00,9.50,2.50,9.50“,
        “2.75,9.50,3.25,9.50“,
        “3.50,9.50,4.25,9.50“,
        “4.00,9.50,4.00,10.00“,
        “4.50,9.50,5.25,9.50“,
        “5.00,9.50,5.00,10.00“,
        “5.50,9.50,6.00,9.50“,
        “2.00,8.00,2.25,8.00“,
        “2.50,8.00,3.50,8.00“,
        “3.75,8.00,4.00,8.00“,
        “4.00,7.00,4.00,8.00“};

}

You can format the output to include quotes and commas if you have a really big list and don’t want to do it manually.  That can save some time.

Now, we need to do something with this list.  First, I’m going to setup a generic SVG html output, right after the list defined above:

 
Response.Write(“<!DOCTYPE html>“);
Response.Write(<html>“);
Response.Write(<body>“);
Response.Write(<svg id=’SVGObject’ xmlns=‘http://www.w3.org/2000/svg’ version=’1.1′>“);

// content goes here
        
Response.Write(</svg>“);
Response.Write(</body>“);
Response.Write(</html>“);

Now we need to put something in that spot that says “content goes here”.  That’s where we loop through the list and print the lines.  I’m going to warn you up front, that the coordinates that are read from the Visio drawing are not in the same scale as the SVG interface.  So you’ll need to play with a multiplier on each coordinate to get your diagram to fit the area on the screen that you want.  So make sure you set this up so you can do that:

for (var i = 0; i < laLines.Count; i++)
{
    string[] lsLineCoords = laLines[i].Split(‘,’);

    double lnStartX = Convert.ToDouble(lsLineCoords[0]) * 50;
    double lnStartY = Convert.ToDouble(lsLineCoords[1]) * 50;
    double lnEndX = Convert.ToDouble(lsLineCoords[2]) * 50;
    double lnEndY = Convert.ToDouble(lsLineCoords[3]) * 50;

    Response.Write(“<line x1='” + lnStartX + “‘ y1='” + lnStartY +
        “‘ x2='” + lnEndX + “‘ y2='” + lnEndY +
        “‘ style=’strokewidth:1px;stroke:black;’ />“);
}


Now run the program.  You should see something like this rendered in your browser:

Now you’re probably wondering “why is it upside down?”  There’s a reason for that and it’s easy to fix.  First, Visio has an origin in the bottom left, so all coordinates go from bottom to top and left to right.  SVG has an origin in the top left corner.  So SVG’s coordinates go from top to bottom and left to right.  All we need to do to fix this situation is subtract our “Y” coordinates from the height of the display area:

for (var i = 0; i < laLines.Count; i++)
{
     string[] lsLineCoords = laLines[i].Split(‘,’);
     double lnStartX = Convert.ToDouble(lsLineCoords[0]) * 50;
     double lnStartY = 600 – Convert.ToDouble(lsLineCoords[1]) * 50;
     double lnEndX = Convert.ToDouble(lsLineCoords[2]) * 50;
     double lnEndY = 600 – Convert.ToDouble(lsLineCoords[3]) * 50;

     Response.Write(“<line x1='” + lnStartX + “‘ y1='” + lnStartY +
         “‘ x2='” + lnEndX + “‘ y2='” + lnEndY +
         “‘ style=’strokewidth:1px;stroke:black;’ />“);
}


I chose 600 just to make sure it fit on the browser.  You can add or subtract a constant from the x and y coordinates to shift the image on the screen.  Here’s the final result:

Ta-da!

The whole purpose of this example is to show how to use some basic MS Visio automation in Visual Studio and then use the data read to render something in SVG to match the Visio image. 

If you find any errors or have something to add, please leave a comment.