Rogue C# – A Few Random Updates

I had to take a break in the development of the roguelike game to look at other things for awhile. As I get back into it, I decided to check off a couple of minor features and a couple of bugfixes from testing. I think I’m avoiding getting into the magical items like scrolls and potions as I try to find just the right way to code them but that’s definitely next on the list.

“Play Again?”

The ability to start a new game after the player dies is an important one in the game; you don’t want the user to have to close and restart the program each time. I finally decided to get on it and it wasn’t a difficult fix. The code change was on the main form itself.

I refactored the code for starting a new game into a StartGame() method which accepts the player name as an option. It’s called by btnStart_Click() which passes the name from txtName. If there is no name passed, it throws a messagebox asking for one.

private void StartGame(string PlayerName = "")
{
    if (PlayerName.Length > 0)
    {
        currentGame = new Game(txtName.Text);
        listStatus.DataSource = currentGame.StatusList;
        pnlName.Visible = false;
        lblArray.Text = currentGame.ScreenDisplay;
        lblStats.Text = currentGame.StatsDisplay();
    }
    else
        MessageBox.Show("Please enter a name for your character.");
}

DungeonMain_KeyDown() will detect if the GameMode property of the current Game object is set to DisplayMode.GameOver, show the panel with the player name and start button control and fill in the player name from the current game as a default.

Bugfix – Falling off the map

The game’s hallway generator occasionally draws hallways to the edge of the map and this led to a fatal error when my player reached the end of one of these hallways because the SearchAdjacent() function tried to find a space with a X coordinate less than 0.

First, I updated MapLevel.HallwayGeneration() to prevent the drawing of hallways that far.

hallwayLimit = hallwaySpace.X < 2 || hallwaySpace.Y < 2 
    || hallwaySpace.X > MAP_WD || hallwaySpace.Y > MAP_HT;

The hallwayLimit boolean variable is then used as an extra condition when deciding whether to add another space to the hallway if it cannot connect to an existing hallway.

if (!hallwayDug && !hallwayLimit)
{
    // If there's no hallway to connect to, just add another space 
    // where possible for the next iteration to pick up on.
    adjacentChars = SearchAdjacent(EMPTY, hallwaySpace.X, hallwaySpace.Y);
    ...

The larger problem was in the overload of the SearchAdjacent() function used to look ahead when the player is moving. It simply assumed that all adjacent spaces were available and didn’t test the coordinates to see if they were on the map edge like the other overload did. I fixed this and updated references to it to account for the update.

public Dictionary<Direction, MapSpace> SearchAdjacent(int x, int y)
{
    // For each direction, add the existing mapspace if available.
    Dictionary<Direction, MapSpace> retValue = 
        new Dictionary<Direction, MapSpace>();
         
    if (y - 1 >= 0)  // North
        retValue.Add(Direction.North, levelMap[x, y - 1]);

    if (x + 1 <= MAP_WD) // East
        retValue.Add(Direction.East, levelMap[x + 1, y]);

    if (y + 1 <= MAP_HT)  // South
        retValue.Add(Direction.South, levelMap[x, y + 1]);

    if ((x - 1) >= 0)  // West
        retValue.Add(Direction.West, levelMap[x - 1, y]);

    return retValue;
}

The references to this function also needed to take into account that it might not be returning all directions anymore so, somewhere before the reference, something like this would be needed.

if (adjacent.ContainsKey(direct)) ...

This searches the Dictionary that the function returns for the specific member of the MapLevel.Direction enumeration that is to be used.

The Great Refactoring – I missed a spot.

I noticed while testing that monsters and inventory were appearing in empty space on the map, outside of the rooms and hallways. I haven’t been able to get it to happen again and it could have been due to testing but I checked the MapLevel.RoomGeneration() method which generates individual rooms on the map and found that it was still looking at the actual character properties of the MapSpace objects rather than using the MapLevel.PriorityChar() function to determine the content of the space when placing inventory and monsters. That’s been fixed.

public char PriorityChar(MapSpace Space, bool ShowHidden)
{
    Monster? monster = DetectMonster(Space);
    Inventory? invItem = DetectInventory(Space);

    char retValue;

    if (monster != null)
        retValue = monster.DisplayCharacter;
    else if (Space == CurrentPlayer.Location)
        retValue = Player.CHARACTER;
    else if (invItem != null)
        retValue = invItem.DisplayCharacter;
    else if (Space.AltMapCharacter != null && !ShowHidden)
        retValue = (char)Space.AltMapCharacter;
    else
        retValue = (char)Space.MapCharacter;

    return retValue;
}

Monsters and inventory items are now maintained in lists at the MapLevel class level since the major refactoring I did a short while ago so PriorityChar() looks for both in a space and then decides which character to display

Spawning new monsters

New monsters are supposed to be added every so often while a player remains on a level. According to the documentation I found, it’s supposed to happen every so many turns but I decided to add one each time a monster is killed – a reincarnation of sorts. I thought about having a new one spawn every time two monsters collided but the idea of an Emu and a Centaur possibly spawning a Hobgoblin together just weirded me out a little.

First, I refactored the code that was adding monsters as part of the room generation into a new AddMonsters() method. This simplified it as I’m now able to use the MapLevel.GetOpenSpace() function to just get an empty space on the map instead of manually searching for a random one. I specified True for the function parameter to indicate that a hallway space was acceptable.

public void AddMonsters(int Number)
{
    Monster? spawned;
    MapSpace itemSpace;

    // Pick random monsters until its probability of appearing is 
    // within the random limit generated.
    for (int i = 1; i <= Number; i++)
    {
        do
        {
            spawned = Monster.SpawnMonster(CurrentLevel);
        } while (spawned != null && rand.Next(1, 101) <= spawned.AppearancePct);

        // Place monster on map.
        if (spawned != null)
        {
            itemSpace = GetOpenSpace(true);
            spawned.Location = itemSpace;
            ActiveMonsters.Add(spawned);
        }
    }
}

The initial spawning of monsters was previously done during the generation of each room but I looked back and couldn’t see a good reason it had to be done that way so now it’s done just after the generation of hallways in the MapGeneration() method. This means that monsters might double up in rooms but that’s okay, they can share. As shown above, the AddMonsters() method accepts an argument for the number of monsters to be created so I can add nine at the beginning and then one as needed. Later in development, I might decide that two or three monsters need to be spawned under certain conditions.

To spawn the new monsters, I simply added a couple of lines in the Game.Attack() method after the player defeats the monster.

if(Defender.CurrentHP < 1)
{
    CurrentMap.ActiveMonsters.Remove(Defender);
    UpdateStatus($"You defeated the {Defender.MonsterName.ToLower()}.", false);
    CurrentPlayer.Experience += Defender.ExpReward + (int)(Defender.MaxHP / 6);                
    CurrentMap.AddMonsters(1);
}

Apple AirTag Tracking Device, 4-Pack with 1-Year Apple Warranty
  • Track & Locate with Find My Network
  • Use with Keys, Wallet, Bags & More
  • Find Items by Asking Siri
  • Over a Year of Battery Life
  • Standard Battery is Replaceable
ComeauSoftware.com uses affiliate links through which we earn commissions for sales.

Sign up for our newsletter to receive updates about new projects, including the upcoming book "Self-Guided SQL"!

We respect your privacy and will never share your information with third-parties. See our privacy policy for more information.

×