Introduction
In our last lesson, you successfully built a functional inventory system in C, complete with functions for adding, displaying, and removing items. We focused on the practical implementation details, including the trade-offs between different array removal strategies. You now have a key piece of game data management in place.
Today, we will build upon that foundation by implementing another cornerstone of many games: a combat loop. We will design and code a turn-based fighting system where a player and an enemy can exchange actions until one is defeated. This lesson will show you how to structure game logic within a loop, handle player and AI turns, and connect character stats (our data) to game actions (our functions). By the end, you'll have a dynamic, interactive system that forms the heart of a text-based RPG.
The Anatomy of a Game Loop
At its core, almost every video game is driven by a loop that runs continuously. This is known as the game loop. While it can become very complex, the fundamental structure is often described as a "Holy Trinity" of operations that occur every single frame or turn.
- Process Input: The game checks for any input from the player (e.g., key presses, mouse clicks).
- Update: The game logic advances. This involves moving characters, calculating physics, checking for collisions, or, in our case, calculating combat damage and new health totals.
- Render: The game draws the updated state of the world to the screen. For us, this means printing text to the console.
The following video provides an excellent conceptual overview of this structure.
Creating a Game Loop with C & SDL (Tutorial)
This video from the pikuma channel introduces the fundamental components of a game loop.
Please watch the segment from the beginning. The presenter clearly explains the setup phase, which is run once, followed by the core loop of process input, update, and render. This is the essential pattern we will be implementing for our combat system.
For our turn-based text game, this pattern simplifies nicely:
- Process Input: We'll ask the player to choose an action (
attack,heal, etc.). - Update: We'll execute the player's action and then the enemy's action, updating their health stats accordingly.
- Render: We'll print the outcome of the turn to the console.
This entire sequence will be enclosed in a while loop that continues as long as both the player and the enemy are alive.
Structuring the Combat Logic
Before writing code, it's crucial to think about architecture. A common pitfall for new programmers is to cram all game logic into a giant, nested series of if-else statements. While this might work for a tiny game, it quickly becomes unreadable and impossible to maintain.

