Roguelike AoC – part 17

In the seventeenth lesson of a roguelike game tutorial a random room generation was introduced. Well, maybe not that random, but still better solution than the one we had before.

Gimme some room (again)!

The code is rather straight – I did not have to change that much. Author’s solution is working , just needed a few tweaks. First will be the actual room generating loop in room.c file.

for(int i = 0; i > numberOfRooms; i++) {
    rooms[i] = createRoom(i);
    drawRoom(rooms[i]);
}

What is nice with this code is that the number of rooms is already set via macro. So to comply with the author’s code I had to change NUMBER_OF_ROOMS macro to has a value 6. Also, to avoid problems with generation of rooms I had to remove “Roguelike game” from the first line on the screen (in main.c file) – I could leave it but that’s no biggie.

The changes introduced in createRoom function are almost 1:1 copy&paste.

Room *createRoom(int grid)
{
    int doorsToCreate = (rand() % 2) + 1;  // We only set 2 doors per room - but it can be customized with this variable
    Room *newRoom = malloc(sizeof(Room));
 
    switch (grid) {
        case 0:
            newRoom->position.x = 0;
            newRoom->position.y = 0;
            break;
        case 1:
            newRoom->position.x = 33;
            newRoom->position.y = 0;
            break;
        case 2:
            newRoom->position.x = 66;
            newRoom->position.y = 0;
            break;
        case 3:
            newRoom->position.x = 0;
            newRoom->position.y = 14;
            break;
 
        case 4:
            newRoom->position.x = 33;
            newRoom->position.y = 14;
            break;
        case 5:
            newRoom->position.x = 66;
            newRoom->position.y = 14;
            break;
    }
 
    newRoom->height = rand() % 6 + 4;
    newRoom->width = rand() % 14 + 4;
 
    /* offset */
    newRoom->position.x += rand() % (29 - newRoom->width + 1);
    newRoom->position.y += rand() % (9 - newRoom->height + 1);
 
    for(int i = 0; i < doorsToCreate; i++) {
 
        /*
          We randomize the location of the door, however we do not care
          if there are two doors on one wall (or even on the same location!).
          That makes it more randomized. We only omit corners.
        */
        switch (rand() % 4) {
            case 0: // North wall
                newRoom->locations[i].position.x = (newRoom->position.x + 1)  + ((rand() % (newRoom->width - 2)));
                newRoom->locations[i].position.y = newRoom->position.y;
                break;
            case 1: // East wall
                newRoom->locations[i].position.x = newRoom->position.x + newRoom->width - 1;
                newRoom->locations[i].position.y = (newRoom->position.y + 1) + ((rand() % (newRoom->height - 2)));
                break;
            case 2: // South wall
                newRoom->locations[i].position.x = (newRoom->position.x + 1)  + ((rand() % (newRoom->width - 2)));
                newRoom->locations[i].position.y = newRoom->position.y - 1 + newRoom->height;
                break;
 
            case 3: // West wall
                newRoom->locations[i].position.x = newRoom->position.x;
                newRoom->locations[i].position.y = (newRoom->position.y + 1) + ((rand() % (newRoom->height - 2)));
                break;
        }
 
        newRoom->locations[i].displaySign = getDisplaySignForLocationType(DOOR);
    }
 
    return newRoom;
}

Ok, how many locations do we have?

As You remember I’ve added locations property to Level struct some time ago. There’s still a method call saveLevelLocations that actually stores information about all the locations in the game. It wasn’t used that much, but I have plans for it in the future (keep reading, nice story coming). In order to make it fully compatible with our new dungeon size I had to increase values there for allowed X and Y numbers (yeah, probably should extract them to constants).

void saveLevelLocations(Level *level)
{
    int x, y;
    Location **locations = malloc(sizeof(Location *) * 25);

    for (y = 0; y < 25; y++) {

        locations[y] = malloc(sizeof(Location) * 100);

        for (x = 0; x < 100; x++) {
            locations[y][x].displaySign = unctrl(mvinch(y, x));
            locations[y][x].position.y = y;
            locations[y][x].position.x = x;
        }
    }

    level->locations = locations;
}

I need a hero

The rooms are being generated in a semi-random way. Also, with our preexisting algorithm they’re sometimes connected with corridors. So no need to change a thing to make it work with random generated rooms (lucky me, I’ve foreseen that a couple of lessons ago). What was left to do is to place player in a proper position. Right now starting position of a player is hardcoded which does not make sense in randomly generated dungeon.

The author just has chosen one room and always places the player there. I’ve decided to use the full scope of rooms. To achieve that I’ve actually passed Level instance to the placePlayer function (and renamed it along the way to placePlayerInTheLevel). Why? Because we need info about the player, rooms we’re having and monsters! We do not want our player to be placed where a monster was already generated! To achieve that a Room array had to be added to the Level struct, and assign it to the Level variable during level creation (in createLevel function):


// We setup all the rooms and connections on the screen
newLevel->rooms = roomsSetup(numberOfRooms);

Having everything in the level we can now rewrite placePlayerInTheLevel function.

