From 4d1477d7c1f4bf0f41a7c80e2389c5c9051cce52 Mon Sep 17 00:00:00 2001 From: Fabian Schmidt Date: Mon, 2 Dec 2024 15:51:01 +0100 Subject: [PATCH] y2015d22 finally, alot of small things to watch out for --- day22.js | 179 ++++++++++++++++++++++++++++++++++ justfile | 2 +- y2015/src/days/d22.rs | 220 ++++++++++++++++++++++++++++++++---------- 3 files changed, 347 insertions(+), 54 deletions(-) create mode 100644 day22.js diff --git a/day22.js b/day22.js new file mode 100644 index 0000000..11e7203 --- /dev/null +++ b/day22.js @@ -0,0 +1,179 @@ +'use strict'; + +var bossStats = { + hp: 71, + damageAmt: 10, +} + +class Player { + constructor(initial, isWizard) { + this.history = []; + this.initial = initial; + this.isWizard = !!isWizard; + + if (this.isWizard) { + this.spells = [ + { + cost: 53, + effect: (m, o) => o.damage(4), + }, + { + cost: 73, + effect: (m, o) => { o.damage(2); m.hp += 2; }, + }, + { + cost: 113, + start: (m, o) => m.armor += 7, + effect: (m, o) => { }, + end: (m, o) => m.armor -= 7, + duration: 6, + }, + { + cost: 173, + effect: (m, o) => o.damage(3), + duration: 6, + }, + { + cost: 229, + effect: (m, o) => m.mana += 101, + duration: 5, + }, + ]; + } + + this.start(); + } + + attack(opponent, spellIdx) { + if (!this.isWizard) { + opponent.damage(this.damageAmt); + } else { + this.history.push(spellIdx); + var spell = this.spells[spellIdx]; + this.spent += spell.cost; + this.mana -= spell.cost; + + if (spell.duration) { + var newSpell = { + idx: spellIdx, + effect: spell.effect, + duration: spell.duration, + }; + if (spell.start) { + spell.start(this, opponent); + } + if (spell.end) { + newSpell.end = spell.end; + } + this.activeSpells.push(newSpell); + } else { + spell.effect(this, opponent); + } + } + } + + damage(n) { + this.hp -= Math.max(1, n - this.armor); + } + + duplicate() { + var newPlayer = new Player(this.initial, this.isWizard); + newPlayer.hp = this.hp; + newPlayer.spent = this.spent; + newPlayer.armor = this.armor; + newPlayer.turn = this.turn; + for (var i = 0; i < this.activeSpells.length; i++) { + newPlayer.activeSpells.push(Object.assign({}, this.activeSpells[i])); + } + for (var i = 0; i < this.history.length; i++) { + newPlayer.history.push(this.history[i]); + } + + if (this.isWizard) + newPlayer.mana = this.mana; + else + newPlayer.damageAmt = this.damageAmt; + + return newPlayer; + } + + takeTurn(opponent) { + this.turn++; + + for (var i = 0; i < this.activeSpells.length; i++) { + var spell = this.activeSpells[i]; + + if (spell.duration > 0) { + spell.effect(this, opponent); + spell.duration--; + + if (spell.duration === 0 && spell.end) { + spell.end(this, opponent); + } + } + } + } + + start() { + this.hp = this.initial.hp; + this.spent = 0; + this.armor = 0; + this.turn = 0; + this.activeSpells = []; + if (this.isWizard) + this.mana = this.initial.mana; + else + this.damageAmt = this.initial.damageAmt; + } +} + + +var me = new Player({ hp: 50, mana: 500 }, true); +var boss = new Player(bossStats); + +var cheapestSpent = Infinity; + +function playAllGames(me, boss, partTwo, depth) { + depth = depth || 0; + for (var i = 0; i < me.spells.length; i++) { + var spellMatch = false; + for (var j = 0; j < me.activeSpells.length; j++) { + if (me.activeSpells[j].duration > 1 && i === me.activeSpells[j].idx) { + spellMatch = true;; + } + } + if (spellMatch) + continue; + if (me.spells[i].cost > me.mana) { + continue; + } + + var newMe = me.duplicate(); + var newBoss = boss.duplicate(); + + if (partTwo) + newMe.hp--; + + newMe.takeTurn(newBoss); + newBoss.takeTurn(newMe); + newMe.attack(newBoss, i); + + newMe.takeTurn(newBoss); + newBoss.takeTurn(newMe); + newBoss.attack(newMe); + + + if (newBoss.hp <= 0) { + cheapestSpent = Math.min(cheapestSpent, newMe.spent); + } + + if (newMe.hp > (partTwo ? 1 : 0) && newBoss.hp > 0 && newMe.spent < cheapestSpent) + playAllGames(newMe, newBoss, partTwo, depth + 1); + } +} + +playAllGames(me, boss); +console.log('Part One:', cheapestSpent); +cheapestSpent = Infinity; +playAllGames(me, boss, true); +console.log('Part Two:', cheapestSpent); diff --git a/justfile b/justfile index 8ae16d2..78696dc 100644 --- a/justfile +++ b/justfile @@ -5,4 +5,4 @@ run year day: cargo run --package y{{year}} --bin d{{day}} test year day *part='1': - cargo test --package y{{year}} days::d{{day}}::tests::part{{part}} -- --nocapture + cargo test --package y{{year}} --lib days::d{{day}}::tests::part{{part}} -- --nocapture diff --git a/y2015/src/days/d22.rs b/y2015/src/days/d22.rs index e81b0f5..930ddc1 100644 --- a/y2015/src/days/d22.rs +++ b/y2015/src/days/d22.rs @@ -24,20 +24,20 @@ pub fn process_part1(input: &str) -> u32 { }; let mut smallest = u32::MAX; let mut leafs = vec![start]; - let mut test = 0; println!("start"); + // Playing, Win, Loss + let mut stats = (0, 0, 0); loop { - println!("iter: {test},"); let current_leafs = leafs.clone(); - println!("leaf count: {}", current_leafs.len()); leafs.clear(); // create potential rounds for leaf in current_leafs { if leaf.state == State::Playing { - //println!("try for leaf"); for spell in Spell::get_all() { match spell.name { - SpellID::MagicMissile | SpellID::Drain => {} + SpellID::MagicMissile | SpellID::Drain => { + leafs.push(leaf.use_spell(spell, false)); + } SpellID::Shield => { if !leaf .player @@ -45,68 +45,151 @@ pub fn process_part1(input: &str) -> u32 { .iter() .any(|effect| effect.name == SpellID::Shield && effect.duration > 1) { - continue; + leafs.push(leaf.use_spell(spell, false)); } } SpellID::Poison => { if !leaf - .player + .boss .status_effects .iter() .any(|effect| effect.name == SpellID::Poison && effect.duration > 1) { - continue; + leafs.push(leaf.use_spell(spell, false)); } } SpellID::Recharge => { if !leaf.player.status_effects.iter().any(|effect| { effect.name == SpellID::Recharge && effect.duration > 1 }) { - continue; + leafs.push(leaf.use_spell(spell, false)); } } } - leafs.push(leaf.use_spell(spell)); } } + //leafs.iter().for_each(|leaf| println!("{:?}", leaf)); } + println!("leaf count: {}", leafs.len()); + stats.0 = 0; for leaf in &leafs { if leaf.state == State::Win && leaf.spent_mana < smallest { smallest = leaf.spent_mana; } + match leaf.state { + State::Playing => stats.0 += 1, + State::Win => stats.1 += 1, + State::Loss => stats.2 += 1, + }; //println!( // "player hp: {}, boss hp: {}, spent mana so far: {}", // leaf.player.hp, leaf.boss.hp, leaf.spent_mana //); } - leafs = leafs - .clone() - .iter() - .filter(|&leaf| leaf.state == State::Playing || leaf.spent_mana <= smallest) - .map(|leaf| leaf.to_owned()) - .collect(); + println!("Playing, win, loss: {stats:?}"); + leafs.retain(|leaf| leaf.state == State::Playing && leaf.spent_mana < smallest); if leafs.is_empty() { break; } - // already know it's higher than this - if smallest <= 2000 { - let mut count_losses = 0; - for leaf in leafs { - if leaf.state == State::Loss { - count_losses += 1; - } - } - println!("Losses: {count_losses}"); - break; - } - test += 1; println!(" smallest: {smallest}"); } smallest } pub fn process_part2(input: &str) -> u32 { - 0 + let mut boss = Character::default(); + input.lines().for_each(|line| { + let (attribute, value) = line.split_once(": ").unwrap(); + if attribute == "Hit Points" { + boss.hp = value.parse::().unwrap(); + } else if attribute == "Damage" { + boss.damage = value.parse::().unwrap(); + } + }); + let player = Character { + hp: 50, + mana: 500, + ..Default::default() + }; + // Initial state, nothing happened yet + let start = RoundNode { + player, + boss, + spent_mana: 0, + state: State::Playing, + }; + let mut smallest = u32::MAX; + let mut leafs = vec![start]; + println!("start"); + // Playing, Win, Loss + let mut stats = (0, 0, 0); + loop { + let current_leafs = leafs.clone(); + leafs.clear(); + // create potential rounds + for leaf in current_leafs { + if leaf.state == State::Playing { + for spell in Spell::get_all() { + match spell.name { + SpellID::MagicMissile | SpellID::Drain => { + leafs.push(leaf.use_spell(spell, true)); + } + SpellID::Shield => { + if !leaf + .player + .status_effects + .iter() + .any(|effect| effect.name == SpellID::Shield && effect.duration > 1) + { + leafs.push(leaf.use_spell(spell, true)); + } + } + SpellID::Poison => { + if !leaf + .boss + .status_effects + .iter() + .any(|effect| effect.name == SpellID::Poison && effect.duration > 1) + { + leafs.push(leaf.use_spell(spell, true)); + } + } + SpellID::Recharge => { + if !leaf.player.status_effects.iter().any(|effect| { + effect.name == SpellID::Recharge && effect.duration > 1 + }) { + leafs.push(leaf.use_spell(spell, true)); + } + } + } + } + } + //leafs.iter().for_each(|leaf| println!("{:?}", leaf)); + } + println!("leaf count: {}", leafs.len()); + stats.0 = 0; + for leaf in &leafs { + if leaf.state == State::Win && leaf.spent_mana < smallest { + smallest = leaf.spent_mana; + } + match leaf.state { + State::Playing => stats.0 += 1, + State::Win => stats.1 += 1, + State::Loss => stats.2 += 1, + }; + //println!( + // "player hp: {}, boss hp: {}, spent mana so far: {}", + // leaf.player.hp, leaf.boss.hp, leaf.spent_mana + //); + } + println!("Playing, win, loss: {stats:?}"); + leafs.retain(|leaf| leaf.state == State::Playing && leaf.spent_mana < smallest); + if leafs.is_empty() { + break; + } + println!(" smallest: {smallest}"); + } + smallest } #[derive(Debug, Clone, PartialEq, Eq)] @@ -125,7 +208,7 @@ struct RoundNode { } impl RoundNode { - fn use_spell(&self, spell: Spell) -> RoundNode { + fn use_spell(&self, spell: Spell, hard_mode: bool) -> RoundNode { if self.state != State::Playing { eprintln!("State: {:#?}", self.state); eprintln!("spell: {spell:#?}"); @@ -133,10 +216,25 @@ impl RoundNode { } let mut player = self.player.clone(); let mut boss = self.boss.clone(); + let mut recharge_actif = false; + if hard_mode { + player.hp -= 1; + } + if player.hp == 0 { + return RoundNode { + player, + boss, + spent_mana: spell.cost + self.spent_mana, + state: State::Loss, + }; + } // Player turn for effect in player.status_effects.iter_mut() { if effect.name == SpellID::Recharge { player.mana += effect.mana; + if effect.duration > 1 { + recharge_actif = true; + } } effect.duration -= 1; if effect.duration > 0 && effect.name == SpellID::Shield { @@ -167,8 +265,15 @@ impl RoundNode { .into_iter() .filter(|&effect| effect.duration > 0) .collect::>(); - let used_mana = spell.cost; let mut dmg = 0; + if player.mana < spell.cost { + return RoundNode { + player, + boss, + spent_mana: spell.cost + self.spent_mana, + state: State::Loss, + }; + } match spell.name { SpellID::MagicMissile => { player.mana = player.mana.saturating_sub(spell.cost); @@ -198,15 +303,15 @@ impl RoundNode { return RoundNode { player, boss, - spent_mana: used_mana + self.spent_mana, + spent_mana: spell.cost + self.spent_mana, state: State::Win, }; } - if player.mana == 0 { + if player.mana == 0 && !recharge_actif { return RoundNode { player, boss, - spent_mana: used_mana + self.spent_mana, + spent_mana: spell.cost + self.spent_mana, state: State::Loss, }; } @@ -236,7 +341,7 @@ impl RoundNode { return RoundNode { player, boss, - spent_mana: used_mana + self.spent_mana, + spent_mana: spell.cost + self.spent_mana, state: State::Win, }; } @@ -252,14 +357,14 @@ impl RoundNode { return RoundNode { player, boss, - spent_mana: used_mana + self.spent_mana, + spent_mana: spell.cost + self.spent_mana, state: State::Loss, }; } RoundNode { player, boss, - spent_mana: used_mana + self.spent_mana, + spent_mana: spell.cost + self.spent_mana, state: State::Playing, } } @@ -401,17 +506,15 @@ mod tests { }; let mut smallest = u32::MAX; let mut leafs = vec![start]; - let mut test = 0; println!("start"); loop { - println!("iter: {test},"); let current_leafs = leafs.clone(); leafs.clear(); // create potential rounds for leaf in current_leafs { if leaf.state == State::Playing { for spell in Spell::get_all() { - leafs.push(leaf.use_spell(spell)); + leafs.push(leaf.use_spell(spell, false)); } } } @@ -423,15 +526,14 @@ mod tests { if leaf.state == State::Playing { all_done = false; } - println!( - "player hp: {}, boss hp: {}, spent mana so far: {}", - leaf.player.hp, leaf.boss.hp, leaf.spent_mana - ); + //println!( + // "player hp: {}, boss hp: {}, spent mana so far: {}", + // leaf.player.hp, leaf.boss.hp, leaf.spent_mana + //); } if all_done { break; } - test += 1; println!(" smallest: {smallest}"); } assert_eq!(smallest, 226); @@ -458,17 +560,15 @@ mod tests { }; let mut smallest = u32::MAX; let mut leafs = vec![start]; - let mut test = 0; println!("start"); loop { - println!("iter: {test},"); let current_leafs = leafs.clone(); leafs.clear(); // create potential rounds for leaf in current_leafs { if leaf.state == State::Playing { for spell in Spell::get_all() { - leafs.push(leaf.use_spell(spell)); + leafs.push(leaf.use_spell(spell, false)); } } } @@ -480,20 +580,34 @@ mod tests { if leaf.state == State::Playing { all_done = false; } - println!( - "player hp: {}, boss hp: {}, spent mana so far: {}", - leaf.player.hp, leaf.boss.hp, leaf.spent_mana - ); + //println!( + // "player hp: {}, boss hp: {}, spent mana so far: {}", + // leaf.player.hp, leaf.boss.hp, leaf.spent_mana + //); } if all_done { break; } - test += 1; println!(" smallest: {smallest}"); } assert_eq!(smallest, 641); } + #[allow(dead_code)] + fn init_characters() -> (Character, Character) { + let player = Character { + hp: 50, + mana: 500, + ..Default::default() + }; + let boss = Character { + hp: 14, + damage: 8, + ..Default::default() + }; + (player, boss) + } + #[test] fn part2() { let result = process_part2("");