Advent of Code Day 21–Beating the Boss
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.