1.1. Drawing the Map
The first thing we need is a map (aka level) to walk around in. To keep things simple we'll use a hard coded map for now (generating random maps will be discussed in a later RogueLulz article):
###############
# # #
# #
# ### ####
#### ### # #
# # #
# # #
# #
# # #
###############
# - wall
Keeping with the tradition established by Rogue, we will use the at-mark (@) as our protagonist. The following code initializes a curses screen, draws the map and the player character and exits after a key has been pressed:
#include <curses.h>
char *map[]={
"###############",
"# # #",
"# #",
"# ### ####",
"#### ### # #",
"# # #",
"# # #",
"# #",
"# # #",
"###############"
};
int main() {
//initialize curses
keypad(initscr(),1);
curs_set(0);
//player's starting coordinates
int y=1;
int x=1;
//draw map
for (int yy=0;yy<10;yy++)
for (int xx=0;xx<15;xx++)
mvaddch(yy,xx,map[yy][xx]);
//draw player
mvaddch(y,x,'@');
//wait for key press before leaving
getch();
//clean up after we've finished using curses
return endwin();
}
You can compile the code by running
gcc 1.1.c -std=c99 -lcurses -o roguelulz
1.2. Walking Around
Now that we have our map, let's handle input from the user: the arrow keys will move @ and the escape key will quit the game. We'll also put the drawing (mvaddch
) and keyboard input (getch
) in a while loop so that the program doesn't exit after one key press:
#define ESC 27 //ASCII for escape
//last key pressed
int c=0;
do {
//draw map
for (int yy=0;yy<10;yy++)
for (int xx=0;xx<15;xx++)
mvaddch(yy,xx,map[yy][xx]);
//move player if there is no wall on the way
if (KEY_UP==c && ' '==map[y-1][x])
y--;
if (KEY_DOWN==c && ' '==map[y+1][x])
y++;
if (KEY_LEFT==c && ' '==map[y][x-1])
x--;
if (KEY_RIGHT==c && ' '==map[y][x+1])
x++;
//draw player
mvaddch(y,x,'@');
//quit when ESC is pressed
} while((ESC!=(c=getch())));
Compile again:
gcc 1.2.c -std=c99 -lcurses -o roguelulz
And @ can now walk around the map!
1.3. Basic Monsters
It's time to add some monsters - for now they'll just hang around waiting to be slain, we'll leave the AI for later. First let's define a struct
to represent a monster. Every monster should have a location on the map and hitpoints- as should the player. The ent
structure (short for entity) will be able to represent both:
typedef struct {
int y,x,hp;
}ent;
We'll have a list of 12 entities in our map- 1 for the player and 11 monsters for him to fight:
#define ENTS_ 12
ent ent_l[ENTS_];
We'll also add a two dimensional array ent_m
the size of our map, such that if an entity in our ent_l
list is located on tile (x,y) in the map, ent_m[y][x]
will point to it (Otherwise it will be NULL
):
#define Y_ 10
#define X_ 15
ent *ent_m[Y_][X_];
Previous references to our map's dimensions have also been replaced by Y_ & X_ so we could easily change its size later. The ent_m
array would allow us to easily find out if a tile is occupied in the following function. We initialise each of our entities to have 3 hitpoints and start in a random vacant tile (one that contains no wall and no other entity):
void init_ent() {
for (int e=0;e<ENTS_;e++) {
ent *ce=&ent_l[e];
ce->hp=3;
do {
ce->y=rand()%Y_;
ce->x=rand()%X_;
} while ( '#'==map[ce->y][ce->x] || NULL!=ent_m[ce->y][ce->x] );
ent_m[ce->y][ce->x]=ce;
}
}
At this point we can change the x & y variables inside the main() function to pointers to the first entity, which is none other than our hero, @:
//initialize entities
srand(time(0));
init_ent();
//player's starting coordinates
int *y=&ent_l[0].y;
int *x=&ent_l[0].x;
Now @ starts at a pseudo-random position every time we load the game. Let's also change the line responsible for drawing the player to also draw the monsters:
//draw entities
for(int e=0;e<ENTS_;e++){
mvaddch(ent_l[e].y,ent_l[e].x, 0==e?'@':'m');
}
Compile and check out the changes:
gcc 1.3.c -std=c99 -lcurses -o roguelulz
1.4. Attacking
As you may have noticed, although the monsters are placed and drawn, @ still passes through them as if they aren't. In this final section of RogueLulz, Part I we will implement a simple attack. Every time @ walks into a monster, the monster's hitpoints will be reduced by 1. When a monster reaches 0 hitpoints it dies. First we modify the drawing code to only draw entities with more than 0 hitpoints:
//draw entities
for(int e=0;e<ENTS_;e++){
if (ent_l[e].hp>0)
mvaddch(ent_l[e].y,ent_l[e].x, 0==e?'@':'m');
Then, we modify the movement code to have @ attack monsters he walks into, instead of just walking through them:
//move player if there is no living entity on the way
void move_to(int *y,int *x,int dy,int dx) {
//remove reference to the player's old position
ent_m[*y][*x]=NULL;
//if the destination tile has an entity in it
if (NULL!=ent_m[*y+dy][*x+dx]) {
//decrement hitpoints of the entity at the destination tile
ent *de=ent_m[*y+dy][*x+dx];
de->hp--;
//if it's still alive don't move into its place
if (0 < de->hp) {
dy=0;
dx=0;
//if it's dead remove its reference
} else {
ent_m[*y+dy][*x+dx]=NULL;
}
}
//update player's position
*y+=dy;
*x+=dx;
//add reference to the player's new position
ent_m[*y][*x]=&ent_l[0];
}
and in the main loop:
//move player if there is no wall on the way
if (KEY_UP==c && ' '==map[*y-1][*x])
move_to(y,x,-1,0);
if (KEY_DOWN==c && ' '==map[*y+1][*x])
move_to(y,x,1,0);
if (KEY_LEFT==c && ' '==map[*y][*x-1])
move_to(y,x,0,-1);
if (KEY_RIGHT==c && ' '==map[*y][*x+1])
move_to(y,x,0,1);
At last, compile RogueLulz, Part I's final result:
gcc 1.4.c -std=c99 -lcurses -o roguelulz
1.5. Conclusion and Future Installments
Thus far we have learned how to draw the map, walk around it, add some dumb monsters and even attack them. In the next installments we'll add random map generation, multilevel dungeons, field-of-view/line-of-sight, AI and winning/losing conditions.
1.6. The Final Source Code
#include <curses.h>
#include <stdlib.h>
#include <time.h>
#define ESC 27// ASCII for escape
typedef struct {
int y,x,hp;
}ent;
#define ENTS_ 12
ent ent_l[ENTS_];
#define Y_ 10
#define X_ 15
ent *ent_m[Y_][X_];
char *map[]={
"###############",
"# # #",
"# #",
"# ### ####",
"#### ### # #",
"# # #",
"# # #",
"# #",
"# # #",
"###############"
};
void init_ent() {
for (int e=0;e<ENTS_;e++) {
ent *ce=&ent_l[e];
ce->hp=3;
do {
ce->y=rand()%Y_;
ce->x=rand()%X_;
} while ( '#'==map[ce->y][ce->x] || NULL!=ent_m[ce->y][ce->x] );
ent_m[ce->y][ce->x]=ce;
}
}
//move player if there is no living entity on the way
void move_to(int *y,int *x,int dy,int dx) {
//remove reference to the player's old position
ent_m[*y][*x]=NULL;
//if the destination tile has an entity in it
if (NULL!=ent_m[*y+dy][*x+dx]) {
//decrement hitpoints of the entity at the destination tile
ent *de=ent_m[*y+dy][*x+dx];
de->hp--;
//if it's still alive don't move into its place
if (0 < de->hp) {
dy=0;
dx=0;
//if it's dead remove its reference
} else {
ent_m[*y+dy][*x+dx]=NULL;
}
}
//update player's position
*y+=dy;
*x+=dx;
//add reference to the player's new position
ent_m[*y][*x]=&ent_l[0];
}
int main() {
//initialize curses
keypad(initscr(),1);
curs_set(0);
//initialize entities
srand(time(0));
init_ent();
//player's starting coordinates
int *y=&ent_l[0].y;
int *x=&ent_l[0].x;
//last key pressed
int c=0;
do {
//draw map
for (int yy=0;yy<Y_;yy++)
for (int xx=0;xx<X_;xx++)
mvaddch(yy,xx,map[yy][xx]);
//move player if there is no wall on the way
if (KEY_UP==c && ' '==map[*y-1][*x])
move_to(y,x,-1,0);
if (KEY_DOWN==c && ' '==map[*y+1][*x])
move_to(y,x,1,0);
if (KEY_LEFT==c && ' '==map[*y][*x-1])
move_to(y,x,0,-1);
if (KEY_RIGHT==c && ' '==map[*y][*x+1])
move_to(y,x,0,1);
//draw entities
for (int e=0;e<ENTS_;e++) {
if (ent_l[e].hp>0)
mvaddch(ent_l[e].y,ent_l[e].x, 0==e?'@':'m');
}
//quit when ESC is pressed
} while ((ESC!=(c=getch())));
//clean up after we've finished using curses
endwin();
}
1.7. Further Reading