C language tutorial

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


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! 😉

To report typo, error or make suggestion: select text and press Ctrl+Enter.