Roguelike AoC – part 7
A seventh lesson of roguelike game tutorial builds up on the previous one. We have created the doors, now it is time to connect them.
Can (and should) I do better?
The concept used in this lesson works not that bad. To make it a little more compliant with my existing code I’ve started small – with addition of next constant sign and new location type:
const char *EMPTY_SIGN = " ";
enum LocationType {FLOOR, WALL, DOOR, EMPTY};
We also have to add it to the switch statement in getDisplaySignForLocationType function.
case EMPTY:
return EMPTY_SIGN;
break;
Next thing is to define an integer constant with information of how many locations-array in the Room struct can hold. Right now it’s hardcoded to 4 – we make it more descriptive constant;
#define ALLOWED_ROOM_LOCATIONS_NUMBER 4
typedef struct Room {
Position position;
int height;
int width;
Location locations[ALLOWED_ROOM_LOCATIONS_NUMBER];
} Room;
Now, there are two problems to solve. First one is related to the fact, that we do not use hardcoded door locations. I mean, every game run we have doors generated, however, we do not know how many of them are there. So in the original code there is no problem with providing connectDoors function with already existing positions of the doors. In my solution that is not possible – first we have to choose doors to connect from the existing rooms and doors that are available. That’s problem number one.
The second thing I wanted to deal with was to avoid a situation, in which a part of wall is being destroyed(overridden with floor sign) when connecting the doors. It is technically handled by checks that original code does (not to go through walls), but it results in sometimes failure to actually connect the doors. However, I’ve taken a sneak peek at the next lessons, and it seems that topic of connecting rooms (and bugs in it) are discussed later, so I’ve decided to just copy a current algorithm and add nothing new. In general, the topic of generating dungeons in roguelikes is huge, and obviously we won’t be making a thesis here about it. So for the time being – I’ve just used whatever author coded and don’t care about the current result. Sometimes the rooms are connected, usually second connection does not work.
Code
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <curses.h>
#include <time.h>
#define ALLOWED_ROOM_LOCATIONS_NUMBER 4
// Constants
const char *WALL_SIGN = "#";
const char *FLOOR_SIGN = ".";
const char *PLAYER_SIGN = "@";
const char *DOOR_SIGN = "+";
const char *EMPTY_SIGN = " ";
//
// Structs
typedef struct Position {
int x;
int y;
} Position;
typedef struct Location {
Position position;
const char *displaySign;
} Location;
typedef struct Room {
Position position;
int height;
int width;
Location locations[ALLOWED_ROOM_LOCATIONS_NUMBER];
} Room;
typedef struct Player {
Position position;
int health;
} Player;
//
// Enums
enum Direction {DIRECTION_NW, DIRECTION_N, DIRECTION_NE, DIRECTION_W, DIRECTION_E, DIRECTION_SW, DIRECTION_S, DIRECTION_SE};
enum LocationType {FLOOR, WALL, DOOR, EMPTY};
//
// Config and bootstrap methods
int screenSetup(void);
Room **mapSetup(void);
// Player methods
Player *playerSetup(void);
int playerMove(Player *user, enum Direction dir);
int playerDraw(Player *user);
// Room methods
Room *createRoom(int y, int x, int height, int width);
int drawRoom(Room *room);
const char *getDisplaySignForLocationType(enum LocationType locationType);
int connectLocations(const Location loc1, const Location loc2);
// Other methods
int handleInput(char, Player *user);
bool canGoToPosition(int y, int x);
int main()
{
Player *user;
char keyPressed;
srand(time(NULL));
screenSetup();
mapSetup();
user = playerSetup();
/* Main game loop */
while((keyPressed = getch()) != 'q') {
handleInput(keyPressed, user);
}
endwin();
return 0;
}
Player *playerSetup(void)
{
Player *newPlayer = malloc(sizeof(Player));
newPlayer->position.x = 14;
newPlayer->position.y = 14;
newPlayer->health = 20;
playerDraw(newPlayer);
return newPlayer;
}
int screenSetup(void)
{
initscr();
printw("Roguelike game!");
noecho();
refresh();
return 0;
}
Room **mapSetup(void)
{
int numberOfRooms = 3;
int totalNumberOfDoors = 0;
int howManyConnectionsWeWant = 2;
Location locationsToConnect[howManyConnectionsWeWant][2]; // Every connection is a pair of Positions
Room **rooms = malloc(sizeof(Room) * numberOfRooms);
rooms[0] = createRoom(13, 13, 6, 9);
drawRoom(rooms[0]);
rooms[1] = createRoom(13, 30, 6, 10);
drawRoom(rooms[1]);
rooms[2] = createRoom(25, 13, 6, 12);
drawRoom(rooms[2]);
for(int i = 0; i < howManyConnectionsWeWant; i++) {
int idOfFirstRoom = rand() % numberOfRooms;
int idOfSecondRoom;
int idOfDoorInFirstRoom;
int idOfDoorInSecondRoom;
// Find out second room to connect to (it must be different than the already chosen one)
while (1) {
idOfSecondRoom = rand() % numberOfRooms;
if(idOfSecondRoom != idOfFirstRoom) {
break;
}
}
// Having two rooms chosen we iterate over their locations to find unconnected doors
// We do not care if doors used for connections are used more than once
int idsOfRoomsToUse[2] = {idOfFirstRoom, idOfSecondRoom};
for(int j = 0; j < 2; j++) {
while (1) {
int idOfRoomLocation = rand() % ALLOWED_ROOM_LOCATIONS_NUMBER;
int idOfRoomToUse = idsOfRoomsToUse[j];
Location randomDoorInRoom = rooms[idOfRoomToUse]->locations[idOfRoomLocation];
if(randomDoorInRoom.displaySign == DOOR_SIGN // index actually contains a location
) {
if(j == 0) {
idOfDoorInFirstRoom = idOfRoomLocation;
} else {
idOfDoorInSecondRoom = idOfRoomLocation;
}
break;
}
}
}
connectLocations(rooms[idOfFirstRoom]->locations[idOfDoorInFirstRoom], rooms[idOfSecondRoom]->locations[idOfDoorInSecondRoom]);
}
return rooms;
}
int connectLocations(const Location loc1, const Location loc2)
{
Position temp;
Position doorTwo = loc2.position;
temp.x = loc1.position.x;
temp.y = loc1.position.y;
while (1)
{
// step left
if ((abs((temp.x - 1) - doorTwo.x) < abs(temp.x - doorTwo.x)) && (mvinch(temp.y, temp.x - 1) == ' ')) {
temp.x = temp.x - 1;
// step right
} else if ((abs((temp.x + 1) - doorTwo.x) < abs(temp.x - doorTwo.x)) && (mvinch(temp.y, temp.x + 1) == ' ')) {
temp.x = temp.x + 1;
// step down
} else if ((abs((temp.y + 1) - doorTwo.y) < abs(temp.y - doorTwo.y)) && (mvinch(temp.y + 1, temp.x) == ' ')) {
temp.y = temp.y + 1;
// step up
} else if ((abs((temp.y - 1) - doorTwo.y) < abs(temp.y - doorTwo.y)) && (mvinch(temp.y - 1, temp.x) == ' ')) {
temp.y = temp.y - 1;
} else {
return 0;
}
mvprintw(temp.y, temp.x, FLOOR_SIGN);
getch();
}
return 0;
}
Room *createRoom(int y, int x, int height, int width)
{
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));
newRoom->position.y = y;
newRoom->position.x = x;
newRoom->height = height;
newRoom->width = width;
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 = (x + 1) + ((rand() % (width - 2)));
newRoom->locations[i].position.y = y;
break;
case 1: // East wall
newRoom->locations[i].position.x = x + width - 1;
newRoom->locations[i].position.y = (y + 1) + ((rand() % (height - 2)));
break;
case 2: // South wall
newRoom->locations[i].position.x = (x + 1) + ((rand() % (width - 2)));
newRoom->locations[i].position.y = y - 1 + height;
break;
case 3: // West wall
newRoom->locations[i].position.x = x;
newRoom->locations[i].position.y = (y + 1) + ((rand() % (height - 2)));
break;
}
newRoom->locations[i].displaySign = getDisplaySignForLocationType(DOOR);
}
return newRoom;
}
const char *getDisplaySignForLocationType(enum LocationType locationType) {
switch (locationType) {
case DOOR:
return DOOR_SIGN;
break;
case FLOOR:
return FLOOR_SIGN;
break;
case WALL:
return WALL_SIGN;
break;
case EMPTY:
return EMPTY_SIGN;
break;
}
return WALL_SIGN; // To avoid possible errors
}
int drawRoom(Room *room)
{
int x, y;
// Draw top and bottom wall
for(x = room->position.x; x < room->position.x + room->width; x++) {
mvprintw(room->position.y, x, WALL_SIGN);
mvprintw(room->position.y + room->height - 1, x, WALL_SIGN);
}
// Draw floors and side walls
for(y = room->position.y + 1; y < room->position.y + room->height - 1; y++) {
// Draw side walls
mvprintw(y, room->position.x, WALL_SIGN);
mvprintw(y, room->position.x + room->width - 1, WALL_SIGN);
// Draw floor inside specific row
for(x = room->position.x + 1; x < room->position.x + room->width - 1; x++) {
mvprintw(y, x, FLOOR_SIGN);
}
}
// Drawing of locations - for the time being there are only doors
for(int i = 0; i < (sizeof room->locations / sizeof room->locations[0]); i++) {
if(room->locations[i].displaySign) {
mvprintw(room->locations[i].position.y, room->locations[i].position.x, room->locations[i].displaySign);
}
}
return 0;
}
int handleInput(char keyPressed, Player *user)
{
switch (keyPressed) {
// move up
case 'w':
case 'W':
playerMove(user, DIRECTION_N);
break;
// move left
case 'a':
case 'A':
playerMove(user, DIRECTION_W);
break;
// move right
case 'd':
case 'D':
playerMove(user, DIRECTION_E);
break;
// move down
case 's':
case 'S':
playerMove(user, DIRECTION_S);
break;
default:
break;
}
return 0;
}
int playerMove(Player *user, enum Direction direction)
{
int possibleX = user->position.x;
int possibleY = user->position.y;
switch (direction) {
// move up
case DIRECTION_N:
possibleY -= 1;
break;
// move left
case DIRECTION_W:
possibleX -= 1;
break;
// move right
case DIRECTION_E:
possibleX += 1;
break;
// move down
case DIRECTION_S:
possibleY += 1;
break;
default:
break; // We stay in place (usually 'turn' will pass then)
}
// Cleanup of current position
mvprintw(user->position.y, user->position.x, FLOOR_SIGN);
if(canGoToPosition(possibleY, possibleX)) {
user->position.y = possibleY;
user->position.x = possibleX;
}
// We redraw the player no matter if the position changed or not
playerDraw(user);
return 0;
}
bool canGoToPosition(int y, int x)
{
const int ALLOWED_LOCATIONS_SIZE = 2; // Hard-coded to simplify iteration loop
char allowedLocations[] = {*FLOOR_SIGN, *DOOR_SIGN};
char locationToCheck = mvinch(y, x);
for(int i = 0; i < ALLOWED_LOCATIONS_SIZE; i++) {
if(locationToCheck == allowedLocations[i]) {
return TRUE;
}
}
return FALSE;
}
int playerDraw(Player *user)
{
mvprintw(user->position.y, user->position.x, PLAYER_SIGN);
move(user->position.y, user->position.x);
return 0;
}
One Comment
Leave a Reply
You must be logged in to post a comment.
December 2020 summary – Bare.Metal.Dev
[…] – Seventh and Eight of Creating a Roguelike Game tutorial. They’re related so much so doing them in one […]