Back to Home

Learning C through ASCII-RPG: code and structures

The article breaks down the code of a simple ASCII-RPG in C for learning the language. Describes data structures, combat functions, menu and typical input errors. Suitable for middle/senior developers.

ASCII-RPG in C: full code breakdown of the game
Advertisement 728x90

Building an ASCII RPG in C: From Structs to the Game Loop

Many developers learn C by diving into hands-on projects. Building a simple ASCII-based RPG is an excellent way to get comfortable with data structures, pointers, functions, and I/O handling. In this guide, we’ll walk through the code for a terminal-based combat game featuring random monster encounters, while highlighting common beginner traps along the way.

Core Data Structures

The project relies on typedef to define the game's core entities:

typedef struct {
    char Name[15];
    int Hp;
    int Attack;
} Player;

typedef struct {
    char Name[15];
    int Hp;
    int min_attack;
    int max_attack;
    int type;
} Monster;

typedef struct {
    int min_heal;
    int max_heal;
} Heal;
  • Player: Name (capped at 14 characters), HP, and attack power.
  • Monster: Name, HP, damage range, and a type identifier for ASCII art.
  • Heal: HP restoration range.

Limiting the name input with %14s in scanf prevents buffer overflows. The player’s maximum HP is hardcoded to 20.

Google AdInline article slot

Initializing Entities

Initialization functions populate the structs with starting values:

void init_player(Player* player) {
    printf("Send Nickname: ");
    scanf("%14s", player->Name);
    player->Hp = 20;
}

Monsters are selected from a template array using rand():

void init_monster(Monster* monster) {
    Monster monster_list[3] = {
        {"Amogus",30,3,6,0},
        {"Slime",20,1,3,1},
        {"Spider",25,2,4,2}
    };
    int id = rand() % 3;
    *monster = monster_list[id];
}

Calling srand(time(NULL)) in main() ensures a fresh seed every time the program runs.

Google AdInline article slot

Rendering Battle State

The print_status function draws the monster’s ASCII art based on its type and displays current HP:

void print_status(Player player, Monster monster){
    printf("\n--- MerRPG ---\n");
    if (monster.type == 0) {
        printf(" 0             (AMOGUS)\n");
        printf("/|\\             (00)\n");
        printf("/ \\             /___\\\n");
    } // ... other types
    printf("%s HP: %d\n", player.Name, player.Hp);
    printf("%s HP: %d\n", monster.Name, monster.Hp);
}

Values are passed by value here since the function only reads the data.

Combat Mechanics

The player’s attack deals 1–5 damage and uses pointers to modify the monster’s HP:

Google AdInline article slot
void player_attack(Player* player, Monster* monster){
    int damage_player = rand() % 5 + 1;
    monster->Hp -= damage_player;
    printf("You caused %d damage\n", damage_player);
    if (monster->Hp < 0) {
        monster->Hp = 0;
    }
}

The monster’s attack pulls from its defined damage range:

void monster_attack(Player* player, Monster* monster){
    int damage_monster = rand() % (monster->max_attack - monster->min_attack + 1) + monster->min_attack;
    player->Hp -= damage_monster;
    printf("Enemy caused %d damage\n", damage_monster);
    if (player->Hp < 0) {
        player->Hp = 0;
    }
}

Healing caps the player’s HP at the maximum of 20:

void player_heal(Player* player, Heal* heal) {
    int heal_player = rand() % (heal->max_heal - heal->min_heal + 1) + heal->min_heal;
    player->Hp += heal_player;
    printf("You have restored %d Hp\n", heal_player);
    if (player->Hp > 20) {
        player->Hp = 20;
    }
}

The Main Game Loop

The start_game() function drives the combat sequence inside a while loop:

void start_game() {    
    Player player;
    Monster monster;
    Heal heal = {1,6};
    init_player(&player);
    init_monster(&monster);

    while(player.Hp > 0 && monster.Hp > 0 ) {
        print_status(player, monster);
        printf("[0] - Exit\n");
        printf("[1] - Attack\n");
        printf("[2] - Healing\n");
        scanf("%d", &choice);
        if (choice == 1) {
            player_attack(&player, &monster);
            if (monster.Hp > 0) {
                monster_attack(&player, &monster);
            }
        } else if (choice == 2) {
            player_heal(&player, &heal);
            monster_attack(&player, &monster);
        } else if (choice == 0) {
            printf("You are out of the game\n");
            break;
        } else {
            break;
        }
    }
    if (player.Hp == 0) {
        printf("You Lose...\n");
    } else if (monster.Hp == 0) {
        printf("You Win!\n");
    }
    printf("Press any button to return to menu\n");
    char tmp;
    getchar();
    scanf("%s", &tmp);  
}

The loop continuously checks both entities' HP. After the player acts, the monster counterattacks if it’s still alive.

Menu and Modularity

The main menu lives in a separate file alongside menu.h:

void menu_game(void) {
    while (1) {
        // ASCII banner art
        printf("1 - Start game\n");
        printf("2 - Exit\n");
        scanf("%d", &choice);
        if (choice == 1) {
            start_game();
        } else if (choice == 2) {
            break;
        }
    }
}

Similarly, start_game() is declared in game.h. The main() function seeds the RNG and launches the menu.

Common Pitfalls and Workarounds

  • Input Buffer Clearing: After scanf("%d"), a newline character (\n) lingers in the buffer. getchar() consumes it before the next scanf("%s", &tmp).
  • Cyrillic/Multibyte Input: %c only reads a single byte, which breaks with multibyte characters like Cyrillic. Switching to %s sidesteps the issue.
  • Hardcoded HP Limits: The Heal struct is initialized locally as {1,6} without being passed through the loop.

Future iterations should introduce dynamic monster arrays, a save system, and expanded gameplay mechanics.

Key Takeaways

  • Use pointers to modify structs inside functions.
  • Always pair rand() with srand(time(NULL)) for proper randomization.
  • Flush the input buffer after reading integers with scanf.
  • Enforce string length limits in scanf (e.g., %14s) to prevent buffer overflows.
  • Split your code into .h and .c files for better modularity.

— Editorial Team

Advertisement 728x90

Read Next