The video below humorously illustrates this "naive approach" and then introduces a much cleaner, more powerful design pattern.
The Ideal Way To Program a Text Adventure Game | CultGame Devlog 2
This video from Bucephalus Studios discusses a common but flawed way to program text-based games and then presents a better architectural solution.
First, watch the section on the naive approach. This highlights the problem of relying on a massive chain of if statements to manage game state. Next, watch the segment from views, handlers, and models. The speaker introduces an architecture inspired by the Model-View-Controller (MVC) pattern, separating data (Models), display (Views), and logic (Handlers). This separation is key to writing clean, scalable code.
We can apply this powerful idea directly to our C combat system:
- Model: Our
structdefinitions for the player and enemy. These hold all the data (health, attack power, etc.). - View: The
printfstatements that display health, action menus, and combat results. We'll create dedicated functions for this, likedisplay_status(). - Handler: Functions that execute specific actions, such as
perform_attack()oruse_potion(). These functions take the models (player/enemy structs) as input and modify them.
By keeping these concerns separate, our main combat loop becomes clean and simple: its only job is to call the right view and handler functions in the correct order.
Implementing the Combat Loop in C
Let's now translate these concepts into a working C implementation. We'll build a Fight function that orchestrates the entire battle. The resource from Wikibooks provides an excellent, incremental guide for this process.
Learning C With Game Concepts/Designing A Roleplaying ...
This guide walks through the creation of a C-based RPG, including a detailed section on building a fight system from the ground up.
First, read the section that starts with the initial concept. This introduces the core idea of calculating EffectiveAttack and sets up the stats in the playerStructure. Next, review the section that expands the Fight function. Focus on the design goals: looping until a character's health is zero and using a menu with a switch statement to handle player choices. This is our "View" and "Handler" pattern in action. Finally, read the crucial section on automating the enemy and swapping turns. Pay close attention to two key implementation details: Adding a bool autoPilot flag to the character struct to differentiate between a human player and an AI-controlled enemy. The clever technique of swapping the Attacker and Target pointers at the end of each turn. This allows the same logic to apply to both the player's and the enemy's turn, making the code clean and reusable.
Putting It All Together
Let's synthesize these ideas into a complete program. The code below sets up a Character struct, initializes a hero and a monster, and then starts the combat loop. It uses the autoPilot flag and pointer swapping technique described in the Wikibooks resource.
#include <stdio.h>
#include <string.h>
#include <stdbool.h> // Required for the bool type
#define MAX_NAME_LENGTH 50
// Model: The data structure for any combatant
typedef struct {
char name[MAX_NAME_LENGTH];
int health;
int max_health;
int attack;
int defense;
bool autoPilot; // Is this character controlled by AI?
} Character;
// Function Prototypes
void display_status(const Character *c);
int fight_loop(Character *player, Character *enemy);
void swap_pointers(Character **a, Character **b);
// --- Main Program ---
int main() {
// Setup: Initialize our combatants
Character hero = {"Hero", 100, 100, 15, 5, false};
Character goblin = {"Goblin", 50, 50, 10, 2, true};
printf("A wild %s appears!\n\n", goblin.name);
int result = fight_loop(&hero, &goblin);
printf("\n--- Battle Over ---\n");
if (result == 1) {
printf("%s is victorious!\n", hero.name);
} else {
printf("%s has been defeated.\n", hero.name);
}
return 0;
}
// --- Function Definitions ---
/**
* The main combat loop.
* Returns 1 if the player wins, 0 otherwise.
*/
int fight_loop(Character *player, Character *enemy) {
Character *attacker = player;
Character *target = enemy;
// Loop continues as long as both are alive
while (player->health > 0 && enemy->health > 0) {
printf("--- New Turn ---\n");
display_status(player);
display_status(enemy);
printf("\nIt is %s's turn.\n", attacker->name);
int choice = 1; // Default to attack
// Process Input (if not on autopilot)
if (attacker->autoPilot == false) {
printf("Choose your action:\n");
printf(" 1) Attack\n");
printf(">> ");
char line[10];
fgets(line, sizeof(line), stdin);
sscanf(line, "%d", &choice);
}
// Update: Execute the chosen action
switch (choice) {
case 1: { // Attack
int damage = attacker->attack - target->defense;
if (damage < 0) damage = 0; // Cannot heal by attacking
printf("%s attacks %s for %d damage!\n", attacker->name, target->name, damage);
target->health -= damage;
break;
}
default:
printf("%s is confused and does nothing.\n", attacker->name);
break;
}
// Check for victory after the attack
if (target->health <= 0) {
target->health = 0; // Don't show negative health
break; // Exit the loop
}
printf("\n");
// Swap turns for the next iteration
swap_pointers(&attacker, &target);
}
// Announce the result and return
display_status(player);
display_status(enemy);
return player->health > 0; // If player health > 0, player won.
}
// View: Displays the status of a single character
void display_status(const Character *c) {
printf("%s | Health: %d/%d\n", c->name, c->health, c->max_health);
}
// Utility: Swaps two Character pointers
void swap_pointers(Character **a, Character **b) {
Character *temp = *a;
*a = *b;
*b = temp;
}
Take a moment to compile and run this code. Play through a few turns. Notice how the fight_loop remains clean and readable. The logic for whose turn it is and what they do is handled elegantly by the autoPilot flag and the swap_pointers function. This is a much more robust design than a tangled mess of if statements.
Conclusion
In this lesson, you've taken a significant step from managing static data to creating dynamic, interactive game logic. You've implemented a complete combat loop, one of the most fundamental patterns in game development.
Key Takeaways:
- A game loop is the central cycle of processing input, updating game state, and rendering the results.
- Structuring your code using a Model-View-Handler pattern (separating data, display, and logic) is critical for creating clean and scalable systems.
- A turn-based combat loop can be implemented with a
whileloop that continues as long as both combatants are alive. - Clever use of C pointers, such as swapping attacker/target pointers, can simplify turn management and make your code more elegant and reusable.
- An
autoPilotflag is a simple but effective way to distinguish between player-controlled and AI-controlled entities within the same logic.
You now have two major game systems—inventory and combat. While they are separate for now, you can already imagine how they might interact. In our next lesson, we'll shift our focus to improving code quality and maintainability by learning about C's preprocessor. You'll see how to replace "magic numbers" (like 100 for health) with symbolic constants, making your code far easier to read and modify.