Posted in:

In today’s Advent of Code challenge, we’re fighting against the boss. One of the big takeaways from my C# solution was that immutable objects can protect you from certain classes of bug.

Here’s the final immutable version of my C# code, with the benefit being that we don’t need to recalculate the options list for part b.

void Main()
{
    var boss = new PlayerStatus(103, 9, 2, 0);
    var options = GetPlayerOptions(100);
        options
        .Where(x => Battle(x, boss))
        .OrderBy(x => x.GoldSpent)
        .First() //.GoldSpent
        .Dump("a"); // 121

    options
    .Where(x => !Battle(x, boss))
    .OrderByDescending(x => x.GoldSpent)
    .First().GoldSpent
    .Dump("b"); // 201
}

class ShopItem
{
    public ShopItem(string name, int cost, int damage, int armor)
    {
        Name = name;
        Cost = cost;
        Damage = damage;
        Armor = armor;
    }
    public string Name { get; }
    public int Cost { get; }
    public int Damage { get; }
    public int Armor { get; }
}

List<ShopItem> weapons = new List<ShopItem>()
{
    new ShopItem("Dagger",8,4,0),
    new ShopItem("Shortsword",10,5,0),
    new ShopItem("Warhammer",25,6,0),
    new ShopItem("Longsword",40,7,0),
    new ShopItem("Greataxe",74,8,0),
};

List<ShopItem> armory = new List<ShopItem>()
{
    new ShopItem("Leather",13,0,1),
    new ShopItem("Chainmail",31,0,2),
    new ShopItem("Splintmail",53,0,3),
    new ShopItem("Bandedmail",75,0,4),
    new ShopItem("Platemail",102,0,5),
};

IEnumerable<PlayerStatus> GetPlayerOptions(int hitPoints)
{
    var startStatus = new PlayerStatus(hitPoints,0,0,0);
    foreach (var weapon in weapons)
    {
        var ps = startStatus.PowerupWith(weapon);
        yield return ps;
        foreach (var powerup in AddRings(ps))
            yield return powerup;
        foreach (var armor in armory)
        {
            var ps2 = ps.PowerupWith(armor);
            yield return ps2;
            foreach (var powerup in AddRings(ps2))
                yield return powerup;
        }
    }
}

List<ShopItem> rings = new List<ShopItem>()
{
    new ShopItem("Damage +1", 25,1,0),
    new ShopItem("Damage +2", 50,2,0),
    new ShopItem("Damage +3", 100,3,0),
    new ShopItem("Defense +1", 20,0,1),
    new ShopItem("Defense +2", 40,0,2),
    new ShopItem("Defense +3", 80,0,3)
};

IEnumerable<PlayerStatus> AddRings(PlayerStatus status)
{
    foreach (var ring1 in rings)
    {
        var with1Ring = status.PowerupWith(ring1);
        yield return with1Ring;
        foreach (var ring2 in rings.Where(r => r != ring1))
        {
            yield return with1Ring.PowerupWith(ring2);
        }
    }
}

bool Battle(PlayerStatus player, PlayerStatus boss, bool debug = false)
{
    while (player.HitPoints > 0 && boss.HitPoints > 0)
    {
        boss = boss.HitBy(player);
        if (debug) Console.WriteLine("Boss: {0}",boss.HitPoints);
        if (boss.HitPoints <= 0) break;
        player = player.HitBy(boss);
        if (debug) Console.WriteLine("Player: {0}", player.HitPoints);
    }
    return player.HitPoints > 0;
}

class PlayerStatus
{
    public PlayerStatus(int hp, int d, int a, int g, string s = "")
    {
        HitPoints = hp;
        Damage = d;
        Armor = a;
        GoldSpent = g;
        Setup = s;
    }
    public int HitPoints { get; }
    public int Damage { get; }
    public int Armor { get;  }
    public int GoldSpent { get; }
    public string Setup { get; }

    public PlayerStatus PowerupWith(ShopItem item)
    {
        return new PlayerStatus(HitPoints, Damage + item.Damage, Armor + item.Armor, GoldSpent + item.Cost, Setup + "," + item.Name);
    }

