7
\$\begingroup\$

I decided to make my own dungeon generator, after reading a bit about it. This is my first effort. I can only say that it works, but I believe there's room for some improvement, because I believe my self-taught coding skills are basic.

To use this code, call:

dungeonGenerator = new DungeonGenerator();
dungeonGenerator.CreateDungeonRoom(115, 32);
dungeonGenerator.CellsToGenerate = 1500;
dungeonGenerator.CreateDungeonScenery();
dungeonGenerator.LogDungeon();

Tile.cs

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

namespace DungeonGenerator
{
    struct Cell
    {
        /// <summary>
        /// Location in the grid
        /// </summary>
        public CellType CellType;

        public Cell(CellType cellType)
        {
            CellType = cellType;
        }

        public override string ToString()
        {
            return "Type: " + CellType;
        }
    }
}

CellType.cs

    /// <summary>
     /// If the cell is walkable or not
     /// </summary>
    public enum CellType
    {
        WALL, GROUND, START
    }

Direction.cs

enum Direction
{
   UP, DOWN, LEFT, RIGHT
}

DungeonGenerator.cs

using System;

namespace DungeonGenerator
{
    public class DungeonGenerator
    {
        public int Width            { get; set; }
        public int Height           { get; set; }
        public int CellsToGenerate  { get; set; }
        Cell[,] DungeonCells    { get; set; }

        public void CreateDungeonRoom(int RoomWidth, int RoomHeight)
        {
            Width = RoomWidth;
            Height = RoomHeight;
            DungeonCells = new Cell[Width,Height];

            for (int y = 0; y < Height; y++)
            {
                for (int x = 0; x < Width; x++)
                {
                    CellType cellType = CellType.WALL;
                    DungeonCells[x, y] = new Cell(cellType);
                }
            }
        }
        /// <summary>
        /// Populate the insides of the room
        /// </summary>
        public void CreateDungeonScenery()
        {

            //Make it so that the outside walls are untouched
            int minValueXY = 1;
            int maxValueX = Width - 2;
            int maxValueY = Height - 2;

            //Choose a random position in the room
            Random random = new Random();
            int startX = random.Next(minValueXY, maxValueX);
            int startY = random.Next(minValueXY, maxValueY);

            //Mark it as the starting position, for player placement, maybe?
            DungeonCells[startX, startY] = new Cell(CellType.START);


            //Get directions to an array for random choosing
            Array values = Enum.GetValues(typeof(Direction));

            //From the starting position, proceed to create X number of ground cells
            int cellCount = 0;
            while (cellCount < CellsToGenerate)
            {
                //Choose a direction at random
                Direction direction = (Direction)values.GetValue(random.Next(values.Length));

                if (direction == Direction.UP)
                {
                    startY -= 1;
                    if (startY < minValueXY) { startY = minValueXY; }
                }
                else if (direction == Direction.DOWN)
                {
                    if (startY < maxValueY) { startY += 1; }
                }
                else if (direction == Direction.LEFT)
                {
                    startX -= 1;
                    if (startX < minValueXY) { startX = minValueXY; }

                }
                else if (direction == Direction.RIGHT)
                {

                    if (startX < maxValueX) { startX += 1; }
                }

                //From the position chosen, mark it as ground, if possible
                if (CreateGround(startX, startY))
                {
                    //Mark the cell as ground
                    DungeonCells[startX, startY].CellType = CellType.GROUND;
                    //Add one to cells created
                    cellCount++;
                }
            }
        }

        private bool CreateGround(int startX, int startY)
        {
            //There's not a wall there, so there's nothing to be done here
            if (DungeonCells[startX, startY].CellType != CellType.WALL)
            {
                return false;
            }

            return true;
        }

        public void LogDungeon()
        {
            Console.Clear();

            for (int y = 0; y < Height; y++)
            {
                string line = "";
                for (int x = 0; x < Width; x++)
                {
                    if (DungeonCells[x, y].CellType == CellType.GROUND)
                    {
                        line += "O";
                    }
                    else if (DungeonCells[x, y].CellType == CellType.WALL)
                    {
                        line += "█";
                    }
                    else if (DungeonCells[x, y].CellType == CellType.START)
                    {
                        line += "H";
                    }
                }
                Console.WriteLine(line);
            }
        }

    }
}

