Wednesday, July 27, 2011

Thoughts dealing with self contained dungeon features

   Recently I have been rereading Ascii Dreams a blog by the creator of Unangband and I ended up thinking quite a bit about some of what I read. As it pertains to this post I was thinking on what I had read in This article which is part 6 of 9 on how Unangband's dungeon is generated. Particularly I was interested in the idea of complex dungeon features being achieved by simple self contained states. An example from the post is that there is regular Water which when hit by something that can hurt water causes it to transition to another state, the crest of a way. The crest in turn hurts all adjacent water tiles and transitions into a wave then next turn into rough water which does nothing except transition into Water. combining this all together and you get dynamic waves that don't need to store any kind of information past what they are. While not that realistic of a water system it does a good enough job without all the hassle of trying to figure out stuff like how to combine flows and water pressure.
   I in an uncharacteristic bout of whimsy programming set up a little console demo thing. Nothing complex but I was going for base level simplicity anyway. I started it to find out what a fire affect would look like. The rules for the fire was that if a space in a cardinal direction was empty the space became fire and the current fire tile transitions to smoke. Smoke does nothing but transistion to empty space but serves the same purpose that the rough water did, IE a buffer zone so the fire does not double back on itself.
   Of course I made a mistake in the code that would check if a space was empty and did not realize it so I made light smoke which I had smoke transition to. That was the beggining of my sinking more then four hours into something that technicly could have been done in one. You see the smoke was interesting. Sure all it did was transition turn by turn till it was gone but each state only took three lines of code in a simple Switch, one of  which was for the break. The code went like so:
      case '▒':
         NewMap[x,y] = '░'
         break;