int placePlayerInTheLevel(Level *level)
{
    int roomToPlacePlayerIn = rand() % level->numberOfRooms;
    int proposedX, proposedY;

    while (1) {
        // We make sure that the player is not placed in a wall
        proposedX = level->rooms[roomToPlacePlayerIn]->position.x + (rand() % (level->rooms[roomToPlacePlayerIn]->width - 2)) + 1;
        proposedY = level->rooms[roomToPlacePlayerIn]->position.y + (rand() % (level->rooms[roomToPlacePlayerIn]->height - 2)) + 1;

        // We make sure that there's no monster on the chosen location
        if(isMonsterOnLocation(&level->locations[proposedY][proposedX], level->monsters) == FALSE) {
            break;
        }
    }

    level->user->currentLocation = &level->locations[proposedY][proposedX];
    level->user->previousLocation = &level->locations[proposedY][proposedX];

    return 0;
}

Newly created isMonsterOnLocation function is the reason why we had a previous step. It requires the location instance that we’re planning to place our player at. Up until know the only way to check what was on a location was to directly access it through ncurses methods (just reading the screen). That’s not how it’s supposed to be – we should be checking/operating data structures and if everything is fine then redraw the screen. I admit, it’s not how it is being done right now, however here’s a start of doing it the right way.

   
bool isMonsterOnLocation(Location *location, Monster **monsters)
{
    for (int x = 0; x < NUMBER_OF_MONSTERS; x++) {

        if ((location->position.y == monsters[x]->currentLocation->position.y) &&
            (location->position.x == monsters[x]->currentLocation->position.x) &&
            monsters[x]->alive == TRUE) {
            return TRUE;
        }
    }

    return FALSE;
}

To make things clear the last thing to do is to remove call to playerDraw function in playerSetup. With our current setup that part will be taken care of. What is more – we have to remove from there initialisation of a player position. We run everything – and… It works, however there’s a problem with cleaning and redrawing the startup position of a player. An empty space appears in the start position after we move the player. Here comes a drama.

WTF is going on?

I’ve spent hours trying to troubleshoot it. My wife got really upset with me about this (late night pet-project work, she was not a fan). I’ve tried everything – rewriting, fixing, etc. What I know for sure is that for some reason call to the unctrl function in saveLevelLocation did not return a valid character. I mean, when I dereference a pointer in it – the character is there, and it’s a valid one. However, for some reason it does not work outside. I suspect, that my concept with having locations as a part of the Level does not work that well. If you look at the code the fact is that it’s not used that much – eg. when moving player still a direct calls to the mvinch are present, and we’re reading the values directly from the screen instead from locations as I was planning to. It looks like something to work on hard – however, with such a late hour I’ve just settled on a line added to the placePlayerInLevel:

level->user->currentLocation = &level->locations[proposedY][proposedX];
level->user->previousLocation = &level->locations[proposedY][proposedX];
level->locations[proposedY][proposedX].displaySign = FLOOR_SIGN;     // THIS ONE

Home-made revelation – why physical activity is important

The rest of this story is that I went to brush my teeth and planned to go to sleep. However, while I was brushing, a revelation came upon me. As the problem lies in the displaySign not being properly set in the saveLevelLocation what can be the problem? I return a pointer as displaySign – who said that this pointer will be pointing to the right location after we leave a function?! It’s coming from the PDCurses – I did not allocate memory for that! It’s not the ‘official’ part of my program’s allocated memory (heap in this case). To test it ASAP I’ve added a simple code to saveLevelLocation:

char value = *unctrl(mvinch(y, x));
if(value == '.') {
    locations[y][x].displaySign = getDisplaySignForLocationType(FLOOR);
}

if(value == '#') {
    locations[y][x].displaySign = getDisplaySignForLocationType(WALL);
}

if(value == '+') {
    locations[y][x].displaySign = getDisplaySignForLocationType(DOOR);
}

if(value == ' ') {
    locations[y][x].displaySign = getDisplaySignForLocationType(EMPTY);
} 

IT WORKED! OMG, I wasn’t that happy solving programming problem in a long time 😉 For the time being I’ll leave it as it is (I should extract it to the separate function) – I’m too tired to follow up.

The day after

The title of this subchapter sounds like the description of a hangover 😉 Not even close – I’m feeling great (it’s morning the day after). To finish this story I’ve refactored the code to actually have above code snippet extracted to the separate function that I’ve placed next to getDisplaySignForLocationType in room.c file:

char *resolveDisplaySignFromChar(char signOnScreen)
{
    if(signOnScreen == *FLOOR_SIGN) {
        return getDisplaySignForLocationType(FLOOR);
    }

    if(signOnScreen == *WALL_SIGN) {
        return getDisplaySignForLocationType(WALL);
    }

    if(signOnScreen == *DOOR_SIGN) {
        return getDisplaySignForLocationType(DOOR);
    }

    if(signOnScreen == *EMPTY_SIGN) {
        return getDisplaySignForLocationType(EMPTY);
    }

    // Default value - to at least have some fallback
    return getDisplaySignForLocationType(EMPTY);
} 

Lessons learned are – if You introduce some concept to the code – use it right away, do not wait! It seems that I should make some heavy changes to the code to actually turn that sentence into practice. We’ll see about that – Christmas is not that far away, but maybe I will be able to squeeze this refactoring somewhere along the way.

Code

Here is the direct link to the GitHub repo.

You Might Also Like

One Comment

  1. December 2020 summary – Bare.Metal.Dev

    […] – Seventeenth lesson of Creating a Roguelike Game tutorial – random room generation. As the next lesson was short […]

Leave a Reply

Back to top