Vin Fletcher
The following is one possible solution to this challenge.
AttackAction.cs
/// <summary>
/// An action type that executes an attack on a target.
/// </summary>
public class AttackAction : IAction
{
// A random number generator for resolving random elements of attacks.
private static readonly Random _random = new Random();
// The attack to run.
private readonly IAttack _attack;
// The target of the attack.
private readonly Character _target;
/// <summary>
/// Creates a new attack action, capturing the attack and target of the attack.
/// </summary>
public AttackAction(IAttack attack, Character target)
{
_attack = attack;
_target = target;
}
/// <summary>
/// Runs the attack action.
/// </summary>
public void Run(Battle battle, Character character)
{
// Display that the attack is happening.
Console.WriteLine($"{character.Name} used {_attack.Name} on {_target.Name}.");
// Get the attack's damage for this specific attack and deal it out to the target.
AttackData data = _attack.Create();
// Some attacks could miss. Check to see if this attack missed the target. If it did, tell the user and be done.
if (_random.NextDouble() > data.ProbabilityOfHitting)
{
ColoredConsole.WriteLine($"{character.Name} MISSED!", ConsoleColor.DarkRed);
return;
}
_target.HP -= data.Damage;
// Display that the damage has been dealt and where the character's HP is at now.
Console.WriteLine($"{_attack.Name} dealt {data.Damage} damage to {_target.Name}.");
Console.WriteLine($"{_target.Name} is now at {_target.HP}/{_target.MaxHP} HP.");
// If the target dies because of the attack, remove it from the party and tell the user.
if (!_target.IsAlive)
{
battle.GetPartyFor(_target).Characters.Remove(_target);
Console.WriteLine($"{_target.Name} was defeated!");
if(_target.EquippedGear != null)
{
IGear acquiredGear = _target.EquippedGear;
battle.GetPartyFor(character).Gear.Add(acquiredGear);
ColoredConsole.WriteLine($"{character.Name}'s party has recovered {_target.Name}'s {acquiredGear.Name}.", ConsoleColor.Magenta);
}
}
}
}
Battle.cs
/// <summary>
/// Represents a single battle in the game.
/// </summary>
public class Battle
{
/// <summary>
/// The party of heroes.
/// </summary>
public Party Heroes { get; }
/// <summary>
/// The party of monsters.
/// </summary>
public Party Monsters { get; }
/// <summary>
/// Creates a new battle with the two parties involved.
/// </summary>
public Battle(Party heroes, Party monsters)
{
Heroes = heroes;
Monsters = monsters;
}
/// <summary>
/// Runs the battle to completion.
/// </summary>
public void Run()
{
// Run rounds until the outcome is known.
while (!IsOver)
{
// For each character in each party...
foreach (Party party in new[] { Heroes, Monsters })
{
foreach (Character character in party.Characters)
{
Console.WriteLine(); // Slight separation gap.
BattleRenderer.Render(this, character);
// Display who's turn it is.
Console.WriteLine($"{character.Name} is taking a turn...");
// Have the player in charge of the party pick an action for the character, and then run that action.
party.Player.ChooseAction(this, character).Run(this, character);
if (IsOver) break; // If the last action ended the battle, there is no need to go on to other characters.
}
if (IsOver) break; // If the last action ended the battle, there is no need to go on to other parties.
}
}
if (Heroes.Characters.Count > 0)
{
ColoredConsole.WriteLine("The HEROES have defeated the MONSTERS and looted their inventory.", ConsoleColor.Magenta);
TransferInventory();
}
}
private void TransferInventory()
{
foreach (IGear gear in Monsters.Gear)
{
ColoredConsole.WriteLine($"The HEROES have acquired {gear.Name}.", ConsoleColor.DarkMagenta);
Heroes.Gear.Add(gear);
}
foreach (IItem item in Monsters.Items)
{
ColoredConsole.WriteLine($"The HEROES have acquired {item.Name}.", ConsoleColor.DarkMagenta);
Heroes.Items.Add(item);
}
}
/// <summary>
/// Indicates whether the game is over or not. This is based on whether a party has no characters left to fight.
/// </summary>
public bool IsOver => Heroes.Characters.Count == 0 || Monsters.Characters.Count == 0;
/// <summary>
/// Gives you the party that the character is not in. The party that is the enemy of the character in question.
/// </summary>
public Party GetEnemyPartyFor(Character character) => Heroes.Characters.Contains(character) ? Monsters : Heroes;
/// <summary>
/// Gives you the party that the character is in.
/// </summary>
public Party GetPartyFor(Character character) => Heroes.Characters.Contains(character) ? Heroes : Monsters;
}
BattleRenderer.cs
public static class BattleRenderer
{
public static void Render(Battle battle, Character activeCharacter)
{
// Display the top banner.
ColoredConsole.WriteLine($"===================================================== BATTLE ====================================================", ConsoleColor.White);
// Display the heroes and equipped gear.
foreach (Character character in battle.Heroes.Characters)
{
string gearString = character.EquippedGear == null ? "" : $" [{character.EquippedGear.Name}]";
ConsoleColor color = character == activeCharacter ? ConsoleColor.Yellow : ConsoleColor.Gray;
ColoredConsole.WriteLine($"{character.Name + gearString,-45} ({character.HP,3}/{character.MaxHP,-3})", color);
}
// Display the middle banner.
ColoredConsole.WriteLine("------------------------------------------------------ VS -------------------------------------------------------", ConsoleColor.White);
// Display the monsters and equipped gear.
foreach (Character character in battle.Monsters.Characters)
{
string gearString = character.EquippedGear == null ? "" : $" [{character.EquippedGear.Name}]";
ConsoleColor color = character == activeCharacter ? ConsoleColor.Yellow : ConsoleColor.Gray;
ColoredConsole.WriteLine($" {character.Name + gearString,45} ({character.HP,3}/{character.MaxHP,-3})", color);
}
// Display the bottom banner.
ColoredConsole.WriteLine("=================================================================================================================", ConsoleColor.White);
}
}
Character.cs
/// <summary>
/// Defines what all characters in the game have in common.
/// </summary>
public abstract class Character
{
/// <summary>
/// The name of the character.
/// </summary>
public abstract string Name { get; }
/// <summary>
/// The character's standard attack.
/// </summary>
public abstract IAttack StandardAttack { get; }
/// <summary>
/// The gear the character has attached. If it exists, it provides a second special attack.
/// </summary>
public IGear? EquippedGear { get; set; }
// Stores the hit points remaining for the character.
private int _hp;
/// <summary>
/// Gets or sets the current hit points for the character, ensuring it always stays at or above 0 and at or below MaxHP.
/// </summary>
public int HP
{
get => _hp;
set => _hp = Math.Clamp(value, 0, MaxHP);
}
/// <summary>
/// The maximum HP that the character has.
/// </summary>
public int MaxHP { get; }
/// <summary>
/// Indicates if the character is alive or not.
/// </summary>
public bool IsAlive => HP > 0;
/// <summary>
/// Creates a new character with a specific amount of HP. The character will start with both HP and MaxHP at this level.
/// </summary>
public Character(int hp)
{
MaxHP = hp;
HP = hp;
}
}
ColoredConsole.cs
/// <summary>
/// A class that provides some convenience methods over the top of the console window for displaying text
/// with color.
/// </summary>
public static class ColoredConsole
{
/// <summary>
/// Writes a line of text in a specific color.
/// </summary>
public static void WriteLine(string text, ConsoleColor color)
{
ConsoleColor previousColor = Console.ForegroundColor;
Console.ForegroundColor = color;
Console.WriteLine(text);
Console.ForegroundColor = previousColor;
}
/// <summary>
/// Writes some text (no new line) in a specific color.
/// </summary>
public static void Write(string text, ConsoleColor color)
{
ConsoleColor previousColor = Console.ForegroundColor;
Console.ForegroundColor = color;
Console.Write(text);
Console.ForegroundColor = previousColor;
}
/// <summary>
/// Asks the user a question and on the same line, gets a reply back, switching the user's response
/// to a cyan color so it stands out.
/// </summary>
/// <param name="questionToAsk"></param>
/// <returns></returns>
public static string Prompt(string questionToAsk)
{
ConsoleColor previousColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Gray;
Console.Write(questionToAsk + " ");
Console.ForegroundColor = ConsoleColor.Cyan;
string input = Console.ReadLine() ?? ""; // If we got null, use empty string instead.
Console.ForegroundColor = previousColor;
return input;
}
}
ComputerPlayer.cs
/// <summary>
/// A simple computer player--an AI. The computer follows a simple set of rules to decide which action to take.
/// </summary>
public class ComputerPlayer : IPlayer
{
private static Random _random = new Random();
public IAction ChooseAction(Battle battle, Character character)
{
// Pretend to think for a bit.
Thread.Sleep(500);
// If there is a potion, and if the character's health is low (less than 50%), then 25% of the time, use a potion.
bool hasPotion = battle.GetPartyFor(character).Items.Count > 0; // Not quite right. This assumes all items are potions, which is true for now but could change later.
bool isHPUnderThreshold = character.HP / (float)character.MaxHP < 0.5;
if (hasPotion && isHPUnderThreshold && _random.NextDouble() < 0.25)
return new UseItemAction(battle.GetPartyFor(character).Items[0]);
if (character.EquippedGear == null && battle.GetPartyFor(character).Gear.Count > 0 && _random.NextDouble() < 0.5)
return new EquipGearAction(battle.GetPartyFor(character).Gear[0]);
// If there's something to attack, attack it with your standard attack.
List<Character> potentialTargets = battle.GetEnemyPartyFor(character).Characters;
if (potentialTargets.Count > 0)
{
// Prefer attacks from equipped gear to the standard attack.
if (character.EquippedGear != null) return new AttackAction(character.EquippedGear.Attack, battle.GetEnemyPartyFor(character).Characters[0]);
else return new AttackAction(character.StandardAttack, battle.GetEnemyPartyFor(character).Characters[0]);
}
// If there's nothing better to do, do nothing.
return new DoNothingAction();
}
}
ConsolePlayer.cs
/// <summary>
/// A player that retrieves commands from the human through the console window.
/// </summary>
public class ConsolePlayer : IPlayer
{
public IAction ChooseAction(Battle battle, Character character)
{
// This uses a menu-based approach. We create the choices from the menu, including their name, whether they are enabled, and
// what action to pick if they are enabled and chosen.
// After that, we display the menu and ask the user to make a selection.
// If the selected option is enabled, use the action associated with it.
List<MenuChoice> menuChoices = CreateMenuOptions(battle, character);
for (int index = 0; index < menuChoices.Count; index++)
ColoredConsole.WriteLine($"{index + 1} - {menuChoices[index].Description}", menuChoices[index].Enabled ? ConsoleColor.Gray : ConsoleColor.DarkGray);
string choice = ColoredConsole.Prompt("What do you want to do?");
int menuIndex = Convert.ToInt32(choice) - 1;
if (menuChoices[menuIndex].Enabled) return menuChoices[menuIndex].Action!; // Checking if it is enabled is as good as a null check.
return new DoNothingAction(); // <-- This is actually fairly unforgiving. Typing in garbage or attempting to use a disabled option results in doing nothing. It would be better to try again. (Maybe that can be done as a Making It Your Own challenge.
}
private List<MenuChoice> CreateMenuOptions(Battle battle, Character character)
{
Party currentParty = battle.GetPartyFor(character);
Party otherParty = battle.GetEnemyPartyFor(character);
List<MenuChoice> menuChoices = new List<MenuChoice>();
if (character.EquippedGear != null)
{
IGear gear = character.EquippedGear;
IAttack specialAttack = gear.Attack;
if (otherParty.Characters.Count > 0)
menuChoices.Add(new MenuChoice($"Special Attack ({specialAttack.Name} with {gear.Name})", new AttackAction(specialAttack, otherParty.Characters[0])));
else
menuChoices.Add(new MenuChoice($"Special Attack ({specialAttack.Name} with {gear.Name})", null));
}
// Add the standard attack as an option.
if (otherParty.Characters.Count > 0)
menuChoices.Add(new MenuChoice($"Standard Attack ({character.StandardAttack.Name})", new AttackAction(character.StandardAttack, otherParty.Characters[0])));
else
menuChoices.Add(new MenuChoice($"Standard Attack ({character.StandardAttack.Name})", null));
// Add using the potion as an item as an option.
if (currentParty.Items.Count > 0)
menuChoices.Add(new MenuChoice($"Use Potion ({currentParty.Items.Count})", new UseItemAction(currentParty.Items[0])));
else
menuChoices.Add(new MenuChoice($"Use Potion (0)", null));
// Give the player the option to equip any gear in the party inventory.
foreach (IGear gear in currentParty.Gear)
menuChoices.Add(new MenuChoice($"Equip {gear.Name}", new EquipGearAction(gear)));
// Add doing nothing as an option.
menuChoices.Add(new MenuChoice("Do Nothing", new DoNothingAction()));
return menuChoices;
}
}
public record MenuChoice(string Description, IAction? Action)
{
public bool Enabled => Action != null;
}
DoNothingAction.cs
/// <summary>
/// An action type that does nothing (besides say that the character did nothing).
/// </summary>
public class DoNothingAction : IAction
{
public void Run(Battle battle, Character actor) => Console.WriteLine($"{actor.Name} did NOTHING.");
}
EquipGearAction.cs
public class EquipGearAction : IAction
{
private readonly IGear _gear;
public EquipGearAction(IGear gear) => _gear = gear;
public void Run(Battle battle, Character actor)
{
Party party = battle.GetPartyFor(actor);
// If we already have equipped gear, unequip it first.
if (actor.EquippedGear != null)
{
Console.WriteLine($"{actor.Name} has unequipped {_gear.Name}.");
party.Gear.Add(actor.EquippedGear);
actor.EquippedGear = null;
}
// Tell the user that the character is equipping the gear.
Console.WriteLine($"{actor.Name} equipped {_gear.Name}.");
actor.EquippedGear = _gear;
party.Gear.Remove(_gear);
}
}
IAction.cs
/// <summary>
/// Defines what all action possibilities in the system must look like.
/// </summary>
public interface IAction
{
/// <summary>
/// Runs the action, giving the action the full context of the battle and the character who is running the action.
/// If an action needs additional information, then they should typically "request" those by having parameters
/// for them in their constructors, and save them to fields for use when `Run` is called.
/// </summary>
void Run(Battle battle, Character actor);
}
IAttack.cs
/// <summary>
/// Represents an attack that a character might have. Each attack has a name and the ability
/// to produce attack data by request, for when somebody uses the attack.
/// </summary>
public interface IAttack
{
/// <summary>
/// The name of the attack.
/// </summary>
string Name { get; }
/// <summary>
/// Creates new attack data. Called when a character uses an attack.
/// </summary>
AttackData Create();
}
/// <summary>
/// The collection of information that defines a specific usage or occurence of an attack.
/// </summary>
public record AttackData(int Damage, double ProbabilityOfHitting = 1.0);
IGear.cs
public interface IGear
{
string Name { get; }
IAttack Attack { get; }
}
IPlayer.cs
/// <summary>
/// Represents a player--one of entities that control characters and pick actions for them when it is the character's turn.
/// </summary>
public interface IPlayer
{
/// <summary>
/// Allows the player to choose an action for a given character. The battle is provided as context, so that it has the
/// information it needs to make good decisions.
/// </summary>
IAction ChooseAction(Battle battle, Character character);
}
Items.cs
public interface IItem
{
string Name { get; }
void Use(Battle battle, Character user);
}
public class HealthPotion : IItem
{
public string Name => "HEALTH POTION";
public void Use(Battle battle, Character user)
{
user.HP += 10;
Console.WriteLine($"{user.Name}'s HP was increased by 10."); // This is not always right. If somebody only recovers 1 HP, it would be cooler if it said "HP was increased by 1." This is another option for a Making It Your Own challenge.
}
}
Party.cs
/// <summary>
/// Represents a party (of either heroes or monsters). Contains the characters in the party and the player that
/// is running the show for the party.
/// </summary>
public class Party
{
/// <summary>
/// The player that is making decisions for this party.
/// </summary>
public IPlayer Player { get; }
/// <summary>
/// The list of characters that are still alive in the party.
/// </summary>
public List<Character> Characters { get; } = new List<Character>();
/// <summary>
/// The items the party has in their collective inventory.
/// </summary>
public List<IItem> Items { get; } = new List<IItem>();
/// <summary>
/// The gear the party has in their collective inventory.
/// </summary>
public List<IGear> Gear { get; } = new List<IGear>();
/// <summary>
/// Creates a new party with the given player controlling it.
/// </summary>
/// <param name="player">The player that will control this party.</param>
public Party(IPlayer player)
{
Player = player;
}
}
Program.cs
// Get the name from the player. I've called ToUpper on this because most elements in the game use ALL CAPS to refer to proper nouns. This keeps it consistent.
string name = ColoredConsole.Prompt("What is your name?").ToUpper();
// Let the user pick a gameplay mode and then create players based on the choice they made.
Console.WriteLine("Game Mode Selection:");
Console.WriteLine("1 - Human vs. Computer");
Console.WriteLine("2 - Computer vs. Computer");
Console.WriteLine("3 - Human vs. Human");
string choice = ColoredConsole.Prompt("What mode do you want to use?");
IPlayer player1, player2;
if (choice == "1") { player1 = new ConsolePlayer(); player2 = new ComputerPlayer(); }
else if (choice == "2") { player1 = new ComputerPlayer(); player2 = new ComputerPlayer(); }
else { player1 = new ConsolePlayer(); player2 = new ConsolePlayer(); }
// Construct the hero party. Put Player 1 in charge of this party.
Party heroes = new Party(player1);
heroes.Characters.Add(new TheTrueProgrammer(name));
heroes.Characters.Add(new VinFletcher());
heroes.Items.Add(new HealthPotion());
heroes.Items.Add(new HealthPotion());
heroes.Items.Add(new HealthPotion());
// Create all the monster parties now. (We could create one at a time, being able to just iterate over an array was too convenient in this case.)
List<Party> monsterParties = new List<Party> { CreateMonsterParty1(player2), CreateMonsterParty2(player2), CreateMonsterParty3(player2) };
// Loop through all the battles (we're tentatively assuming the hero is going to win them all...
for (int battleNumber = 0; battleNumber < monsterParties.Count; battleNumber++)
{
// Create the battle between the two and run it to completion.
Party monsters = monsterParties[battleNumber];
Battle battle = new Battle(heroes, monsters);
battle.Run();
// If our assumption was wrong and the heroes all died off, then end the game.
if (heroes.Characters.Count == 0) break;
}
// Display who won.
if (heroes.Characters.Count > 0) ColoredConsole.WriteLine("You have defeated the Uncoded One's forces! You have won the battle!", ConsoleColor.Green);
else ColoredConsole.WriteLine("You have been defeated. The Uncoded One has won.", ConsoleColor.Red);
// Create the monster party for Battle 1
Party CreateMonsterParty1(IPlayer controllingPlayer)
{
Party monsters = new Party(controllingPlayer);
monsters.Characters.Add(new Skeleton { EquippedGear = new Dagger() });
return monsters;
}
// Create the monster party for Battle 2
Party CreateMonsterParty2(IPlayer controllingPlayer)
{
Party monsters = new Party(controllingPlayer);
monsters.Characters.Add(new Skeleton());
monsters.Characters.Add(new Skeleton());
monsters.Gear.Add(new Dagger());
monsters.Gear.Add(new Dagger());
return monsters;
}
// Create the monster party for Battle 3
Party CreateMonsterParty3(IPlayer controllingPlayer)
{
Party monsters = new Party(controllingPlayer);
monsters.Characters.Add(new TheUncodedOne());
return monsters;
}
Skeleton.cs
/// <summary>
/// A character that represents a skeleton--a simple monster type with a bone crunch attack.
/// </summary>
public class Skeleton : Character
{
public override string Name => "SKELETON";
public override IAttack StandardAttack { get; } = new BoneCrunch();
public Skeleton() : base(5) { }
}
public class Dagger : IGear
{
public string Name => "DAGGER";
public IAttack Attack { get; } = new Stab();
}
public class Stab : IAttack
{
public string Name => "STAB";
public AttackData Create() => new AttackData(1);
}
/// <summary>
/// An attack that deals 0 or 1 damage randomly.
/// </summary>
public class BoneCrunch : IAttack
{
private static readonly Random _random = new Random();
public string Name => "BONE CRUNCH";
public AttackData Create() => new AttackData(_random.Next(2));
}
TheTrueProgrammer.cs
/// <summary>
/// The main hero and player character of the game.
/// </summary>
public class TheTrueProgrammer : Character
{
public override string Name { get; }
public TheTrueProgrammer(string name) : base(25)
{
Name = name;
EquippedGear = new Sword();
}
public override IAttack StandardAttack { get; } = new Punch();
}
public class Sword : IGear
{
public string Name => "SWORD";
public IAttack Attack { get; } = new Slash();
}
public class Slash : IAttack
{
public string Name => "SLASH";
public AttackData Create() => new AttackData(2);
}
/// <summary>
/// Punch is a simple attack that reliably deals 1 damage.
/// </summary>
public class Punch : IAttack
{
public string Name => "PUNCH";
public AttackData Create() => new AttackData(1);
}
TheUncodedOne.cs
/// <summary>
/// The character that represents the big bad evil in the game.
/// </summary>
public class TheUncodedOne : Character
{
public override string Name => "THE UNCODED ONE";
public TheUncodedOne() : base(15) { }
public override IAttack StandardAttack { get; } = new Unraveling();
}
/// <summary>
/// An attack that deals 0 to 2 damage randomly.
/// </summary>
public class Unraveling : IAttack
{
private static readonly Random _random = new Random();
public string Name => "UNRAVELING";
public AttackData Create() => new AttackData(_random.Next(3));
}
UseItemAction.cs
/// <summary>
/// An action type that uses an item.
/// </summary>
public class UseItemAction : IAction
{
// The item to use.
private readonly IItem _item;
/// <summary>
/// Creates a new UseItemAction with the item to use.
/// </summary>
public UseItemAction(IItem item) => _item = item;
public void Run(Battle battle, Character actor)
{
// Tell the user that the character is using the item.
Console.WriteLine($"{actor.Name} used {_item.Name}.");
// Use the item.
_item.Use(battle, actor);
// Items are consumed after use.
battle.GetPartyFor(actor).Items.Remove(_item);
}
}
VinFletcher.cs
/// <summary>
/// A hero companion that has a bow that misses sometimes, but when he hits, deals a lot of damage.
/// </summary>
public class VinFletcher : Character
{
public override string Name => "VIN FLETCHER";
public override IAttack StandardAttack { get; } = new Punch();
public VinFletcher() : base(15) => EquippedGear = new VinsBow();
}
public class VinsBow : IGear
{
public string Name => "VIN'S BOW";
public IAttack Attack => new QuickShot();
}
public class QuickShot : IAttack
{
public string Name => "QUICK SHOT";
public AttackData Create() => new AttackData(3, 0.5);
}