Roguelike AoC – part 6
A sixth lesson of roguelike game tutorial added a nice feature – doors.
Position is not enough
Due to the fact that I come from way more business-oriented world (Java, banking, pharma – You do the math) than usual C programmer, I couldn’t agree with author’s approach to creating doors. Using Position structure for that is not right – position is position – it is a convenience wrapper around coordinates in 2D env. That’s all. In the tutorial, this structure is used to store data about doors, which it should not do.
Therefore, I’ve decided to create a new struct called Location. The idea is that this data structure should represent what its name suggests – actual location in the dungeon. Position is a part of it, same as the sign used to represent it. In the future data we would need (can it be accessed, does it hold any items, etc). For the time being. In my opinion it is better to just have locations property holding all the special locations present in the room. What is more – maybe all the locations in the room could be represented with this struct. I’ve decided to keep that in mind, but for the time being I’ve started slow.
typedef struct Location {
Position position;
const char *displaySign;
} Location;
typedef struct Room {
Position position;
int height;
int width;
/** Added this one - we're prepared for 4 special locations in the Room right now (for the time being we only have
doors). But this is just a possible amount of total - how many doors per room will be generated is decided in the
createRoom function.
*/
Location locations[4];
} Room;
I did not forget about adding additional constant for doors:
const char *DOOR_SIGN = "+";
Four doors? Why not thirty?
To make things more robust I’ve also decided to avoid hard-coding target amount of doors. Why not let the fate decide? Here is new and improved createRoom function, that draws the random number of doors (right now limited to only 2). The algorithm also keeps the doors being generated in the room corners.
Room *createRoom(int y, int x, int height, int width)
{
/** We only set 2 doors per room right now - but it can be customized with this variable in the future. The arrays
that holds locations in a room can right now store up to 4 elements */
int doorsToCreate = (rand() % 2) + 1;
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++) {
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 = "+";
}
return newRoom;
}
Where is my sign?
In the above code example a sign used for displaying a door is hardcoded. As You probably remember I’ve created separate constants for that. Therefore, it would be nice to use them, and make the process of fetching a specific display sign more robust. In order to do that, I’ve added new enum that specifies the type of location’s signs and wrote additional method to use it.
enum LocationType {FLOOR, WALL, DOOR};
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;
}
return WALL_SIGN; // To avoid possible errors
}
Code
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <curses.h>
#include <time.h>
// Constants
const char *WALL_SIGN = "#";
const char *FLOOR_SIGN = ".";
const char *PLAYER_SIGN = "@";
const char *DOOR_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[4];
} 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};
//
// 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);
// 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)
{
Room **rooms = malloc(sizeof(Room)*3);
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]);
return rooms;
}
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;
}
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 = 1; // Hard-coded to simplify iteration loop
char allowedLocations[] = {*FLOOR_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;
}
Leave a Reply
You must be logged in to post a comment.