10.7 - Problem-Solving Challenge: Turn-Based Combat System
In this challenge, you'll apply the algorithmic concepts and patterns you've learned to create a turn-based combat system similar to those found in RPGs and strategy games. This project will integrate multiple algorithms and data structures into a cohesive gameplay feature.
Project Overview
You'll build a console-based turn-based combat system with the following features:
- Character stats and abilities
- Turn order determination
- Action selection and targeting
- Damage calculation
- Status effects
- AI decision-making for enemies
This challenge will test your ability to:
- Break down a complex problem into manageable components
- Apply appropriate algorithms and data structures
- Implement game mechanics using algorithmic patterns
- Create a balanced and engaging system
Step 1: Define the Core Components
Let's start by defining the basic classes and interfaces for our combat system:
// Base class for all combatants (players and enemies)
public abstract class Combatant
{
public string Name { get; protected set; }
public int MaxHealth { get; protected set; }
public int CurrentHealth { get; protected set; }
public int Attack { get; protected set; }
public int Defense { get; protected set; }
public int Speed { get; protected set; }
public bool IsAlive => CurrentHealth > 0;
// List of status effects applied to this combatant
protected List<StatusEffect> statusEffects = new List<StatusEffect>();
// List of abilities this combatant can use
protected List<Ability> abilities = new List<Ability>();
public Combatant(string name, int health, int attack, int defense, int speed)
{
Name = name;
MaxHealth = health;
CurrentHealth = health;
Attack = attack;
Defense = defense;
Speed = speed;
}
// Take damage from an attack
public virtual void TakeDamage(int amount)
{
// Apply defense reduction
int actualDamage = Math.Max(1, amount - Defense);
CurrentHealth = Math.Max(0, CurrentHealth - actualDamage);
Console.WriteLine($"{Name} takes {actualDamage} damage! ({CurrentHealth}/{MaxHealth} HP)");
if (CurrentHealth <= 0)
{
Console.WriteLine($"{Name} has been defeated!");
}
}
// Heal health
public virtual void Heal(int amount)
{
int actualHeal = Math.Min(MaxHealth - CurrentHealth, amount);
CurrentHealth += actualHeal;
Console.WriteLine($"{Name} heals {actualHeal} HP! ({CurrentHealth}/{MaxHealth} HP)");
}
// Add a status effect
public virtual void AddStatusEffect(StatusEffect effect)
{
// Check if this type of effect is already applied
StatusEffect existingEffect = statusEffects.FirstOrDefault(e => e.GetType() == effect.GetType());
if (existingEffect != null)
{
// Refresh duration of existing effect
existingEffect.ResetDuration();
Console.WriteLine($"{Name}'s {effect.Name} has been refreshed!");
}
else
{
// Apply new effect
statusEffects.Add(effect);
effect.Apply(this);
Console.WriteLine($"{Name} is affected by {effect.Name}!");
}
}
// Process status effects at the start of turn
public virtual void ProcessStatusEffects()
{
// Create a copy of the list to avoid issues when removing during iteration
List<StatusEffect> currentEffects = new List<StatusEffect>(statusEffects);
foreach (StatusEffect effect in currentEffects)
{
effect.OnTurn(this);
if (effect.IsExpired)
{
effect.Remove(this);
statusEffects.Remove(effect);
Console.WriteLine($"{effect.Name} has worn off from {Name}!");
}
}
}
// Get all available abilities
public virtual List<Ability> GetAbilities()
{
return new List<Ability>(abilities);
}
// Select an action to perform (implemented by subclasses)
public abstract CombatAction SelectAction(List<Combatant> allies, List<Combatant> enemies);
}
// Player character class
public class PlayerCharacter : Combatant
{
public PlayerCharacter(string name, int health, int attack, int defense, int speed)
: base(name, health, attack, defense, speed)
{
// Add basic abilities
abilities.Add(new BasicAttack());
abilities.Add(new Heal());
}
public override CombatAction SelectAction(List<Combatant> allies, List<Combatant> enemies)
{
// Display available abilities
Console.WriteLine($"\n{Name}'s turn! Select an action:");
List<Ability> availableAbilities = GetAbilities();
for (int i = 0; i < availableAbilities.Count; i++)
{
Console.WriteLine($"{i + 1}. {availableAbilities[i].Name} - {availableAbilities[i].Description}");
}
// Get player input for ability selection
int abilityIndex = -1;
while (abilityIndex < 0 || abilityIndex >= availableAbilities.Count)
{
Console.Write("Enter ability number: ");
if (int.TryParse(Console.ReadLine(), out abilityIndex))
{
abilityIndex--; // Convert to 0-based index
}
else
{
abilityIndex = -1;
}
}
Ability selectedAbility = availableAbilities[abilityIndex];
// Get valid targets for the selected ability
List<Combatant> validTargets = selectedAbility.GetValidTargets(this, allies, enemies);
// Display valid targets
Console.WriteLine($"Select target for {selectedAbility.Name}:");
for (int i = 0; i < validTargets.Count; i++)
{
Console.WriteLine($"{i + 1}. {validTargets[i].Name} ({validTargets[i].CurrentHealth}/{validTargets[i].MaxHealth} HP)");
}
// Get player input for target selection
int targetIndex = -1;
while (targetIndex < 0 || targetIndex >= validTargets.Count)
{
Console.Write("Enter target number: ");
if (int.TryParse(Console.ReadLine(), out targetIndex))
{
targetIndex--; // Convert to 0-based index
}
else
{
targetIndex = -1;
}
}
Combatant selectedTarget = validTargets[targetIndex];
// Create and return the combat action
return new CombatAction(this, selectedAbility, selectedTarget);
}
}
// Enemy class with AI behavior
public class Enemy : Combatant
{
public Enemy(string name, int health, int attack, int defense, int speed)
: base(name, health, attack, defense, speed)
{
// Add basic abilities
abilities.Add(new BasicAttack());
}
public override CombatAction SelectAction(List<Combatant> allies, List<Combatant> enemies)
{
// Simple AI: randomly select an ability and target
Random random = new Random();
// Get available abilities
List<Ability> availableAbilities = GetAbilities();
Ability selectedAbility = availableAbilities[random.Next(availableAbilities.Count)];
// Get valid targets for the selected ability
List<Combatant> validTargets = selectedAbility.GetValidTargets(this, allies, enemies);
// Select a random target
Combatant selectedTarget = validTargets[random.Next(validTargets.Count)];
Console.WriteLine($"{Name} is planning to use {selectedAbility.Name} on {selectedTarget.Name}!");
// Create and return the combat action
return new CombatAction(this, selectedAbility, selectedTarget);
}
}
// Represents an ability that can be used in combat
public abstract class Ability
{
public string Name { get; protected set; }
public string Description { get; protected set; }
// Determine valid targets for this ability
public abstract List<Combatant> GetValidTargets(Combatant user, List<Combatant> allies, List<Combatant> enemies);
// Execute the ability on the target
public abstract void Execute(Combatant user, Combatant target);
}
// Basic attack ability
public class BasicAttack : Ability
{
public BasicAttack()
{
Name = "Attack";
Description = "A basic attack that deals damage based on Attack stat.";
}
public override List<Combatant> GetValidTargets(Combatant user, List<Combatant> allies, List<Combatant> enemies)
{
// Can only target enemies that are alive
return enemies.Where(e => e.IsAlive).ToList();
}
public override void Execute(Combatant user, Combatant target)
{
Console.WriteLine($"{user.Name} attacks {target.Name}!");
// Calculate damage based on attacker's Attack stat
int damage = user.Attack;
// Apply damage to target
target.TakeDamage(damage);
}
}
// Healing ability
public class Heal : Ability
{
public Heal()
{
Name = "Heal";
Description = "Restore health to self or an ally.";
}
public override List<Combatant> GetValidTargets(Combatant user, List<Combatant> allies, List<Combatant> enemies)
{
// Can target self or allies that are alive and not at full health
return allies.Where(a => a.IsAlive && a.CurrentHealth < a.MaxHealth).ToList();
}
public override void Execute(Combatant user, Combatant target)
{
Console.WriteLine($"{user.Name} heals {target.Name}!");
// Calculate heal amount (based on user's Attack stat for simplicity)
int healAmount = user.Attack / 2;
// Apply healing
target.Heal(healAmount);
}
}
// Status effect base class
public abstract class StatusEffect
{
public string Name { get; protected set; }
public int Duration { get; protected set; }
public bool IsExpired => Duration <= 0;
public StatusEffect(int duration)
{
Duration = duration;
}
// Apply the effect when first added
public abstract void Apply(Combatant target);
// Process the effect at the start of the target's turn
public abstract void OnTurn(Combatant target);
// Remove the effect when it expires
public abstract void Remove(Combatant target);
// Reset the duration (when refreshed)
public virtual void ResetDuration()
{
Duration = 3; // Default duration
}
}
// Poison status effect
public class PoisonEffect : StatusEffect
{
private int damagePerTurn;
public PoisonEffect(int duration = 3, int damagePerTurn = 5) : base(duration)
{
Name = "Poison";
this.damagePerTurn = damagePerTurn;
}
public override void Apply(Combatant target)
{
// No immediate effect when applied
}
public override void OnTurn(Combatant target)
{
Console.WriteLine($"{target.Name} takes {damagePerTurn} poison damage!");
target.TakeDamage(damagePerTurn);
Duration--;
}
public override void Remove(Combatant target)
{
// No effect when removed
}
}
// Represents an action selected by a combatant
public class CombatAction
{
public Combatant User { get; }
public Ability Ability { get; }
public Combatant Target { get; }
public CombatAction(Combatant user, Ability ability, Combatant target)
{
User = user;
Ability = ability;
Target = target;
}
// Execute this action
public void Execute()
{
Ability.Execute(User, Target);
}
}
Step 2: Implement the Combat System
Now, let's implement the main combat system that will manage the battle:
public class CombatSystem
{
private List<Combatant> playerTeam;
private List<Combatant> enemyTeam;
private List<Combatant> turnOrder;
private bool isCombatActive;
public CombatSystem(List<Combatant> playerTeam, List<Combatant> enemyTeam)
{
this.playerTeam = playerTeam;
this.enemyTeam = enemyTeam;
this.turnOrder = new List<Combatant>();
this.isCombatActive = true;
}
// Start the combat encounter
public void StartCombat()
{
Console.WriteLine("=== COMBAT START ===");
// Display initial state
DisplayCombatState();
// Main combat loop
while (isCombatActive)
{
// Determine turn order
DetermineTurnOrder();
// Process each combatant's turn
foreach (Combatant combatant in turnOrder)
{
// Skip if combatant is no longer alive
if (!combatant.IsAlive)
continue;
// Process status effects
combatant.ProcessStatusEffects();
// Skip turn if combatant died from status effects
if (!combatant.IsAlive)
continue;
// Select and execute action
CombatAction action = combatant.SelectAction(
combatant is PlayerCharacter ? playerTeam : enemyTeam,
combatant is PlayerCharacter ? enemyTeam : playerTeam
);
// Skip if target is no longer valid
if (!action.Target.IsAlive)
{
Console.WriteLine($"{action.Target.Name} is already defeated. {combatant.Name}'s turn is skipped.");
continue;
}
// Execute the action
action.Execute();
// Check if combat should end
if (CheckCombatEnd())
{
break;
}
}
// Display state at the end of the round
DisplayCombatState();
}
}
// Determine the order in which combatants take their turns
private void DetermineTurnOrder()
{
// Combine all combatants
turnOrder.Clear();
turnOrder.AddRange(playerTeam.Where(p => p.IsAlive));
turnOrder.AddRange(enemyTeam.Where(e => e.IsAlive));
// Sort by Speed (higher goes first)
turnOrder.Sort((a, b) => b.Speed.CompareTo(a.Speed));
Console.WriteLine("\n=== NEW ROUND ===");
Console.WriteLine("Turn order:");
foreach (Combatant combatant in turnOrder)
{
Console.WriteLine($"- {combatant.Name} (Speed: {combatant.Speed})");
}
Console.WriteLine();
}
// Check if combat should end
private bool CheckCombatEnd()
{
// Check if all players are defeated
bool allPlayersDead = playerTeam.All(p => !p.IsAlive);
if (allPlayersDead)
{
Console.WriteLine("\n=== DEFEAT ===");
Console.WriteLine("All players have been defeated!");
isCombatActive = false;
return true;
}
// Check if all enemies are defeated
bool allEnemiesDead = enemyTeam.All(e => !e.IsAlive);
if (allEnemiesDead)
{
Console.WriteLine("\n=== VICTORY ===");
Console.WriteLine("All enemies have been defeated!");
isCombatActive = false;
return true;
}
return false;
}
// Display the current state of combat
private void DisplayCombatState()
{
Console.WriteLine("\n=== COMBAT STATE ===");
Console.WriteLine("Player Team:");
foreach (Combatant player in playerTeam)
{
string status = player.IsAlive ? $"{player.CurrentHealth}/{player.MaxHealth} HP" : "DEFEATED";
Console.WriteLine($"- {player.Name}: {status}");
}
Console.WriteLine("\nEnemy Team:");
foreach (Combatant enemy in enemyTeam)
{
string status = enemy.IsAlive ? $"{enemy.CurrentHealth}/{enemy.MaxHealth} HP" : "DEFEATED";
Console.WriteLine($"- {enemy.Name}: {status}");
}
Console.WriteLine();
}
}
Step 3: Create Advanced Abilities and Status Effects
Let's expand our system with more interesting abilities and status effects:
// Area of effect attack
public class AreaAttack : Ability
{
public AreaAttack()
{
Name = "Area Attack";
Description = "Attack all enemies for reduced damage.";
}
public override List<Combatant> GetValidTargets(Combatant user, List<Combatant> allies, List<Combatant> enemies)
{
// This is a special case - we return one enemy to represent "all enemies"
// The actual targeting of all enemies happens in Execute
return enemies.Where(e => e.IsAlive).Take(1).ToList();
}
public override void Execute(Combatant user, Combatant target)
{
// Find all enemies (including the selected target)
List<Combatant> allEnemies = target is PlayerCharacter
? user.GetType().GetMethod("SelectAction").GetParameters()[0].GetValue(user) as List<Combatant>
: user.GetType().GetMethod("SelectAction").GetParameters()[1].GetValue(user) as List<Combatant>;
// Filter for alive enemies
List<Combatant> aliveEnemies = allEnemies.Where(e => e.IsAlive).ToList();
Console.WriteLine($"{user.Name} performs an area attack!");
// Calculate reduced damage (60% of normal attack)
int damage = (int)(user.Attack * 0.6);
// Apply damage to all enemies
foreach (Combatant enemy in aliveEnemies)
{
Console.WriteLine($"{enemy.Name} is hit!");
enemy.TakeDamage(damage);
}
}
}
// Poison attack
public class PoisonAttack : Ability
{
public PoisonAttack()
{
Name = "Poison Strike";
Description = "Attack an enemy and apply poison.";
}
public override List<Combatant> GetValidTargets(Combatant user, List<Combatant> allies, List<Combatant> enemies)
{
// Can only target enemies that are alive
return enemies.Where(e => e.IsAlive).ToList();
}
public override void Execute(Combatant user, Combatant target)
{
Console.WriteLine($"{user.Name} uses Poison Strike on {target.Name}!");
// Calculate damage (slightly less than basic attack)
int damage = (int)(user.Attack * 0.8);
// Apply damage
target.TakeDamage(damage);
// Apply poison status effect
target.AddStatusEffect(new PoisonEffect(3, user.Attack / 4));
}
}
// Defensive buff
public class DefenseBuff : Ability
{
public DefenseBuff()
{
Name = "Defend";
Description = "Increase defense for 3 turns.";
}
public override List<Combatant> GetValidTargets(Combatant user, List<Combatant> allies, List<Combatant> enemies)
{
// Can only target self or allies that are alive
return allies.Where(a => a.IsAlive).ToList();
}
public override void Execute(Combatant user, Combatant target)
{
Console.WriteLine($"{user.Name} casts Defense Buff on {target.Name}!");
// Apply defense buff status effect
target.AddStatusEffect(new DefenseBuffEffect());
}
}
// Defense buff status effect
public class DefenseBuffEffect : StatusEffect
{
private int defenseBonus;
public DefenseBuffEffect(int duration = 3, int defenseBonus = 5) : base(duration)
{
Name = "Defense Up";
this.defenseBonus = defenseBonus;
}
public override void Apply(Combatant target)
{
// Increase target's defense
target.Defense += defenseBonus;
Console.WriteLine($"{target.Name}'s defense increased by {defenseBonus}!");
}
public override void OnTurn(Combatant target)
{
// Decrease duration
Duration--;
}
public override void Remove(Combatant target)
{
// Remove defense bonus
target.Defense -= defenseBonus;
Console.WriteLine($"{target.Name}'s defense returned to normal.");
}
}
Step 4: Implement Advanced Enemy AI
Let's improve the enemy AI to make more strategic decisions:
// Advanced enemy with better AI
public class AdvancedEnemy : Enemy
{
public AdvancedEnemy(string name, int health, int attack, int defense, int speed)
: base(name, health, attack, defense, speed)
{
// Add more abilities
abilities.Add(new PoisonAttack());
abilities.Add(new AreaAttack());
abilities.Add(new DefenseBuff());
}
public override CombatAction SelectAction(List<Combatant> allies, List<Combatant> enemies)
{
// More strategic AI
// Get available abilities
List<Ability> availableAbilities = GetAbilities();
// If health is low (below 30%), prioritize defense
if (CurrentHealth < MaxHealth * 0.3)
{
Ability defenseBuff = availableAbilities.FirstOrDefault(a => a is DefenseBuff);
if (defenseBuff != null)
{
List<Combatant> validTargets = defenseBuff.GetValidTargets(this, allies, enemies);
// Find the ally with the lowest health as target
Combatant target = validTargets.OrderBy(t => (float)t.CurrentHealth / t.MaxHealth).FirstOrDefault();
if (target != null)
{
Console.WriteLine($"{Name} is using a defensive strategy!");
return new CombatAction(this, defenseBuff, target);
}
}
}
// If multiple enemies are alive, consider area attack
if (enemies.Count(e => e.IsAlive) > 1)
{
Ability areaAttack = availableAbilities.FirstOrDefault(a => a is AreaAttack);
if (areaAttack != null)
{
List<Combatant> validTargets = areaAttack.GetValidTargets(this, allies, enemies);
if (validTargets.Any())
{
Console.WriteLine($"{Name} is preparing an area attack!");
return new CombatAction(this, areaAttack, validTargets.First());
}
}
}
// Target the weakest enemy with poison or basic attack
Combatant weakestEnemy = enemies.Where(e => e.IsAlive)
.OrderBy(e => e.CurrentHealth)
.FirstOrDefault();
if (weakestEnemy != null)
{
// Check if enemy already has poison
bool isAlreadyPoisoned = false; // In a real implementation, check for poison status
if (!isAlreadyPoisoned)
{
Ability poisonAttack = availableAbilities.FirstOrDefault(a => a is PoisonAttack);
if (poisonAttack != null)
{
Console.WriteLine($"{Name} targets the weakest enemy with poison!");
return new CombatAction(this, poisonAttack, weakestEnemy);
}
}
// Fallback to basic attack
Ability basicAttack = availableAbilities.FirstOrDefault(a => a is BasicAttack);
if (basicAttack != null)
{
Console.WriteLine($"{Name} targets the weakest enemy!");
return new CombatAction(this, basicAttack, weakestEnemy);
}
}
// If all else fails, use random selection as fallback
return base.SelectAction(allies, enemies);
}
}
Step 5: Create the Main Program
Finally, let's create the main program to run our turn-based combat system:
public class Program
{
public static void Main()
{
Console.WriteLine("Welcome to the Turn-Based Combat System!");
// Create player team
List<Combatant> playerTeam = new List<Combatant>
{
new PlayerCharacter("Hero", 100, 20, 10, 15),
new PlayerCharacter("Mage", 70, 25, 5, 10)
};
// Add special abilities to players
((PlayerCharacter)playerTeam[0]).GetAbilities().Add(new DefenseBuff());
((PlayerCharacter)playerTeam[1]).GetAbilities().Add(new PoisonAttack());
// Create enemy team
List<Combatant> enemyTeam = new List<Combatant>
{
new AdvancedEnemy("Goblin Chief", 120, 15, 8, 12),
new Enemy("Goblin Warrior", 60, 12, 6, 8),
new Enemy("Goblin Archer", 40, 18, 4, 14)
};
// Create and start combat system
CombatSystem combatSystem = new CombatSystem(playerTeam, enemyTeam);
combatSystem.StartCombat();
Console.WriteLine("\nThanks for playing!");
}
}
Challenge Extensions
Once you've implemented the basic system, try these extensions to further develop your algorithmic skills:
-
Initiative System: Implement a more complex initiative system where Speed determines the probability of going first rather than a fixed order.
-
Critical Hits: Add a chance for attacks to critically hit, dealing extra damage.
-
Resource Management: Add a mana/energy system for abilities, requiring players to manage resources.
-
Equipment System: Create an equipment system that modifies character stats.
-
Elemental Damage: Implement an elemental system with strengths and weaknesses.
-
Experience and Leveling: Add an experience system where defeating enemies grants XP and levels.
-
More Complex AI: Enhance the enemy AI to consider team synergies and counter-strategies.
Algorithmic Concepts Applied
This challenge incorporates several algorithmic concepts:
- Object-Oriented Design: Using inheritance and polymorphism to create a flexible system
- State Management: Tracking combat state and character conditions
- Decision Trees: Used in the AI to make strategic decisions
- Priority Queues: Implicitly used in turn order determination
- Event-Driven Programming: Status effects trigger on specific events
- Strategy Pattern: Different abilities implement different strategies
- Factory Pattern: Creating different types of combatants and abilities
Conclusion
This turn-based combat system challenge brings together many of the algorithmic concepts and patterns we've explored in this module. By implementing this system, you've practiced:
- Breaking down a complex problem into manageable components
- Applying appropriate data structures and algorithms
- Implementing game mechanics using algorithmic patterns
- Creating a balanced and engaging system
These skills are directly applicable to game development in Unity, where you'll implement similar systems with the added dimension of visual representation and user interaction.
Remember that good game systems are a balance of technical implementation and game design. As you refine your combat system, consider not just how it works, but how it feels to play.