    public PlayerStatus HitBy(PlayerStatus opponent)
    {
        return new PlayerStatus(HitPoints - opponent.Damage +Armor, Damage, Armor, GoldSpent, Setup);
    }
}

Obviously in F#, it’s no effort at all to make our types immutable, and the with keyword makes them easier to work with. Our battle function also is recursive, to help us avoid any mutable state.

type ShopItem = { Name : string; Cost: int; Damage: int; Armor: int }
type Player = {HitPoints : int; Damage: int; Armor: int; GoldSpent: int; Inventory: string list }
let powerupWith (player:Player) (item:ShopItem) = 
    { player with Damage = player.Damage + item.Damage; 
                    Armor = player.Armor + item.Armor;
                    GoldSpent = player.GoldSpent + item.Cost;
                    Inventory = (item.Name)::(player.Inventory)}
let hitBy (player:Player) (opponent:Player) = { player with HitPoints = player.HitPoints - opponent.Damage + player.Armor }

let weapons = [
    {Name="Dagger";Cost=8;Damage=4;Armor=0};
    {Name="Shortsword";Cost=10;Damage=5;Armor=0};
    {Name="Warhammer";Cost=25;Damage=6;Armor=0};
    {Name="Longsword";Cost=40;Damage=7;Armor=0};
    {Name="Greataxe";Cost=74;Damage=8;Armor=0};
    ]
let armory = [
    {Name="Leather";Cost=13;Damage=0;Armor=1};
    {Name="Chainmail";Cost=31;Damage=0;Armor=2};
    {Name="Splintmail";Cost=53;Damage=0;Armor=3};
    {Name="Bandedmail";Cost=75;Damage=0;Armor=4};
    {Name="Platemail";Cost=102;Damage=0;Armor=5};
    ]
let rings = [
    {Name="Damage +1";  Cost=25;  Damage=1; Armor=0};
    {Name="Damage +2";  Cost=50;  Damage=2; Armor=0};
    {Name="Damage +3";  Cost=100; Damage=3; Armor=0};
    {Name="Defense +1"; Cost=20;  Damage=0; Armor=1};
    {Name="Defense +2"; Cost=40;  Damage=0; Armor=2};
    {Name="Defense +3"; Cost=80;  Damage=0; Armor=3}
]

let addRings player = seq {
    for ring1 in rings do
        let with1Ring = powerupWith player ring1
        yield with1Ring
        for ring2 in rings |> Seq.except [ring1] do
            yield powerupWith with1Ring ring2
    }
    
let getOptions hitPoints = seq {
    let startStatus = { HitPoints = hitPoints; Damage =0; Armor = 0; GoldSpent = 0; Inventory = []}
    for weapon in weapons do
        let ps = powerupWith startStatus weapon
        yield ps
        yield! addRings ps
        for armor in armory do
            let ps2 = powerupWith ps armor
            yield ps2;
            yield! addRings ps2
}

let rec battle boss player =
    let b2 = hitBy boss player
    //printfn "Boss %d" b2.HitPoints
    if b2.HitPoints > 0 then
        let p2 = hitBy player boss
        //printfn "Player %d" p2.HitPoints
        if p2.HitPoints > 0 then
            battle b2 p2
        else false
    else true

let boss = { HitPoints = 103; Damage= 9; Armor = 2; GoldSpent = 0; Inventory = [] }

let getGold p = p.GoldSpent
getOptions 100 |> Seq.filter (battle boss) |> Seq.minBy getGold |> getGold |> printfn "a: %d"
getOptions 100 |> Seq.filter ((battle boss) >> not) |> Seq.maxBy getGold |> getGold |> printfn "b: %d"

As I said in the video, I haven’t attempted to refactor either the C# or F# versions for succinctness like I’ve done with a few puzzles in the past. Instead, I decided that today’s code would be written in the way I might write it if this was the first step in creating my own game, with classes / records representing the players and inventory items.

Want to learn more about LINQ? Be sure to check out my Pluralsight course LINQ Best Practices.