From the start, I realize I can improve the code:

By preventing backtracking, but backtracking sometimes makes some nice crevices here and there. Resolve this by making impossible to select the direct opposite of the previous direction? I want to figure out a way to improve that direction choosing part (currently I don't know how).

On purpose, I left the number of cells to generate out of the CreateDungeonRoom() method because I'm still deciding if I really want to make users insert the number of ground tiles as a specific number or a percentage, or enum. We will see.

Here is an example of a dungeon with 115 width, 32 height and 1500 ground tiles and the player starting position (it's the H somewhere) enter image description here

\$\endgroup\$
  • \$\begingroup\$ (Welcome to CR!) (as incisive as [needed] beats indecisive almost every time - or nearly? Almost always? Often? Arrghh…) \$\endgroup\$ – greybeard Feb 10 '18 at 18:45
  • \$\begingroup\$ Could you add an example how to run it please? \$\endgroup\$ – t3chb0t Feb 13 '18 at 15:14
  • 1
    \$\begingroup\$ Sure thing, though I mentioned it in a comment below, there sure is no point in leaving it out. Thanks for the heads up. \$\endgroup\$ – José Teixeira Feb 13 '18 at 19:04
  • \$\begingroup\$ Oops, I didn't see that comment. Thx for adding it anyway. Now it's working ;-) \$\endgroup\$ – t3chb0t Feb 13 '18 at 19:48
6
\$\begingroup\$

Clean Your Code

  • Convert if-else ladder to switch. Switch is much faster.
  • startY -= to startY--
  • Same goes for LogDungeon() method

Before

if (direction == Direction.UP)
{
    startY -= 1;
    if (startY < minValueXY) { startY = minValueXY; }
}
else if (direction == Direction.DOWN)
{
    if (startY < maxValueY) { startY += 1; }
}
else if (direction == Direction.LEFT)
{
    startX -= 1;
    if (startX < minValueXY) { startX = minValueXY; }

}
else if (direction == Direction.RIGHT)
{

    if (startX < maxValueX) { startX += 1; }
}

After

switch (direction)
{
    case Direction.UP:
        startY--;
        if (startY < minValueXY)
            startY = minValueXY;
        break;
    case Direction.DOWN:
        if (startY < maxValueY)
            startY++;
        break;
    case Direction.LEFT:
        startX--;
        if (startX < minValueXY)
            startX = minValueXY;
        break;
    case Direction.RIGHT:
        if (startX < maxValueX)
            startX++;
        break;
}

Simplify Methods & Conditions

Before

private bool CreateGround(int startX, int startY)
{
    //There's not a wall there, so there's nothing to be done here
    if (DungeonCells[startX, startY].CellType != CellType.WALL)
    {
        return false;
    }

    return true;
}

After

private bool CreateGround(int startX, int startY) =>
    DungeonCells[startX, startY].CellType == CellType.WALL;

Observations

Place the following code outside of while loop if you don't really need it inside the loop.

int cellCount = 0;
while (cellCount < CellsToGenerate)
{
    //Choose a direction at random
    Direction direction = (Direction)values.GetValue(random.Next(values.Length))
    switch (direction)
    {
        case Direction.UP:
            startY--;
            if (startY < minValueXY)
                startY = minValueXY;
            break;
        case Direction.DOWN:
            if (startY < maxValueY)
                startY++;
            break;
        case Direction.LEFT:
            startX--;
            if (startX < minValueXY)
                startX = minValueXY;
            break;
        case Direction.RIGHT:
            if (startX < maxValueX)
                startX++;
            break;
    }

}

Suggestions

  • Please avoid using global variable/properties inside a class. Instead use parameters to pass values inside method and make use of return type. If you need to return multiple values, create a new type and return it. This will help avoid confusion.
  • Use correct naming guidelines. You have some incorrect naming conventions in the CreateDungeonRoom method. Parameter names should always be in camelCase.

Hope this helps

\$\endgroup\$
  • \$\begingroup\$ Hello SiD, Thank you very much for you time! May I ask for a bit of clarification on the first suggestion? I don't see what needs to be done, mostly because I'm still getting to grips with some lingo. Also, after replacing the if clause for Switch, I don't see that big of a difference, but that might be because of the random events, as there are no two equal dungeons. \$\endgroup\$ – José Teixeira Feb 12 '18 at 11:23
  • 1
    \$\begingroup\$ Hi @JoséTeixeira, In your code properties aren't accessed by any external class. It is accessed by class itself from multiple locations. If code grows, it will be hard to keep track of which piece of code updated the values, it is hard to debug, have tight coupling etc. Hence it is good to have scoped variables. And yes, that's correct you won't see much difference if you use switch. But these are the best practises which help you in longer run. \$\endgroup\$ – SiD Feb 12 '18 at 11:35
  • \$\begingroup\$ Hm... I see it now. Of course, you are correct. But, if I access this from another class, is this correct? I use this outside my main project file, and call if from there (this is a project in the solution). I guess I could simplify it further, though. I currently call it from my MonoGame project like this: 'dungeonGenerator.CreateDungeonRoom(115, 32); dungeonGenerator.CellsToGenerate = 1500; dungeonGenerator.CreateDungeonScenery(); dungeonGenerator.LogDungeon();' \$\endgroup\$ – José Teixeira Feb 12 '18 at 12:26
  • 1
    \$\begingroup\$ You can also improve your UP and LEFT blocks by checking the value of the variable before changing it, rather than changing it and then checking. For example: case Direction.UP: if (startY > minValueXY) startY--; break; \$\endgroup\$ – Rufus L Feb 12 '18 at 17:51
  • 1
    \$\begingroup\$ @JoséTeixeira, Yes that's okay, methods and properties are usually called by another class or even project. I think you should goto GitHub and check some projects out there. This will give you idea how projects are maintained and most of your queries will get resolve. \$\endgroup\$ – SiD Feb 13 '18 at 5:23
4
\$\begingroup\$

Naming

DungeonGenerator, to me, would mean that it is a class that generates a Dungeon. But the way you use it, DungeonGenerator is itself, a Dungeon. I would drop the "Generator" part of the name, or refactor it so it is in-fact a dungeon generator.

Additionally you have a DungeonGenerator namespace and a DungeonGenerator class within that namespace. I would avoid having a class the same name as its containing namespace. Otherwise you need to write confusing things like:

var x = new DungeonGenerator.DungeonGenerator(); 

Avoid Mutable Properties

Your DungeonGenerator has a few externally mutable properties that I don't see a reason for them to be mutable. It would be better if these weren't mutable:

public class DungeonGenerator
{
    public int Width { get; private set; }
    public int Height { get; private set; }
    public int CellsToGenerate { get; private set; }
    private Cell[,] DungeonCells;

    public void CreateDungeonRoom(int RoomWidth, int RoomHeight) { ... }
    /// <summary>
    /// Populate the insides of the room
    /// </summary>
    public void CreateDungeonScenery() { ... }

    private bool CreateGround(int startX, int startY) { ...}

    public void LogDungeon(){ ... }
}

Additionally, DungeonCells doesn't really need to be a property at all, so just make it a private field.

Consistent Documentation

You documented the CreateDungeonScenery method, but nothing else that is public. You should be "all-in" with documentation on methods, and make it descriptive (don't just re-word the method name). If the method has side-effects, mention them in the <remarks></remarks> section.

Consistent Method Parameter Names

You got the names in CreateGround as camelCased but you have the names in CreateDungeonRoom as PascalCased. According to the .NET Naming Guidelines you should be using PascalCased for method/property names and camelCased for method parameters.

Code Use

I don't see any reason why you would want to create a new DungeonGenerator and not run CreateDungeonRoom, so why not make that into the constructor?

public class DungeonGenerator
{
    public int Width { get; private set; }
    public int Height { get; private set; }
    public int CellsToGenerate { get; private set; }
    private Cell[,] dungeonCells;

    public void DungeonGenerator(int roomWidth, int roomHeight, int cellsToGenerate) { ... }
    /// <summary>
    /// Populate the insides of the room
    /// </summary>
    public void CreateDungeonScenery() { ... }

    private bool CreateGround(int startX, int startY) { ...}

    public void LogDungeon(){ ... }
}

Additionally the constructor can call the CreateDungeonScenery method as well. So now instead of the original way, you can consolidate code:

var dungeon = new DungeonGenerator(115, 32, 1500);

Method Name

The LogDungeon method name is confusing, because the word "Log" to most developers means some kind of diagnostic routine that writes data to a file or console purely for debugging purposes. You are using it to take over the console (by clearing it) and output the text graphic of the dungeon to the console. I think a better name would be something like FlushToConsole or something like that.

However I think you shouldn't be having Console methods anywhere in this class, let the user decide what to do with it. If a class has a valid string representation, you should be overriding the ToString() method instead, so LogDungeon becomes ToString():

        public override void ToString()
        {
            StringBuilder dungeon = new StringBuilder();
            for (int y = 0; y < Height; y++)
            {
                StringBuilder line = new StringBuilder();
                for (int x = 0; x < Width; x++)
                {
                    switch (DungeonCell[x, y].CellType)
                    {
                        case CellType.GROUND: 
                            line.Append("O"); break;
                        case CellType.WALL:
                            line.Append("█"); break;
                        case CellType.START:
                            line.Append("H"); break;
                    }
                }
                dungeon.AppendLine(line.ToString());
            }
            return dungeon.ToString();
        }

Now the user of your class can decide to do something else with it, such as write it to a file, send it over the network, print it on a webpage, etc. The user is no longer tied to the console.

\$\endgroup\$
  • \$\begingroup\$ Awesome reading material! I'll start working on it as soon as I can! I knew it would be worth it to show the code around, but wasn't expecting such a wealth of information. I am very grateful for your input. \$\endgroup\$ – José Teixeira Feb 13 '18 at 21:48
2
\$\begingroup\$

Some general comments about how your code "reads":

Usage

You wrote: "To use this code, call:"

dungeonGenerator = new DungeonGenerator();
dungeonGenerator.CreateDungeonRoom(115, 32);
dungeonGenerator.CellsToGenerate = 1500;
dungeonGenerator.CreateDungeonScenery();
dungeonGenerator.LogDungeon();

First, let me say that I support the idea that the dungeon generator and the dungeon object should be different. The code to randomly create rooms has nothing to do with the code that decides whether a user has stepped on a trap, etc. So I like the idea of a separate class/object/function.

That said, this is a very awkward set of calls. It doesn't "scan" well (to use a term from literature). I suspect that all these fine details need to be wrapped up into a single factory function that a user can call:

cavern = Dungeon.Generate(width: 115, height: 32, min_cells: 1500);
cavern.WriteTo(Console.Out);

CellType

Your cell type is an enumeration, which seems obvious but actually isn't very useful. The thing you mainly do with the cell type is display it, and sadly C# enums cannot have char as their underlying type. But you can work around that using an int and just assigning char values:

public enum CellType 
{
    WALL = '█', 
    GROUND = 'O',
    START = 'H'
};

Just be careful about what size characters you use (16 bit only). If you have to use a full-out string, consider the solution in this answer.

I suggest this because your output code would benefit from having "concrete" values. (The rest of your code is happy with the CellType being an abstract enum.)

                switch (DungeonCell[x, y].CellType)
                {
                    case CellType.GROUND: 
                        line.Append("O"); break;

Note: As a rule of thumb, if you find yourself executing a switch on internal data it's a good indicator that you should consider a new type. Switching on external data might be a correct way to deal with user input. But switching on internal data frequently means "these things should be different objects, or different classes". In this case, the enum values are serving as proxies for objects that have their own output string representation.

It would be better, IMO, to simply emit the value, or a method of the value (as in the linked answer):

line.Append(DungeonCell[x, y].cellType);

// -or-

line.Append(DungeonCell[x, y].cellType.ToFriendlyString());

(but see below about that x, y thing!)

Direction

Direction is another enum that doesn't quite do what you need. Currently, you write code like this (code modified for size):

//Choose a direction at random
Direction direction = (Direction)values.GetValue(random.Next(values.Length));

if (direction == Direction.UP)
    if (startY > minValueXY)
        --startY;
else if (direction == Direction.DOWN)
    if (startY < maxValueY) 
        ++startY;
else if (direction == Direction.LEFT)
    if (startX > minValueXY)
        --startX;
else if (direction == Direction.RIGHT)
    if (startX < maxValueX)
        ++startX;

//From the position chosen, mark it as ground, if possible

Instead of making Direction an enum, what if you make it a class? You can then have Direction.AtRandom() return what you want without having to cast it.

What's more, consider what you are doing: you immediately decode the direction into an X or a Y offset. You then either adjust the variable startX or startY.

Why are those two variables different? What is startX? What is startY? Aren't they part of a greater whole, called Position (or Location or Coords something)?

If you had a Position type, you could just have start instead of startX and startY. You might have to adjust start.X and start.Y, but probably not, because Direction should be a vector.

If Direction.North is a Vector defined as { δx = 0, δy = -1 } then you can define addition of a Position and a Vector in the obvious way (x+δx, y+δy) and then simplify your code:

public static Position operator +(Position pos, Vector v)
{
    return new Position(pos.x + v.δx, pos.y + v.δy);
}

Then:

// Choose a direction at random (but don't go out-of-bounds)

dir = Direction.AtRandom();

if (dungeon.Contains(start + dir))
    start += dir;

// From the position chosen, mark it as ground, if possible

Note: .Contains may be the wrong name, since there is that implicit rule about not modifying the outer edges. Maybe "CanBeRoom()"?

Position

If you define an indexer for your Dungeon, you can use the Position directly instead of reaching in for the x and y values. This will cost some performance, but that likely doesn't matter during generation. Alternatively, you might just write a Dungeon.At(Position) method.

Dungeon Rules

I saw one "implicit" rule in your code: the dungeon's outside walls are inviolable. I'd suggest that you make those rules explicit, and encode the operations you are currently implementing with just a paragraph or two of code:

//Make it so that the outside walls are untouched
int minValueXY = 1;
int maxValueX = Width - 2;
int maxValueY = Height - 2;

//Choose a random position in the room
Random random = new Random();
int startX = random.Next(minValueXY, maxValueX);
int startY = random.Next(minValueXY, maxValueY);

This can be replaced by defining methods:

topLeft = dungeon.PlayerTopLeft();      // returns a Position, (1,1)?
botRight = dungeon.PlayerBottomRight(); // returns a Position.

start = Position.AtRandom(min: topLeft, max: botRight);

Iterators

I'd suggest writing at least one position iterator for the Dungeon. Something like Dungeon.PositionsInViewOrder(), that would yield the cell positions in the right order for display.

(Note: if Cell objects knew their own Position, I'd suggest just writing a Cell iterator and skipping the Position. But as things are, this is the way to go.)

You could rewrite your LogDungeon function as a Dungeon method:

public void WriteTo(System.IO.TextWriter out)
{
    // Only care about Y
    int last_pos = TopLeft() + Vector(δx:0, δy:-1); 

    for (Position pos in PositionsInViewOrder())
    {
        if (last_pos.y != pos.y)
            out.WriteLine();

        last_pos = pos;
        out.Write(dungeon[pos].cellType);
    }

    out.WriteLine();
}

You might find that many dungeon creation tasks are made easier by defining Position iterators. For example, if you want to randomly generate rooms and hallways you could define Position.WithinRect(topLeft:Position, bottomRight:Position) and Position.AlongLine(from:Position, to:Position). (Be consistent on how you handle the end position: inclusive or exclusive!)

\$\endgroup\$
  • \$\begingroup\$ Thank you Austin! You have input that I have, frankly, a bit of a hard time understanding but that's what learning is about. Also, there's some serious code refactoring to be done, I liked particularly the proposed CellType refactoring. I wouldn't know it if you hadn't posted it. The dungeon rules part is very elegant. Great explanation. I am humbled and motivated to continue further. \$\endgroup\$ – José Teixeira Feb 14 '18 at 11:32
  • 1
    \$\begingroup\$ Feel free to ask about the parts you don't understand. I can expand the answer if needed. \$\endgroup\$ – Austin Hastings Feb 14 '18 at 12:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.