Introduction by Tangar Igroglaz:
This article is a tutorial how to create basic roguelike game in C language by Joe Sixpack. This article was originally written in 2007 at freeshell.org; but as this website no longer available, I host it there. I’ve made some edits and notes to the article, but preserve authors coding style. At the bottom of article I’ve also enclose variant of RogueLulz code with my formatting where I’ve fixed some unclear spots.
Author: Joe Sixpack
RogueLulz, Part I: Drawing the Map, Walking Around, Basic Monsters and Attacking
You should be familiar with C to better understand the source code. A C99 compiler (I use gcc) and a Curses compatible library. We will use there ncurses – to install it under Linux:
sudo apt-get install libncurses-dev
Ncurses is required for compiling the code snippets in the article. The easiest way get both running on Windows is to install Cygwin.
Also you can use PDCurses for Windows.
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(); }
Save the file as main.c
Now you can compile the code by running:
gcc main.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())));
So we will have such code:
#include <curses.h> #define ESC 27 // ASCII for escape char *map[]={ "###############", "# # #", "# #", "# ### ####", "#### ### # #", "# # #", "# # #", "# #", "# # #", "###############" }; int main() { //initialize curses keypad(initscr(),1); curs_set(0); // player's starting coordinates int y = 1; int x = 1; //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()))); // wait for key press before leaving getch(); // clean up after we've finished using curses return endwin(); }
Compile again:
gcc main.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 initialize 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 main.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. Now 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 main.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
- The rec.games.roguelike.development newsgroup
- The Roguelike Dev FAQ.
Q&A section:
what does this do?
#define ENTS_ 12
ent ent_l[ENTS_];
all occurences of ENTS_ in the code (NUM_ENTS would have been better).
How do I get rid of the cursor? It appears after the last monster drawn and is struck there.
curs_set(0);
I don’t like the kill/move maneuver
I’d rewrite the extremely compact “if hp-1 = 0 then trample monster and remove it” to “if hp-1 = 0 then remove monster, but don’t move”
this line is too difficult to understand and should be replaced
//decrement entity's hitpoints & if it's
//still alive don't move into its place
if (0<--(ent_m[*y+dy][*x+dx]->hp)) {
perhaps by:
//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) {
Formatted code by Tangar Igroglaz:
#include <curses.h> #include <stdlib.h> #include <time.h> #define ESC 27 // ASCII for escape char *map[] = { "###############", "# # #", "# #", "# ### ####", "#### ### # #", "# # #", "# # #", "# #", "# # #", "###############" }; // entity (for monster and player) typedef struct { int y, x, hp; } ent; // number of entities at map #define ENTS_ 12 ent ent_l[ENTS_]; // entity list array #define Y_ 10 #define X_ 15 ent *ent_m[Y_][X_]; // 2d array for 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] == '#' || ent_m[ce->y][ce->x] != NULL); 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 (ent_m[*y+dy][*x+dx] != NULL) { // 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 (de->hp > 0) { 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 < 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 (c == KEY_UP && map[*y - 1][*x] == ' ') move_to(y, x, -1, 0); if (c == KEY_DOWN && map[*y + 1][*x] == ' ') move_to(y, x, 1, 0); if (c == KEY_LEFT && map[*y][*x - 1] == ' ') move_to(y, x, 0, -1); if (c == KEY_RIGHT && 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, e == 0 ? '@' : 'm'); // quit when ESC is pressed } while ((ESC != (c = getch()))); //clean up after we've finished using curses endwin(); }
Enjoy! 😉