Now I will admit I am a little over exuberant about all this and it is mostly old new and what not but still I am posting it because it was amusing for me to fool around with. Anyway the next thing I whipped up was A weird arrow thing that I can't really remember what I did except that because of how I cycled through the tiles it ended up having artifacts and a big fondness for diagonally downwards arrows and the whole thing ended up being transformed into the current form.
   As I mentioned I like how the smoke worked and wanted to do more with it so I thought of it in roguelike terms. How could I incorporate the spreading of smoke as a goal in and of itself. The easy answer would have been magic so I decided to try a different route. Smoke arrows was what I finally came up with. What good would they be? Why smoke obscures line of sight and maybe even light. The only problem I had was that the simple smoke I had lasted at most three turns. Of course while in flight the arrow itself puts out smoke so there is a trail but and seeing as I did not really plan on doing anything big with it the arrows have infinite range seeing as all they do is check if the space in the direction they are facing is empty and going there if so. It still did not leave a smoke trail like I wanted so I did something more complex. Well calling it complex is a little overstating it. All I did was have the tile be a number going from 6 to 1 and when the console actually writes out the screen I have it put '░' for 1 and 2, '▒' for 3 and 4, and '▓' for 5 and 6. I only applied this to the smoke left directly in the arrows path and it created a good looking affect.
   Now for the specifics on the actual code. The map itself is held in an char array called CurrentMap and is 35 characters wide and 21 characters tall though because of asthetics I only have the actual game space take up 35 by 19 leaving the bottom two rows blank where in a roguelike the stats and such would be. I then use a for loop nested in a for loop to fill in the NewMap array with what the game space will look like. All this currently means is that it puts a wall around the game space made of '#' and fills the interior with spaces. After that a few thing including the code that resizes the actual console size and buffer as well as a line that puts a character in the middle of the map so I can test the functionality of whatever I am working on.
   The main game loop is simple enough, just a do while loop that continues until you press the escape key. Inside the loop the first thing is once again a for loop nested in a for loop which lets me cycle through all the characters stored in CurrentMap and put them through a Switch with a case for each of the currently implemented tiles. Because most of them are just smoke and the ones that are not I have elsewhere in the code most of the cases take three lines if that. Then surprise, surprise more for loops though this set is important as its the place we put all the characters on the screen from. First there is an If statement that checks whether CurrentMap[x, y] != NewMap[x, y] so I don't end up redrawing the whole game area every time and only change what has actually changed. Then it has the bit of code that changes the numbers into smoke, just another switch. Finally the program makes the CurrentMap equal the NewMap and thats all there is to the main loop.
   Of course thats not all the code though. I have most of the functionality of the various things in methods. I have one for how fire spreads, one to write what I want where I want, one for all the smoke arrow directions all in one, and finally two I should probably merge together because the only difference is one checks if the  spaces next to a tile in the cardinal directions are clear and the other checks the diagonal directions. Both the checks return a bool array so I can just declare it 'i' and do If (i[0]). The only thing really stopping me right now is after I removed the weird arrow thing I did not use the diagonal check for anything so having to check four extra spaces did not make much sense.
   The full code as of now is below and its not really commented that well as I only did what I needed to have right when I was coding it. I should have more even though most of it is really simple if only for the fact I know I will forget something import later when I come back to it. If you have any questions just ask in the comments. Also if you see any errors on my part don't keep it to yourself and snigger at my incompetence, point it out in the comments so others can avoid my mistakes.



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Testing_with_Squares
{
    class Program
    {
        static char[,] CurrentMap = new char[35, 21];
        static char[,] NewMap = new char[35, 21];
        static void Main(string[] args)
        {
            for (int x = 0; x < 35; x++)
            {
                for (int y = 0; y < 19; y++)
                {
                    if ((x == 0) || (x == 34) || (y == 0) || (y == 18))
                    {
                        NewMap[x, y] = '#';
                    }
                    else
                    {
                        NewMap[x, y] = ' ';
                    }
                }
            }
            Console.SetWindowSize(35, 21);
            Console.SetBufferSize(35, 21);
            Console.CursorVisible = false;
            NewMap[17, 9] = '>';
            ConsoleKeyInfo cki = new ConsoleKeyInfo();
            do
            {
                cki = Console.ReadKey(true);
                for (int x = 0; x < 35; x++)
                {
                    for (int y = 0; y < 21; y++)
                    {
                        switch (CurrentMap[x, y])
                        {
                            case '*':
                                FireSpread(x, y);
                                break;
                            case '1':
                                NewMap[x, y] = ' ';
                                break;
                            case '2':
                                NewMap[x, y] = '1';
                                break;
                            case '3':
                                NewMap[x, y] = '2';
                                break;
                            case '4':
                                NewMap[x, y] = '3';
                                break;
                            case '5':
                                NewMap[x, y] = '4';
                                break;
                            case '6':
                                NewMap[x, y] = '5';
                                break;
                            case '░':
                                NewMap[x, y] = ' ';
                                break;
                            case '▒':
                                NewMap[x, y] = '░';
                                break;
                            case '▓':
                                NewMap[x, y] = '▒';
                                break;
                            case '<':
                            case '>':
                            case '^':
                            case 'v':
                                SmokeTravel(x, y);
                                break;
                            default:
                                break;
                        }
                    }
                }
                for (int x = 0; x < 35; x++)
                {
                    for (int y = 0; y < 21; y++)
                    {
                        if (CurrentMap[x, y] != NewMap[x, y])
                        {
                            switch (NewMap[x, y])
                            {
                                case '6':
                                case '5':
                                    Write(x, y, '▓');
                                    break;
                                case '4':
                                case '3':
                                    Write(x, y, '▒');
                                    break;
                                case '2':
                                case '1':
                                    Write(x, y, '░');
                                    break;
                                default:
                                    Write(x, y, NewMap[x, y]);
                                    break;
                            }
                            CurrentMap[x, y] = NewMap[x, y];
                        }
                    }
                }
            } while (cki.Key != ConsoleKey.Escape);
        }

        static void Write(int x, int y, char c)
        {
            Console.SetCursorPosition(x, y);
            Console.Write(c);
        }

        static void FireSpread(int x, int y)
        {
            NewMap[x, y] = '░';
            bool[] i = CardinalCheck(x, y);   // 0 = ^, 1 = >, 2 = v, 3 = <
            if (i[0])
            {
                NewMap[x, y - 1] = '*';
            }
            if (i[1])
            {
                NewMap[x + 1, y] = '*';
            }
            if (i[2])
            {
                NewMap[x, y + 1] = '*';
            }
            if (i[3])
            {
                NewMap[x - 1, y] = '*';
            }
        }
       
        static void SmokeTravel(int x, int y)
        {
            NewMap[x, y] = '6';
            bool[] i = CardinalCheck(x, y);   // 0 = ^, 1 = >, 2 = v, 3 = <
            switch (CurrentMap[x, y])
            {
                case '<':
                    if (i[3])
                    {
                        x--;
                        NewMap[x, y] = '<';
                        i = CardinalCheck(x, y);
                    }
                    break;
                case '>':
                    if (i[1])
                    {
                        x++;
                        NewMap[x, y] = '>';
                        i = CardinalCheck(x, y);
                    }
                    break;
                case '^':
                    if (i[0])
                    {
                        y--;
                        NewMap[x, y] = '^';
                        i = CardinalCheck(x, y);
                    }
                    break;
                case 'v':
                    if (i[2])
                    {
                        y++;
                        NewMap[x, y] = 'v';
                        i = CardinalCheck(x, y);
                    }
                    break;
                default:
                    break;
            }
            switch (NewMap[x, y])
            {
                case '<':
                case '>':
                    if (i[0]) { NewMap[x, y - 1] = '▓'; }
                    if (i[2]) { NewMap[x, y + 1] = '▓'; }
                    break;
                case '^':
                case 'v':
                    if (i[1]) { NewMap[x + 1, y] = '▓'; }
                    if (i[3]) { NewMap[x - 1, y] = '▓'; }
                    break;
                default:
                    break;
            }
        }

        static bool[] CardinalCheck(int x, int y)
        {
            bool[] i = new bool[4];   // 0 = ^, 1 = >, 2 = v, 3 = <
            if ((CurrentMap[x, y - 1] == ' ') && (NewMap[x, y - 1] == ' '))
            { i[0] = true; }
            if ((CurrentMap[x + 1, y] == ' ') && (NewMap[x + 1, y] == ' '))
            { i[1] = true; }
            if ((CurrentMap[x, y + 1] == ' ') && (NewMap[x, y + 1] == ' '))
            { i[2] = true; }
            if ((CurrentMap[x - 1, y] == ' ') && (NewMap[x - 1, y] == ' '))
            { i[3] = true; }
            return i;
        }

        static bool[] DiagonalCheck(int x, int y)
        {
            bool[] ii = new bool[4];   // 0 = ┌218, 1 = ┐191, 2 = ┘217, 3 = └192
            if ((CurrentMap[x - 1, y - 1] == ' ') && (NewMap[x - 1, y - 1] == ' '))
            { ii[0] = true; }
            if ((CurrentMap[x + 1, y - 1] == ' ') && (NewMap[x + 1, y - 1] == ' '))
            { ii[1] = true; }
            if ((CurrentMap[x + 1, y + 1] == ' ') && (NewMap[x + 1, y + 1] == ' '))
            { ii[2] = true; }
            if ((CurrentMap[x - 1, y + 1] == ' ') && (NewMap[x - 1, y + 1] == ' '))
            { ii[3] = true; }

            return ii;
        }
    }
}

No comments:

Post a Comment