From 80d7e62d0fbd4222e90d1795cf63cb900893a8ca Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Mon, 7 Oct 2024 14:56:46 +0200 Subject: [PATCH 01/27] Add puzzle1 --- scripts/aoc2015/day21/input.txt | 3 ++ scripts/aoc2015/day21/puzzle1.ts | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 scripts/aoc2015/day21/input.txt create mode 100644 scripts/aoc2015/day21/puzzle1.ts diff --git a/scripts/aoc2015/day21/input.txt b/scripts/aoc2015/day21/input.txt new file mode 100644 index 0000000..7f536e8 --- /dev/null +++ b/scripts/aoc2015/day21/input.txt @@ -0,0 +1,3 @@ +Hit Points: 109 +Damage: 8 +Armor: 2 \ No newline at end of file diff --git a/scripts/aoc2015/day21/puzzle1.ts b/scripts/aoc2015/day21/puzzle1.ts new file mode 100644 index 0000000..c1b264b --- /dev/null +++ b/scripts/aoc2015/day21/puzzle1.ts @@ -0,0 +1,64 @@ +/* +--- Day 21: RPG Simulator 20XX --- +Little Henry Case got a new video game for Christmas. +It's an RPG, and he's stuck on a boss. +He needs to know what equipment to buy at the shop. He hands you the controller. + +In this game, the player (you) and the enemy (the boss) take turns attacking. +The player always goes first. +Each attack reduces the opponent's hit points by at least 1. +The first character at or below 0 hit points loses. + +Damage dealt by an attacker each turn is equal to the attacker's damage score minus the defender's armor score. +An attacker always does at least 1 damage. +So, if the attacker has a damage score of 8, and the defender has an armor score of 3, the defender loses 5 hit points. +If the defender had an armor score of 300, the defender would still lose 1 hit point. + +Your damage score and armor score both start at zero. +They can be increased by buying items in exchange for gold. +You start with no items and have as much gold as you need. +Your total damage or armor is equal to the sum of those stats from all of your items. +You have 100 hit points. + +Here is what the item shop is selling: + +Weapons: Cost Damage Armor +Dagger 8 4 0 +Shortsword 10 5 0 +Warhammer 25 6 0 +Longsword 40 7 0 +Greataxe 74 8 0 + +Armor: Cost Damage Armor +Leather 13 0 1 +Chainmail 31 0 2 +Splintmail 53 0 3 +Bandedmail 75 0 4 +Platemail 102 0 5 + +Rings: Cost Damage Armor +Damage +1 25 1 0 +Damage +2 50 2 0 +Damage +3 100 3 0 +Defense +1 20 0 1 +Defense +2 40 0 2 +Defense +3 80 0 3 + +You must buy exactly one weapon; no dual-wielding. +Armor is optional, but you can't use more than one. +You can buy 0-2 rings (at most one for each hand). +You must use any items you buy. The shop only has one of each item, so you can't buy, for example, two rings of Damage +3. + +For example, suppose you have 8 hit points, 5 damage, and 5 armor, and that the boss has 12 hit points, 7 damage, and 2 armor: + +The player deals 5-2 = 3 damage; the boss goes down to 9 hit points. +The boss deals 7-5 = 2 damage; the player goes down to 6 hit points. +The player deals 5-2 = 3 damage; the boss goes down to 6 hit points. +The boss deals 7-5 = 2 damage; the player goes down to 4 hit points. +The player deals 5-2 = 3 damage; the boss goes down to 3 hit points. +The boss deals 7-5 = 2 damage; the player goes down to 2 hit points. +The player deals 5-2 = 3 damage; the boss goes down to 0 hit points. +In this scenario, the player wins! (Barely.) + +You have 100 hit points. The boss's actual stats are in your puzzle input. What is the least amount of gold you can spend and still win the fight? + */ From 632f8b8fdd2c2cb9cd1a7fc51cda3accc2771c3f Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Mon, 7 Oct 2024 15:02:19 +0200 Subject: [PATCH 02/27] Add algorithm. --- scripts/aoc2015/day21/puzzle1.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/aoc2015/day21/puzzle1.ts b/scripts/aoc2015/day21/puzzle1.ts index c1b264b..ab680a8 100644 --- a/scripts/aoc2015/day21/puzzle1.ts +++ b/scripts/aoc2015/day21/puzzle1.ts @@ -62,3 +62,20 @@ In this scenario, the player wins! (Barely.) You have 100 hit points. The boss's actual stats are in your puzzle input. What is the least amount of gold you can spend and still win the fight? */ + +// Algorithm + +// create your basic character +// create boss character (read file) + +// create list of items in shop + +// create board with prices + +// loop +// create variations of armor (make sure to safe it's complete price) +// modify your basic character +// simulate fight +// if you win, store price into an board + +// find min price From 4e70144a31ea14e6c03bf63200965119b28157f2 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Mon, 7 Oct 2024 15:14:48 +0200 Subject: [PATCH 03/27] Add some types --- scripts/aoc2015/day21/utils.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 scripts/aoc2015/day21/utils.ts diff --git a/scripts/aoc2015/day21/utils.ts b/scripts/aoc2015/day21/utils.ts new file mode 100644 index 0000000..aee9df0 --- /dev/null +++ b/scripts/aoc2015/day21/utils.ts @@ -0,0 +1,30 @@ +enum ItemType { + Weapon = "Weapon", + Armor = "Armor", + Ring = "Ring", +} +export type Item = { + type: ItemType; + damage: number; + defense: number; + cost: number; +}; + +export type Weapon = Item & { + type: ItemType.Weapon; +}; +export type Armor = Item & { + type: ItemType.Armor; +}; +export type Ring = Item & { + type: ItemType.Ring; +}; + +export type Character = { + hitPoints: number; + damage: number; + defense: number; + weapon?: Weapon; + armor?: Armor; + rings?: [Ring?, Ring?]; +}; From 740403020cb26482ccc08d2a4cf7849290894647 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Mon, 7 Oct 2024 15:26:00 +0200 Subject: [PATCH 04/27] Define some variables. --- scripts/aoc2015/day21/utils.ts | 131 +++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/scripts/aoc2015/day21/utils.ts b/scripts/aoc2015/day21/utils.ts index aee9df0..5d6631a 100644 --- a/scripts/aoc2015/day21/utils.ts +++ b/scripts/aoc2015/day21/utils.ts @@ -5,6 +5,7 @@ enum ItemType { } export type Item = { type: ItemType; + name: string; damage: number; defense: number; cost: number; @@ -28,3 +29,133 @@ export type Character = { armor?: Armor; rings?: [Ring?, Ring?]; }; + +const basicPlayer: Character = { + hitPoints: 100, + damage: 0, + defense: 0, +}; + +const boss: Character = { + hitPoints: 109, + damage: 8, + defense: 2, +}; + +// Weapons: Cost Damage Armor + +const allWeapons: Weapon[] = [{ + type: ItemType.Weapon, + name: "Dagger", + cost: 8, + damage: 4, + defense: 0, +}, { + type: ItemType.Weapon, + name: "Warhammer", + cost: 25, + damage: 6, + defense: 0, +}, { + type: ItemType.Weapon, + name: "Shortsword", + cost: 10, + damage: 5, + defense: 0, +}, { + type: ItemType.Weapon, + name: "Longsword", + cost: 40, + damage: 7, + defense: 0, +}, { + type: ItemType.Weapon, + name: "Greataxe", + cost: 74, + damage: 8, + defense: 0, +}]; +// Armor: Cost Damage Armor +const allArmors: Armor[] = [ + { + name: "Leather", + cost: 13, + damage: 0, + defense: 1, + type: ItemType.Armor, + }, + { + name: "Chainmail", + cost: 31, + damage: 0, + defense: 2, + type: ItemType.Armor, + }, + { + name: "Splintmail", + cost: 53, + damage: 0, + defense: 3, + type: ItemType.Armor, + }, + { + name: "Bandedmail", + cost: 75, + damage: 0, + defense: 4, + type: ItemType.Armor, + }, + { + name: "Platemail", + cost: 102, + damage: 0, + defense: 5, + type: ItemType.Armor, + }, +]; + +// Rings: Cost Damage Armor +const rings: Ring[] = [ + { + name: "Damage +1", + cost: 25, + damage: 1, + defense: 0, + type: ItemType.Ring, + }, + { + name: "Damage +2", + cost: 50, + damage: 2, + defense: 0, + type: ItemType.Ring, + }, + { + name: "Damage +3", + cost: 100, + damage: 3, + defense: 0, + type: ItemType.Ring, + }, + { + name: "Defense +1", + cost: 20, + damage: 0, + defense: 1, + type: ItemType.Ring, + }, + { + name: "Defense +2", + cost: 40, + damage: 0, + defense: 2, + type: ItemType.Ring, + }, + { + name: "Defense +3", + cost: 80, + damage: 0, + defense: 3, + type: ItemType.Ring, + }, +]; From 5cd6df8666799409618a0d2422ddc0bf8d6bb45c Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Mon, 7 Oct 2024 15:46:16 +0200 Subject: [PATCH 05/27] Implement equipCharacter --- scripts/aoc2015/day21/utils.test.ts | 37 +++++++++++++++++++++++++++++ scripts/aoc2015/day21/utils.ts | 28 +++++++++++++++++----- 2 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 scripts/aoc2015/day21/utils.test.ts diff --git a/scripts/aoc2015/day21/utils.test.ts b/scripts/aoc2015/day21/utils.test.ts new file mode 100644 index 0000000..50dbaa7 --- /dev/null +++ b/scripts/aoc2015/day21/utils.test.ts @@ -0,0 +1,37 @@ +import { assertEquals, assertThrows } from "@std/assert"; +import { describe, it } from "@std/testing/bdd"; +import { + allArmors, + allRings, + allWeapons, + Character, + equipCharacter, +} from "./utils.ts"; + +describe("equipCharacter", function () { + it("should equip character with some items", function () { + const character: Character = { + hitPoints: 100, + damage: 0, + defense: 0, + }; + const armor = allArmors[0]; + const weapon = allWeapons[0]; + const ring = allRings[0]; + assertEquals( + equipCharacter(character, { + armor, + weapon, + ringsLeft: ring, + }), + { + character: { + ...character, + defense: armor.defense + weapon.defense + ring.defense, + damage: armor.damage + weapon.damage + ring.damage, + }, + goldSpent: armor.cost + weapon.cost + ring.cost, + }, + ); + }); +}); diff --git a/scripts/aoc2015/day21/utils.ts b/scripts/aoc2015/day21/utils.ts index 5d6631a..ca12aee 100644 --- a/scripts/aoc2015/day21/utils.ts +++ b/scripts/aoc2015/day21/utils.ts @@ -25,9 +25,6 @@ export type Character = { hitPoints: number; damage: number; defense: number; - weapon?: Weapon; - armor?: Armor; - rings?: [Ring?, Ring?]; }; const basicPlayer: Character = { @@ -44,7 +41,7 @@ const boss: Character = { // Weapons: Cost Damage Armor -const allWeapons: Weapon[] = [{ +export const allWeapons: Weapon[] = [{ type: ItemType.Weapon, name: "Dagger", cost: 8, @@ -76,7 +73,7 @@ const allWeapons: Weapon[] = [{ defense: 0, }]; // Armor: Cost Damage Armor -const allArmors: Armor[] = [ +export const allArmors: Armor[] = [ { name: "Leather", cost: 13, @@ -115,7 +112,7 @@ const allArmors: Armor[] = [ ]; // Rings: Cost Damage Armor -const rings: Ring[] = [ +export const allRings: Ring[] = [ { name: "Damage +1", cost: 25, @@ -159,3 +156,22 @@ const rings: Ring[] = [ type: ItemType.Ring, }, ]; + +export function equipCharacter(character: Character, equip: { + weapon: Weapon; + armor?: Armor; + ringsLeft?: Ring; + ringRight?: Ring; +}): { character: Character; goldSpent: number } { + return { + character: { + ...character, + defense: equip.weapon.defense + (equip.armor?.defense ?? 0) + + (equip.ringRight?.defense ?? 0) + (equip.ringsLeft?.defense ?? 0), + damage: equip.weapon.damage + (equip.armor?.damage ?? 0) + + (equip.ringRight?.damage ?? 0) + (equip.ringsLeft?.damage ?? 0), + }, + goldSpent: equip.weapon.cost + (equip.armor?.cost ?? 0) + + (equip.ringRight?.cost ?? 0) + (equip.ringsLeft?.cost ?? 0), + }; +} From 4bce6d7291844e4b0891ea20fb06c86baf1c1d32 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Mon, 7 Oct 2024 16:03:19 +0200 Subject: [PATCH 06/27] Implement generateWeapon, generateArmor, generateRings --- scripts/aoc2015/day21/utils.test.ts | 45 +++++++++++++++++++++++++++++ scripts/aoc2015/day21/utils.ts | 28 ++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/scripts/aoc2015/day21/utils.test.ts b/scripts/aoc2015/day21/utils.test.ts index 50dbaa7..36c4f3a 100644 --- a/scripts/aoc2015/day21/utils.test.ts +++ b/scripts/aoc2015/day21/utils.test.ts @@ -6,6 +6,7 @@ import { allWeapons, Character, equipCharacter, + generateRings, } from "./utils.ts"; describe("equipCharacter", function () { @@ -35,3 +36,47 @@ describe("equipCharacter", function () { ); }); }); + +describe("generateRings", function () { + it("should generate all variations for rings", function () { + assertEquals([...generateRings()], [ + [undefined, undefined], + [allRings[0], undefined], + [allRings[1], undefined], + [allRings[2], undefined], + [allRings[3], undefined], + [allRings[4], undefined], + [allRings[5], undefined], + [allRings[0], allRings[1]], + [allRings[0], allRings[2]], + [allRings[0], allRings[3]], + [allRings[0], allRings[4]], + [allRings[0], allRings[5]], + [allRings[1], allRings[0]], + [allRings[1], allRings[2]], + [allRings[1], allRings[3]], + [allRings[1], allRings[4]], + [allRings[1], allRings[5]], + [allRings[2], allRings[0]], + [allRings[2], allRings[1]], + [allRings[2], allRings[3]], + [allRings[2], allRings[4]], + [allRings[2], allRings[5]], + [allRings[3], allRings[0]], + [allRings[3], allRings[1]], + [allRings[3], allRings[2]], + [allRings[3], allRings[4]], + [allRings[3], allRings[5]], + [allRings[4], allRings[0]], + [allRings[4], allRings[1]], + [allRings[4], allRings[2]], + [allRings[4], allRings[3]], + [allRings[4], allRings[5]], + [allRings[5], allRings[0]], + [allRings[5], allRings[1]], + [allRings[5], allRings[2]], + [allRings[5], allRings[3]], + [allRings[5], allRings[4]], + ]); + }); +}); diff --git a/scripts/aoc2015/day21/utils.ts b/scripts/aoc2015/day21/utils.ts index ca12aee..d09f7d2 100644 --- a/scripts/aoc2015/day21/utils.ts +++ b/scripts/aoc2015/day21/utils.ts @@ -175,3 +175,31 @@ export function equipCharacter(character: Character, equip: { (equip.ringRight?.cost ?? 0) + (equip.ringsLeft?.cost ?? 0), }; } + +export function* generateWeapon(): Generator { + for (const weapon of allWeapons) { + yield weapon; + } +} + +export function* generateArmor(): Generator { + yield undefined; + for (const armor of allArmors) { + yield armor; + } +} + +export function* generateRings(): Generator<[Ring?, Ring?]> { + yield [undefined, undefined]; + for (const ring of allRings) { + yield [ring, undefined]; + } + for (const ring1 of allRings) { + for ( + const ring2 of allRings + .filter((r) => r.name !== ring1.name) + ) { + yield [ring1, ring2]; + } + } +} From e133c23cec6cb8c982f27007f9382c26af53abec Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Mon, 7 Oct 2024 16:07:14 +0200 Subject: [PATCH 07/27] Implement modifyCharWithEquip --- scripts/aoc2015/day21/utils.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/aoc2015/day21/utils.ts b/scripts/aoc2015/day21/utils.ts index d09f7d2..6ed6800 100644 --- a/scripts/aoc2015/day21/utils.ts +++ b/scripts/aoc2015/day21/utils.ts @@ -203,3 +203,20 @@ export function* generateRings(): Generator<[Ring?, Ring?]> { } } } + +export function* modifyCharWithEquip( + char: Character, +): Generator<{ character: Character; goldSpent: number }> { + for (const weapon of generateWeapon()) { + for (const armor of generateArmor()) { + for (const [ring1, ring2] of generateRings()) { + yield equipCharacter(char, { + armor, + weapon, + ringsLeft: ring1, + ringRight: ring2, + }); + } + } + } +} From 15aba2361b49c1ecf0f1d743f639f8a4215df147 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Mon, 7 Oct 2024 16:23:22 +0200 Subject: [PATCH 08/27] Implement charAAttacksCharB --- scripts/aoc2015/day21/utils.test.ts | 32 +++++++++++++++++++++++++++-- scripts/aoc2015/day21/utils.ts | 8 ++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/scripts/aoc2015/day21/utils.test.ts b/scripts/aoc2015/day21/utils.test.ts index 36c4f3a..06332a4 100644 --- a/scripts/aoc2015/day21/utils.test.ts +++ b/scripts/aoc2015/day21/utils.test.ts @@ -1,9 +1,10 @@ -import { assertEquals, assertThrows } from "@std/assert"; -import { describe, it } from "@std/testing/bdd"; +import { assert, assertEquals, assertThrows } from "@std/assert"; +import { beforeEach, describe, it } from "@std/testing/bdd"; import { allArmors, allRings, allWeapons, + charAAttacksCharB, Character, equipCharacter, generateRings, @@ -80,3 +81,30 @@ describe("generateRings", function () { ]); }); }); + +describe("charAAttacksCharB", function () { + const charA: Character = { hitPoints: 100, damage: 10, defense: 20 }; + const charB: Character = { hitPoints: 200, damage: 10, defense: 3 }; + + it("should execute charA attack on charB", function () { + const charAClone = Object.assign({}, charA); + const charBClone = Object.assign({}, charB); + charAAttacksCharB(charAClone, charBClone); + assertEquals(charAClone, charA); + assertEquals(charBClone, { + ...charB, + hitPoints: charB.hitPoints - (charA.damage - charB.defense), + }); + }); + + it("should execute charB attack on charA", function () { + const charAClone = Object.assign({}, charA); + const charBClone = Object.assign({}, charB); + charAAttacksCharB(charBClone, charAClone); + assertEquals(charBClone, charB); + assertEquals(charAClone, { + ...charA, + hitPoints: charA.hitPoints - 1, + }); + }); +}); diff --git a/scripts/aoc2015/day21/utils.ts b/scripts/aoc2015/day21/utils.ts index 6ed6800..aa50cb1 100644 --- a/scripts/aoc2015/day21/utils.ts +++ b/scripts/aoc2015/day21/utils.ts @@ -220,3 +220,11 @@ export function* modifyCharWithEquip( } } } + +export function charAAttacksCharB(charA: Character, charB: Character) { + if (charA.damage > charB.defense) { + charB.hitPoints = charB.hitPoints - (charA.damage - charB.defense); + } else { + charB.hitPoints = charB.hitPoints - 1; + } +} From 1354edb31b7e740ffd4c146067be1f18f86555c1 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Tue, 8 Oct 2024 08:59:16 +0200 Subject: [PATCH 09/27] Implement simulateFight --- scripts/aoc2015/day21/utils.test.ts | 20 +++++++++++++++++ scripts/aoc2015/day21/utils.ts | 35 +++++++++++++++++++---------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/scripts/aoc2015/day21/utils.test.ts b/scripts/aoc2015/day21/utils.test.ts index 06332a4..d4ab62c 100644 --- a/scripts/aoc2015/day21/utils.test.ts +++ b/scripts/aoc2015/day21/utils.test.ts @@ -8,6 +8,7 @@ import { Character, equipCharacter, generateRings, + simulateFight, } from "./utils.ts"; describe("equipCharacter", function () { @@ -108,3 +109,22 @@ describe("charAAttacksCharB", function () { }); }); }); + +describe("simulateFight", function () { + it("return true, if player wins", function () { + // For example, suppose you have 8 hit points, 5 damage, and 5 armor, + // and that the boss has 12 hit points, 7 damage, and 2 armor: + assertEquals( + simulateFight({ + hitPoints: 8, + damage: 5, + defense: 5, + }, { + hitPoints: 12, + damage: 7, + defense: 2, + }), + true, + ); + }); +}); diff --git a/scripts/aoc2015/day21/utils.ts b/scripts/aoc2015/day21/utils.ts index aa50cb1..b290c6d 100644 --- a/scripts/aoc2015/day21/utils.ts +++ b/scripts/aoc2015/day21/utils.ts @@ -27,18 +27,6 @@ export type Character = { defense: number; }; -const basicPlayer: Character = { - hitPoints: 100, - damage: 0, - defense: 0, -}; - -const boss: Character = { - hitPoints: 109, - damage: 8, - defense: 2, -}; - // Weapons: Cost Damage Armor export const allWeapons: Weapon[] = [{ @@ -228,3 +216,26 @@ export function charAAttacksCharB(charA: Character, charB: Character) { charB.hitPoints = charB.hitPoints - 1; } } + +enum Turn { + Player = "player", + Boss = "boss", +} + +/** + * @return true if player wins + */ +export function simulateFight(player: Character, boss: Character): boolean { + let turn = Turn.Player; + while (player.hitPoints > 0 && boss.hitPoints > 0) { + if (turn === Turn.Player) { + charAAttacksCharB(player, boss); + turn = Turn.Boss; + } else { + charAAttacksCharB(boss, player); + turn = Turn.Player; + } + console.log("player", player, "boss", boss); + } + return player.hitPoints > boss.hitPoints; +} From ecb3db76f006d5f45bdd9b0c1ff3852d4da2ea1c Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Tue, 8 Oct 2024 09:09:13 +0200 Subject: [PATCH 10/27] Find solution for puzzle1 --- scripts/aoc2015/day21/puzzle1.ts | 30 +++++++++++++++++++++++++++--- scripts/aoc2015/day21/utils.ts | 1 - 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/scripts/aoc2015/day21/puzzle1.ts b/scripts/aoc2015/day21/puzzle1.ts index ab680a8..42e4d10 100644 --- a/scripts/aoc2015/day21/puzzle1.ts +++ b/scripts/aoc2015/day21/puzzle1.ts @@ -66,16 +66,40 @@ You have 100 hit points. The boss's actual stats are in your puzzle input. What // Algorithm // create your basic character +import { Character, modifyCharWithEquip, simulateFight } from "./utils.ts"; + +const basicPlayer: Character = { + hitPoints: 100, + damage: 0, + defense: 0, +}; + // create boss character (read file) +const boss: Character = { + hitPoints: 109, + damage: 8, + defense: 2, +}; // create list of items in shop // create board with prices +const board: { character: Character; goldSpent: number }[] = []; // loop // create variations of armor (make sure to safe it's complete price) // modify your basic character -// simulate fight -// if you win, store price into an board - +for (const playerWithEquip of modifyCharWithEquip(basicPlayer)) { + // simulate fight + const playerWins = simulateFight( + playerWithEquip.character, + Object.assign({}, boss), + ); + // if you win, store price into an board + if (playerWins) { + board.push(playerWithEquip); + } +} +console.log("min price ", Math.min(...board.map(({ goldSpent }) => goldSpent))); +console.log("winner ", board.find(({ goldSpent }) => goldSpent === 111)); // find min price diff --git a/scripts/aoc2015/day21/utils.ts b/scripts/aoc2015/day21/utils.ts index b290c6d..86ba18d 100644 --- a/scripts/aoc2015/day21/utils.ts +++ b/scripts/aoc2015/day21/utils.ts @@ -235,7 +235,6 @@ export function simulateFight(player: Character, boss: Character): boolean { charAAttacksCharB(boss, player); turn = Turn.Player; } - console.log("player", player, "boss", boss); } return player.hitPoints > boss.hitPoints; } From 20082b57c4be2d0d3259ee71ab0e4629cce27550 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Tue, 8 Oct 2024 09:25:04 +0200 Subject: [PATCH 11/27] Find solution for puzzle2 --- scripts/aoc2015/day21/puzzle1.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/scripts/aoc2015/day21/puzzle1.ts b/scripts/aoc2015/day21/puzzle1.ts index 42e4d10..04c8133 100644 --- a/scripts/aoc2015/day21/puzzle1.ts +++ b/scripts/aoc2015/day21/puzzle1.ts @@ -103,3 +103,27 @@ for (const playerWithEquip of modifyCharWithEquip(basicPlayer)) { console.log("min price ", Math.min(...board.map(({ goldSpent }) => goldSpent))); console.log("winner ", board.find(({ goldSpent }) => goldSpent === 111)); // find min price + +// find max price and loose + +const looseBoard: { character: Character; goldSpent: number }[] = []; + +for (const playerWithEquip of modifyCharWithEquip(basicPlayer)) { + // simulate fight + const playerWins = simulateFight( + playerWithEquip.character, + Object.assign({}, boss), + ); + // if you win, store price into an looseBoard + if (!playerWins) { + looseBoard.push(playerWithEquip); + } +} +console.log( + "Max price ", + Math.max(...looseBoard.map(({ goldSpent }) => goldSpent)), +); +console.log( + "looser with most expensive armor ", + looseBoard.find(({ goldSpent }) => goldSpent === 188), +); From c0adfafe3aa5246bd2d0d6da276f86e75c2662d1 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Tue, 8 Oct 2024 13:20:05 +0200 Subject: [PATCH 12/27] Add puzzle1 for day 22 --- scripts/aoc2015/day22/puzzle1.tsx | 171 ++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 scripts/aoc2015/day22/puzzle1.tsx diff --git a/scripts/aoc2015/day22/puzzle1.tsx b/scripts/aoc2015/day22/puzzle1.tsx new file mode 100644 index 0000000..c8ff537 --- /dev/null +++ b/scripts/aoc2015/day22/puzzle1.tsx @@ -0,0 +1,171 @@ +/* +--- Day 22: Wizard Simulator 20XX --- +Little Henry Case decides that defeating bosses with swords and stuff is boring. Now he's playing the game with a wizard. +Of course, he gets stuck on another boss and needs your help again. + +In this version, combat still proceeds with the player and the boss taking alternating turns. + +The player still goes first. Now, however, you don't get any equipment; instead, you must choose one of your spells to cast. The first character at or below 0 hit points loses. + +Since you're a wizard, you don't get to wear armor, and you can't attack normally. +However, since you do magic damage, your opponent's armor is ignored, and so the boss effectively has zero armor as well. +As before, if armor (from a spell, in this case) would reduce damage below 1, it becomes 1 instead - that is, the boss' attacks always deal at least 1 damage. + +On each of your turns, you must select one of your spells to cast. +If you cannot afford to cast any spell, you lose. +Spells cost mana; you start with 500 mana, but have no maximum limit. +You must have enough mana to cast a spell, and its cost is immediately deducted when you cast it. Your spells are Magic Missile, Drain, Shield, Poison, and Recharge. + +Magic Missile costs 53 mana. It instantly does 4 damage. +Drain costs 73 mana. It instantly does 2 damage and heals you for 2 hit points. +Shield costs 113 mana. It starts an effect that lasts for 6 turns. While it is active, your armor is increased by 7. +Poison costs 173 mana. It starts an effect that lasts for 6 turns. At the start of each turn while it is active, it deals the boss 3 damage. +Recharge costs 229 mana. It starts an effect that lasts for 5 turns. At the start of each turn while it is active, it gives you 101 new mana. + +Effects all work the same way. +Effects apply at the start of both the player's turns and the boss' turns. +Effects are created with a timer (the number of turns they last); at the start of each turn, after they apply any effect they have, their timer is decreased by one. If this decreases the timer to zero, the effect ends. You cannot cast a spell that would start an effect which is already active. However, effects can be started on the same turn they end. + +For example, suppose the player has 10 hit points and 250 mana, and that the boss has 13 hit points and 8 damage: + +-- Player turn -- +- Player has 10 hit points, 0 armor, 250 mana +- Boss has 13 hit points +Player casts Poison. + +-- Boss turn -- +- Player has 10 hit points, 0 armor, 77 mana +- Boss has 13 hit points +Poison deals 3 damage; its timer is now 5. +Boss attacks for 8 damage. + +-- Player turn -- +- Player has 2 hit points, 0 armor, 77 mana +- Boss has 10 hit points +Poison deals 3 damage; its timer is now 4. +Player casts Magic Missile, dealing 4 damage. + +-- Boss turn -- +- Player has 2 hit points, 0 armor, 24 mana +- Boss has 3 hit points +Poison deals 3 damage. This kills the boss, and the player wins. +Now, suppose the same initial conditions, except that the boss has 14 hit points instead: + +-- Player turn -- +- Player has 10 hit points, 0 armor, 250 mana +- Boss has 14 hit points +Player casts Recharge. + +-- Boss turn -- +- Player has 10 hit points, 0 armor, 21 mana +- Boss has 14 hit points +Recharge provides 101 mana; its timer is now 4. +Boss attacks for 8 damage! + +-- Player turn -- +- Player has 2 hit points, 0 armor, 122 mana +- Boss has 14 hit points +Recharge provides 101 mana; its timer is now 3. +Player casts Shield, increasing armor by 7. + +-- Boss turn -- +- Player has 2 hit points, 7 armor, 110 mana +- Boss has 14 hit points +Shield's timer is now 5. +Recharge provides 101 mana; its timer is now 2. +Boss attacks for 8 - 7 = 1 damage! + +-- Player turn -- +- Player has 1 hit point, 7 armor, 211 mana +- Boss has 14 hit points +Shield's timer is now 4. +Recharge provides 101 mana; its timer is now 1. +Player casts Drain, dealing 2 damage, and healing 2 hit points. + +-- Boss turn -- +- Player has 3 hit points, 7 armor, 239 mana +- Boss has 12 hit points +Shield's timer is now 3. +Recharge provides 101 mana; its timer is now 0. +Recharge wears off. +Boss attacks for 8 - 7 = 1 damage! + +-- Player turn -- +- Player has 2 hit points, 7 armor, 340 mana +- Boss has 12 hit points +Shield's timer is now 2. +Player casts Poison. + +-- Boss turn -- +- Player has 2 hit points, 7 armor, 167 mana +- Boss has 12 hit points +Shield's timer is now 1. +Poison deals 3 damage; its timer is now 5. +Boss attacks for 8 - 7 = 1 damage! + +-- Player turn -- +- Player has 1 hit point, 7 armor, 167 mana +- Boss has 9 hit points +Shield's timer is now 0. +Shield wears off, decreasing armor by 7. +Poison deals 3 damage; its timer is now 4. +Player casts Magic Missile, dealing 4 damage. + +-- Boss turn -- +- Player has 1 hit point, 0 armor, 114 mana +- Boss has 2 hit points +Poison deals 3 damage. This kills the boss, and the player wins. +You start with 50 hit points and 500 mana points. The boss's actual stats are in your puzzle input. + +What is the least amount of mana you can spend and still win the fight? (Do not include mana recharge effects as "spending" negative mana.) + */ + +import { Character } from "../day21/utils.ts"; + +const boss: Character = { + hitPoints: 71, + damage: 10, + defense: 0, +} + +const basicPlayer: Character = { + hitPoints: 50, + mana: 500, + damage: 0 +} + +// implement effects (after spells) +// effects: +// shielded defense + 7 for 6 turns +// poisoned damage 3 for 6 turns +// recharging gain 101 mana for 5 turns + +// how fight will look like: +// player looses - with no hitpoints +// player looses - with no mana +// boss looses - with no hitpoints + +// loop +// make effects on the player +// make the boss +// if it is player turn +// pick a spell from list of spells (limitations - mana, existing effects) +// attack +// if it is bosses turn +// attack the player + +// ALGORITHM 1 +// what is the least amount of mana, I need to use and still win the fight? +// lazy algorithm - always pick the esiest way according to rules +// - I don't know, If my rules are right + + +// ALGORITHM 2 +// brute force - try all combinations, pick the best +// I will need to run fight simulations +// negative - each player turn will increase number of possible scanarios by 1 to 5 +// once I will find "winning scenario", I can drop all scenarios with more mana spent +// stop cases: +// player has no mana for next spell +// player dies +// there is other scenario with less mana spent \ No newline at end of file From 675c693fb60880c9c1f23d39c8687f7afc647dc6 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Tue, 8 Oct 2024 13:28:46 +0200 Subject: [PATCH 13/27] Update algorithm --- scripts/aoc2015/day21/utils.ts | 1 + scripts/aoc2015/day22/puzzle1.tsx | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/scripts/aoc2015/day21/utils.ts b/scripts/aoc2015/day21/utils.ts index 86ba18d..74dc29a 100644 --- a/scripts/aoc2015/day21/utils.ts +++ b/scripts/aoc2015/day21/utils.ts @@ -25,6 +25,7 @@ export type Character = { hitPoints: number; damage: number; defense: number; + mana?: number; }; // Weapons: Cost Damage Armor diff --git a/scripts/aoc2015/day22/puzzle1.tsx b/scripts/aoc2015/day22/puzzle1.tsx index c8ff537..85df547 100644 --- a/scripts/aoc2015/day22/puzzle1.tsx +++ b/scripts/aoc2015/day22/puzzle1.tsx @@ -131,7 +131,8 @@ const boss: Character = { const basicPlayer: Character = { hitPoints: 50, mana: 500, - damage: 0 + damage: 0, + defense: 0, } // implement effects (after spells) @@ -163,9 +164,19 @@ const basicPlayer: Character = { // ALGORITHM 2 // brute force - try all combinations, pick the best // I will need to run fight simulations -// negative - each player turn will increase number of possible scanarios by 1 to 5 +// negative - each player turn will increase number of possible scenarios by 1 to 5 // once I will find "winning scenario", I can drop all scenarios with more mana spent // stop cases: // player has no mana for next spell // player dies -// there is other scenario with less mana spent \ No newline at end of file +// there is other scenario with less mana spent + + +// what describes single step in scenario +// inputs: +// who will move (Player/ Boss) +// list of combinations of Player and Boss, and mana spent +// +// outputs: +// who will move +// list of combinations of Player and Boss (very likely the count will differ, some combinations will be added, some wil), and mana spent \ No newline at end of file From d3c35c2a8de1317d696e392b06e5eb8e4d905fd4 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 10:20:30 +0200 Subject: [PATCH 14/27] Partially implement castSpell --- scripts/aoc2015/day22/utils.test.ts | 126 ++++++++++++++++++++++++++++ scripts/aoc2015/day22/utils.ts | 111 ++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 scripts/aoc2015/day22/utils.test.ts create mode 100644 scripts/aoc2015/day22/utils.ts diff --git a/scripts/aoc2015/day22/utils.test.ts b/scripts/aoc2015/day22/utils.test.ts new file mode 100644 index 0000000..1a0d52c --- /dev/null +++ b/scripts/aoc2015/day22/utils.test.ts @@ -0,0 +1,126 @@ +import { assertEquals, assertThrows } from "@std/assert"; +import { beforeEach, describe, it } from "@std/testing/bdd"; +import { castSpell, Character, EffectName, Spell } from "./utils.ts"; + +describe("castSpell", function () { + const playerTemplate: Character = { + hitPoints: 100, + damage: 0, + defense: 0, + mana: 500, + effects: [], + }; + const bossTemplate: Character = { + hitPoints: 200, + damage: 10, + defense: 2, + mana: 0, + effects: [], + }; + let player: Character; + let boss: Character; + beforeEach(function () { + player = Object.assign({}, playerTemplate); + boss = Object.assign({}, bossTemplate); + }); + // Magic Missile costs 53 mana. It instantly does 4 damage. + + it("should throw error, when player has not enough mana to cast spells", function () { + assertThrows(() => + castSpell( + { ...player, mana: 10 }, + boss, + Spell.MagicMissile, + ) + ); + }); + + it("should cast magic missile", function () { + castSpell( + player, + boss, + Spell.MagicMissile, + ); + assertEquals(player.effects.length, 0); + assertEquals(boss.effects.length, 0); + assertEquals(boss.hitPoints, bossTemplate.hitPoints - 4); + assertEquals(player.mana, playerTemplate.mana - 53); + }); + // Drain costs 73 mana. It instantly does 2 damage and heals you for 2 hit points. + it("should cast drain", function () { + castSpell( + player, + boss, + Spell.Drain, + ); + assertEquals(boss, { + ...bossTemplate, + hitPoints: bossTemplate.hitPoints - 2, + }); + assertEquals(player, { + ...playerTemplate, + hitPoints: playerTemplate.hitPoints + 2, + mana: playerTemplate.mana - 73, + }); + }); + // Shield costs 113 mana. It starts an effect that lasts for 6 turns. While it is active, your armor is increased by 7. + it("should cast shield", function () { + castSpell( + player, + boss, + Spell.Shield, + ); + assertEquals(boss, bossTemplate); + assertEquals(player, { + ...playerTemplate, + effects: [{ + name: EffectName.Shielded, + charges: 6, + }], + defense: playerTemplate.defense + 7, + mana: playerTemplate.mana - 113, + }); + }); + // Poison costs 173 mana. It starts an effect that lasts for 6 turns. At the start of each turn while it is active, it deals the boss 3 damage. + it("should cast poison", function () { + castSpell( + player, + boss, + Spell.Poison, + ); + assertEquals(boss, { + ...bossTemplate, + effects: [{ + name: EffectName.Poisoned, + charges: 6, + }], + }); + assertEquals(player, { + ...playerTemplate, + mana: playerTemplate.mana - 173, + }); + }); + // Recharge costs 229 mana. It starts an effect that lasts for 5 turns. At the start of each turn while it is active, it gives you 101 new mana. + it("should cast recharge", function () { + castSpell( + player, + boss, + Spell.Recharge, + ); + assertEquals(boss, bossTemplate); + assertEquals(player, { + ...playerTemplate, + effects: [{ + name: EffectName.Recharging, + charges: 5, + }], + mana: playerTemplate.mana - 229, + }); + }); +}); + +// describe.skip("castSpell", function () { +// it("should cast spell", function () { +// assertEquals(); +// }); +// }); diff --git a/scripts/aoc2015/day22/utils.ts b/scripts/aoc2015/day22/utils.ts new file mode 100644 index 0000000..d2f3552 --- /dev/null +++ b/scripts/aoc2015/day22/utils.ts @@ -0,0 +1,111 @@ +export type Character = { + hitPoints: number; + damage: number; + defense: number; + mana: number; + effects: Effect[]; +}; + +export enum EffectName { + Poisoned = "poisoned", + Shielded = "shielded", + Recharging = "recharging", +} + +export type EffectBlueprint = { + onEffectTick?: (characters: { target: Character; author: Character }) => void; + onEffectExpires?: ( + characters: { target: Character; author: Character }, + ) => void; + onEffectInit?: (characters: { target: Character; author: Character }) => void; + charges: number; + name: EffectName; +}; + +// implement effects (after spells) +// effects: +// shielded defense + 7 for 6 turns +// poisoned damage 3 for 6 turns +// recharging gain 101 mana for 5 turns + +const effectBlueprints: EffectBlueprint[] = [{ + name: EffectName.Shielded, + onEffectInit: function ( + { author }: { target: Character; author: Character }, + ) { + author.defense = author.defense + 7; + }, + onEffectExpires: function ( + { author }: { target: Character; author: Character }, + ) { + author.defense = author.defense - 7; + }, + charges: 6, +}]; + +export type Effect = { + name: EffectName; + charges: number; +}; + +export enum Spell { + MagicMissile = "magicMissile", + Drain = "drain", + Shield = "shield", + Poison = "poison", + Recharge = "recharge", +} + +// Magic Missile costs 53 mana. It instantly does 4 damage. +// Drain costs 73 mana. It instantly does 2 damage and heals you for 2 hit points. +// Shield costs 113 mana. It starts an effect that lasts for 6 turns. While it is active, your armor is increased by 7. +// Poison costs 173 mana. It starts an effect that lasts for 6 turns. At the start of each turn while it is active, it deals the boss 3 damage. +// Recharge costs 229 mana. It starts an effect that lasts for 5 turns. At the start of each turn while it is active, it gives you 101 new mana. + +export function castSpell( + author: Character, + target: Character, + spell: Spell, +): void { + switch (spell) { + case Spell.MagicMissile: + author.mana = author.mana - 53; + target.hitPoints = target.hitPoints - 4; + break; + case Spell.Drain: + author.mana = author.mana - 73; + author.hitPoints = author.hitPoints + 2; + target.hitPoints = target.hitPoints - 2; + break; + case Spell.Shield: + author.mana = author.mana - 113; + author.defense = author.defense + 7; + author.effects = [...author.effects, { + name: EffectName.Shielded, + charges: 6, + }]; + break; + case Spell.Poison: + author.mana = author.mana - 173; + target.effects = [...target.effects, { + name: EffectName.Poisoned, + charges: 6, + }]; + break; + case Spell.Recharge: + author.mana = author.mana - 229; + author.effects = [...author.effects, { + name: EffectName.Recharging, + charges: 5, + }]; + break; + } +} + +export function applyEffects( + author: Character, + target: Character, + effectBlueprints: EffectBlueprint[], +): void { + console.log("apply effects"); +} From 4a61a680e705e80516b70756183b195164363b05 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 10:36:36 +0200 Subject: [PATCH 15/27] Implement isSpellAvailable --- scripts/aoc2015/day22/puzzle1.tsx | 8 +++- scripts/aoc2015/day22/utils.test.ts | 63 ++++++++++++++++++++++++++--- scripts/aoc2015/day22/utils.ts | 41 +++++++++++++++++++ 3 files changed, 104 insertions(+), 8 deletions(-) diff --git a/scripts/aoc2015/day22/puzzle1.tsx b/scripts/aoc2015/day22/puzzle1.tsx index 85df547..07016d4 100644 --- a/scripts/aoc2015/day22/puzzle1.tsx +++ b/scripts/aoc2015/day22/puzzle1.tsx @@ -175,8 +175,12 @@ const basicPlayer: Character = { // what describes single step in scenario // inputs: // who will move (Player/ Boss) -// list of combinations of Player and Boss, and mana spent +// list of combinations of Player and Boss, and mana spent and effects, that can be used +// // // outputs: // who will move -// list of combinations of Player and Boss (very likely the count will differ, some combinations will be added, some wil), and mana spent \ No newline at end of file +// list of combinations of Player and Boss (very likely the count will differ, some combinations will be added, some wil), and mana spent + +// function getListOfAvailableSpells +// depends on player mana, player effects, boss effects \ No newline at end of file diff --git a/scripts/aoc2015/day22/utils.test.ts b/scripts/aoc2015/day22/utils.test.ts index 1a0d52c..cf39f8c 100644 --- a/scripts/aoc2015/day22/utils.test.ts +++ b/scripts/aoc2015/day22/utils.test.ts @@ -1,6 +1,12 @@ import { assertEquals, assertThrows } from "@std/assert"; import { beforeEach, describe, it } from "@std/testing/bdd"; -import { castSpell, Character, EffectName, Spell } from "./utils.ts"; +import { + castSpell, + Character, + EffectName, + isSpellAvailable, + Spell, +} from "./utils.ts"; describe("castSpell", function () { const playerTemplate: Character = { @@ -119,8 +125,53 @@ describe("castSpell", function () { }); }); -// describe.skip("castSpell", function () { -// it("should cast spell", function () { -// assertEquals(); -// }); -// }); +describe("isSpellAvailable", function () { + const player: Character = { + hitPoints: 100, + damage: 0, + defense: 0, + mana: 500, + effects: [], + }; + const boss: Character = { + hitPoints: 200, + damage: 10, + defense: 2, + mana: 0, + effects: [], + }; + it("should return false if player has no mana", function () { + assertEquals( + isSpellAvailable({ ...player, mana: 52 }, boss, Spell.MagicMissile), + false, + ); + }); + it("should return false if player has related effect on", function () { + assertEquals( + isSpellAvailable( + { + ...player, + effects: [{ + name: EffectName.Recharging, + charges: 3, + }], + }, + boss, + Spell.Recharge, + ), + false, + ); + }); + it("should return false if target has related effect on", function () { + assertEquals( + isSpellAvailable(player, { + ...boss, + effects: [{ name: EffectName.Poisoned, charges: 3 }], + }, Spell.Poison), + false, + ); + }); + it("should return true if spell is available", function () { + assertEquals(isSpellAvailable(player, boss, Spell.Shield), true); + }); +}); diff --git a/scripts/aoc2015/day22/utils.ts b/scripts/aoc2015/day22/utils.ts index d2f3552..899eece 100644 --- a/scripts/aoc2015/day22/utils.ts +++ b/scripts/aoc2015/day22/utils.ts @@ -56,6 +56,44 @@ export enum Spell { Recharge = "recharge", } +export const spellCost: Record = { + [Spell.MagicMissile]: 53, + [Spell.Drain]: 73, + [Spell.Shield]: 113, + [Spell.Poison]: 173, + [Spell.Recharge]: 229, +}; + +// spell is available, when player has enough mana +// when author/target has no related effect already +export function isSpellAvailable( + author: Character, + target: Character, + spell: Spell, +): boolean { + if (author.mana < spellCost[spell]) { + return false; + } + switch (spell) { + case Spell.Shield: + if (author.effects.find((e) => e.name === EffectName.Shielded)) { + return false; + } + break; + case Spell.Poison: + if (target.effects.find((e) => e.name === EffectName.Poisoned)) { + return false; + } + break; + case Spell.Recharge: + if (author.effects.find((e) => e.name === EffectName.Recharging)) { + return false; + } + break; + } + return true; +} + // Magic Missile costs 53 mana. It instantly does 4 damage. // Drain costs 73 mana. It instantly does 2 damage and heals you for 2 hit points. // Shield costs 113 mana. It starts an effect that lasts for 6 turns. While it is active, your armor is increased by 7. @@ -67,6 +105,9 @@ export function castSpell( target: Character, spell: Spell, ): void { + if (!isSpellAvailable(author, target, spell)) { + throw new Error("Spell not available"); + } switch (spell) { case Spell.MagicMissile: author.mana = author.mana - 53; From fd7d64f2c79eae85127d74591d70d4c471186de4 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 11:01:53 +0200 Subject: [PATCH 16/27] Add first test for applyEffects --- scripts/aoc2015/day22/utils.test.ts | 47 +++++++++++++++++++++++++++++ scripts/aoc2015/day22/utils.ts | 7 ++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/scripts/aoc2015/day22/utils.test.ts b/scripts/aoc2015/day22/utils.test.ts index cf39f8c..9454225 100644 --- a/scripts/aoc2015/day22/utils.test.ts +++ b/scripts/aoc2015/day22/utils.test.ts @@ -1,11 +1,14 @@ import { assertEquals, assertThrows } from "@std/assert"; import { beforeEach, describe, it } from "@std/testing/bdd"; import { + applyEffects, castSpell, Character, + cloneChar, EffectName, isSpellAvailable, Spell, + spellCost, } from "./utils.ts"; describe("castSpell", function () { @@ -175,3 +178,47 @@ describe("isSpellAvailable", function () { assertEquals(isSpellAvailable(player, boss, Spell.Shield), true); }); }); + +describe("applyEffects", function () { + const playerTemplate: Character = { + hitPoints: 100, + damage: 0, + defense: 0, + mana: 500, + effects: [], + }; + const bossTemplate: Character = { + hitPoints: 200, + damage: 10, + defense: 2, + mana: 0, + effects: [], + }; + it("should validate ticks and end exit of shielded effect", function () { + const player = cloneChar(playerTemplate); + const boss = cloneChar(bossTemplate); + castSpell(player, boss, Spell.Shield); + applyEffects(player, boss); + assertEquals(player, { + ...playerTemplate, + mana: playerTemplate.mana - spellCost[Spell.Shield], + effects: [{ name: EffectName.Shielded, charges: 5 }], + defense: playerTemplate.defense + 7, + }); + assertEquals(boss, bossTemplate); + applyEffects(player, boss); + applyEffects(player, boss); + applyEffects(player, boss); + applyEffects(player, boss); + applyEffects(player, boss); + assertEquals(player, { + ...playerTemplate, + mana: playerTemplate.mana - spellCost[Spell.Shield], + effects: [], + }); + }); + it("should validate ticks and end exit of poisoned effect", function () { + }); + it("should validate ticks and end exit of recharging effect", function () { + }); +}); diff --git a/scripts/aoc2015/day22/utils.ts b/scripts/aoc2015/day22/utils.ts index 899eece..c57090b 100644 --- a/scripts/aoc2015/day22/utils.ts +++ b/scripts/aoc2015/day22/utils.ts @@ -143,10 +143,15 @@ export function castSpell( } } +// modifies author/target stats +// wear effect off after all charges are used export function applyEffects( author: Character, target: Character, - effectBlueprints: EffectBlueprint[], ): void { console.log("apply effects"); } + +export function cloneChar(char: Character): Character { + return Object.assign({}, char); +} From f2e9fffc809071493879c32657f6e2c369062c5f Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 11:04:20 +0200 Subject: [PATCH 17/27] Use spellCost --- scripts/aoc2015/day22/utils.test.ts | 13 ++++++++----- scripts/aoc2015/day22/utils.ts | 6 +----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/scripts/aoc2015/day22/utils.test.ts b/scripts/aoc2015/day22/utils.test.ts index 9454225..de32e32 100644 --- a/scripts/aoc2015/day22/utils.test.ts +++ b/scripts/aoc2015/day22/utils.test.ts @@ -53,7 +53,10 @@ describe("castSpell", function () { assertEquals(player.effects.length, 0); assertEquals(boss.effects.length, 0); assertEquals(boss.hitPoints, bossTemplate.hitPoints - 4); - assertEquals(player.mana, playerTemplate.mana - 53); + assertEquals( + player.mana, + playerTemplate.mana - spellCost[Spell.MagicMissile], + ); }); // Drain costs 73 mana. It instantly does 2 damage and heals you for 2 hit points. it("should cast drain", function () { @@ -69,7 +72,7 @@ describe("castSpell", function () { assertEquals(player, { ...playerTemplate, hitPoints: playerTemplate.hitPoints + 2, - mana: playerTemplate.mana - 73, + mana: playerTemplate.mana - spellCost[Spell.Drain], }); }); // Shield costs 113 mana. It starts an effect that lasts for 6 turns. While it is active, your armor is increased by 7. @@ -87,7 +90,7 @@ describe("castSpell", function () { charges: 6, }], defense: playerTemplate.defense + 7, - mana: playerTemplate.mana - 113, + mana: playerTemplate.mana - spellCost[Spell.Shield], }); }); // Poison costs 173 mana. It starts an effect that lasts for 6 turns. At the start of each turn while it is active, it deals the boss 3 damage. @@ -106,7 +109,7 @@ describe("castSpell", function () { }); assertEquals(player, { ...playerTemplate, - mana: playerTemplate.mana - 173, + mana: playerTemplate.mana - spellCost[Spell.Poison], }); }); // Recharge costs 229 mana. It starts an effect that lasts for 5 turns. At the start of each turn while it is active, it gives you 101 new mana. @@ -123,7 +126,7 @@ describe("castSpell", function () { name: EffectName.Recharging, charges: 5, }], - mana: playerTemplate.mana - 229, + mana: playerTemplate.mana - spellCost[Spell.Recharge], }); }); }); diff --git a/scripts/aoc2015/day22/utils.ts b/scripts/aoc2015/day22/utils.ts index c57090b..6be0e0f 100644 --- a/scripts/aoc2015/day22/utils.ts +++ b/scripts/aoc2015/day22/utils.ts @@ -108,18 +108,16 @@ export function castSpell( if (!isSpellAvailable(author, target, spell)) { throw new Error("Spell not available"); } + author.mana = author.mana - spellCost[spell]; switch (spell) { case Spell.MagicMissile: - author.mana = author.mana - 53; target.hitPoints = target.hitPoints - 4; break; case Spell.Drain: - author.mana = author.mana - 73; author.hitPoints = author.hitPoints + 2; target.hitPoints = target.hitPoints - 2; break; case Spell.Shield: - author.mana = author.mana - 113; author.defense = author.defense + 7; author.effects = [...author.effects, { name: EffectName.Shielded, @@ -127,14 +125,12 @@ export function castSpell( }]; break; case Spell.Poison: - author.mana = author.mana - 173; target.effects = [...target.effects, { name: EffectName.Poisoned, charges: 6, }]; break; case Spell.Recharge: - author.mana = author.mana - 229; author.effects = [...author.effects, { name: EffectName.Recharging, charges: 5, From cde7a87400aace409ab92b32c2529b48c38deaca Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 11:15:35 +0200 Subject: [PATCH 18/27] Implement apply shielded effect --- scripts/aoc2015/day22/utils.test.ts | 51 +++++++++++++++++++++++++++-- scripts/aoc2015/day22/utils.ts | 33 ++++++++++--------- 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/scripts/aoc2015/day22/utils.test.ts b/scripts/aoc2015/day22/utils.test.ts index de32e32..4625d89 100644 --- a/scripts/aoc2015/day22/utils.test.ts +++ b/scripts/aoc2015/day22/utils.test.ts @@ -7,6 +7,8 @@ import { cloneChar, EffectName, isSpellAvailable, + RECHARGE_GAIN, + SHIELD_DEFENSE_GAIN, Spell, spellCost, } from "./utils.ts"; @@ -89,7 +91,7 @@ describe("castSpell", function () { name: EffectName.Shielded, charges: 6, }], - defense: playerTemplate.defense + 7, + defense: playerTemplate.defense + SHIELD_DEFENSE_GAIN, mana: playerTemplate.mana - spellCost[Spell.Shield], }); }); @@ -197,7 +199,7 @@ describe("applyEffects", function () { mana: 0, effects: [], }; - it("should validate ticks and end exit of shielded effect", function () { + it.only("should validate ticks and end exit of shielded effect", function () { const player = cloneChar(playerTemplate); const boss = cloneChar(bossTemplate); castSpell(player, boss, Spell.Shield); @@ -206,7 +208,7 @@ describe("applyEffects", function () { ...playerTemplate, mana: playerTemplate.mana - spellCost[Spell.Shield], effects: [{ name: EffectName.Shielded, charges: 5 }], - defense: playerTemplate.defense + 7, + defense: playerTemplate.defense + SHIELD_DEFENSE_GAIN, }); assertEquals(boss, bossTemplate); applyEffects(player, boss); @@ -221,7 +223,50 @@ describe("applyEffects", function () { }); }); it("should validate ticks and end exit of poisoned effect", function () { + const player = cloneChar(playerTemplate); + const boss = cloneChar(bossTemplate); + castSpell(player, boss, Spell.Poison); + applyEffects(player, boss); + assertEquals(boss, { + ...bossTemplate, + effects: [{ name: EffectName.Poisoned, charges: 5 }], + hitPoints: bossTemplate.hitPoints - 3, + }); + applyEffects(player, boss); + assertEquals(boss, { + ...bossTemplate, + effects: [{ name: EffectName.Poisoned, charges: 4 }], + hitPoints: bossTemplate.hitPoints - 2 * 3, + }); + applyEffects(player, boss); + applyEffects(player, boss); + applyEffects(player, boss); + applyEffects(player, boss); + assertEquals(boss, { + ...bossTemplate, + effects: [], + hitPoints: bossTemplate.hitPoints - 6 * 3, + }); }); it("should validate ticks and end exit of recharging effect", function () { + const player = cloneChar(playerTemplate); + const boss = cloneChar(bossTemplate); + castSpell(player, boss, Spell.Recharge); + applyEffects(player, boss); + assertEquals(player, { + ...playerTemplate, + mana: playerTemplate.mana - spellCost[Spell.Recharge] + RECHARGE_GAIN, + effects: [{ name: EffectName.Recharging, charges: 4 }], + }); + assertEquals(boss, bossTemplate); + applyEffects(player, boss); + applyEffects(player, boss); + applyEffects(player, boss); + applyEffects(player, boss); + assertEquals(player, { + ...playerTemplate, + mana: playerTemplate.mana - spellCost[Spell.Shield] + 5 * RECHARGE_GAIN, + effects: [], + }); }); }); diff --git a/scripts/aoc2015/day22/utils.ts b/scripts/aoc2015/day22/utils.ts index 6be0e0f..698a9f8 100644 --- a/scripts/aoc2015/day22/utils.ts +++ b/scripts/aoc2015/day22/utils.ts @@ -28,21 +28,6 @@ export type EffectBlueprint = { // poisoned damage 3 for 6 turns // recharging gain 101 mana for 5 turns -const effectBlueprints: EffectBlueprint[] = [{ - name: EffectName.Shielded, - onEffectInit: function ( - { author }: { target: Character; author: Character }, - ) { - author.defense = author.defense + 7; - }, - onEffectExpires: function ( - { author }: { target: Character; author: Character }, - ) { - author.defense = author.defense - 7; - }, - charges: 6, -}]; - export type Effect = { name: EffectName; charges: number; @@ -100,6 +85,9 @@ export function isSpellAvailable( // Poison costs 173 mana. It starts an effect that lasts for 6 turns. At the start of each turn while it is active, it deals the boss 3 damage. // Recharge costs 229 mana. It starts an effect that lasts for 5 turns. At the start of each turn while it is active, it gives you 101 new mana. +export const RECHARGE_GAIN = 101; +export const SHIELD_DEFENSE_GAIN = 7; + export function castSpell( author: Character, target: Character, @@ -118,7 +106,7 @@ export function castSpell( target.hitPoints = target.hitPoints - 2; break; case Spell.Shield: - author.defense = author.defense + 7; + author.defense = author.defense + SHIELD_DEFENSE_GAIN; author.effects = [...author.effects, { name: EffectName.Shielded, charges: 6, @@ -146,6 +134,19 @@ export function applyEffects( target: Character, ): void { console.log("apply effects"); + const shieldEffect = author.effects.find((e) => + e.name === EffectName.Shielded + ); + if (shieldEffect) { + if (shieldEffect.charges === 1) { + author.effects = author.effects.filter((e) => + e.name !== EffectName.Shielded + ); + author.defense = author.defense - SHIELD_DEFENSE_GAIN; + } else { + shieldEffect.charges = shieldEffect.charges - 1; + } + } } export function cloneChar(char: Character): Character { From d4f994c4d074541b2f2f333c557e2d3b31077dbe Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 11:21:59 +0200 Subject: [PATCH 19/27] Implement apply recharging effect --- scripts/aoc2015/day22/utils.test.ts | 9 +++++++-- scripts/aoc2015/day22/utils.ts | 14 +++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/scripts/aoc2015/day22/utils.test.ts b/scripts/aoc2015/day22/utils.test.ts index 4625d89..23e7b61 100644 --- a/scripts/aoc2015/day22/utils.test.ts +++ b/scripts/aoc2015/day22/utils.test.ts @@ -248,10 +248,15 @@ describe("applyEffects", function () { hitPoints: bossTemplate.hitPoints - 6 * 3, }); }); - it("should validate ticks and end exit of recharging effect", function () { + it.only("should validate ticks and end exit of recharging effect", function () { const player = cloneChar(playerTemplate); const boss = cloneChar(bossTemplate); castSpell(player, boss, Spell.Recharge); + assertEquals(player, { + ...playerTemplate, + mana: playerTemplate.mana - spellCost[Spell.Recharge], + effects: [{ name: EffectName.Recharging, charges: 5 }], + }); applyEffects(player, boss); assertEquals(player, { ...playerTemplate, @@ -265,7 +270,7 @@ describe("applyEffects", function () { applyEffects(player, boss); assertEquals(player, { ...playerTemplate, - mana: playerTemplate.mana - spellCost[Spell.Shield] + 5 * RECHARGE_GAIN, + mana: playerTemplate.mana - spellCost[Spell.Recharge] + 5 * RECHARGE_GAIN, effects: [], }); }); diff --git a/scripts/aoc2015/day22/utils.ts b/scripts/aoc2015/day22/utils.ts index 698a9f8..2058639 100644 --- a/scripts/aoc2015/day22/utils.ts +++ b/scripts/aoc2015/day22/utils.ts @@ -133,7 +133,6 @@ export function applyEffects( author: Character, target: Character, ): void { - console.log("apply effects"); const shieldEffect = author.effects.find((e) => e.name === EffectName.Shielded ); @@ -147,6 +146,19 @@ export function applyEffects( shieldEffect.charges = shieldEffect.charges - 1; } } + const rechargingEffect = author.effects.find((e) => + e.name === EffectName.Recharging + ); + if (rechargingEffect) { + author.mana = author.mana + RECHARGE_GAIN; + if (rechargingEffect.charges === 1) { + author.effects = author.effects.filter((e) => + e.name !== EffectName.Recharging + ); + } else { + rechargingEffect.charges = rechargingEffect.charges - 1; + } + } } export function cloneChar(char: Character): Character { From e934ffbf316312a271f32b5f490d940d09506a7c Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 11:23:33 +0200 Subject: [PATCH 20/27] Implement apply poisoned effect --- scripts/aoc2015/day22/utils.test.ts | 4 ++-- scripts/aoc2015/day22/utils.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/aoc2015/day22/utils.test.ts b/scripts/aoc2015/day22/utils.test.ts index 23e7b61..505543c 100644 --- a/scripts/aoc2015/day22/utils.test.ts +++ b/scripts/aoc2015/day22/utils.test.ts @@ -199,7 +199,7 @@ describe("applyEffects", function () { mana: 0, effects: [], }; - it.only("should validate ticks and end exit of shielded effect", function () { + it("should validate ticks and end exit of shielded effect", function () { const player = cloneChar(playerTemplate); const boss = cloneChar(bossTemplate); castSpell(player, boss, Spell.Shield); @@ -248,7 +248,7 @@ describe("applyEffects", function () { hitPoints: bossTemplate.hitPoints - 6 * 3, }); }); - it.only("should validate ticks and end exit of recharging effect", function () { + it("should validate ticks and end exit of recharging effect", function () { const player = cloneChar(playerTemplate); const boss = cloneChar(bossTemplate); castSpell(player, boss, Spell.Recharge); diff --git a/scripts/aoc2015/day22/utils.ts b/scripts/aoc2015/day22/utils.ts index 2058639..43b898b 100644 --- a/scripts/aoc2015/day22/utils.ts +++ b/scripts/aoc2015/day22/utils.ts @@ -159,6 +159,20 @@ export function applyEffects( rechargingEffect.charges = rechargingEffect.charges - 1; } } + + const poisonedEffect = target.effects.find((e) => + e.name === EffectName.Poisoned + ); + if (poisonedEffect) { + target.hitPoints = target.hitPoints - 3; + if (poisonedEffect.charges === 1) { + target.effects = target.effects.filter((e) => + e.name !== EffectName.Poisoned + ); + } else { + poisonedEffect.charges = poisonedEffect.charges - 1; + } + } } export function cloneChar(char: Character): Character { From 2d113dae504fc73f9b982b4418a0c565acfb9d3c Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 11:48:35 +0200 Subject: [PATCH 21/27] Implement isThereAWinner --- scripts/aoc2015/day22/utils.test.ts | 51 +++++++++++++++++++++++++++++ scripts/aoc2015/day22/utils.ts | 13 ++++++++ 2 files changed, 64 insertions(+) diff --git a/scripts/aoc2015/day22/utils.test.ts b/scripts/aoc2015/day22/utils.test.ts index 505543c..f3e8cd2 100644 --- a/scripts/aoc2015/day22/utils.test.ts +++ b/scripts/aoc2015/day22/utils.test.ts @@ -7,6 +7,7 @@ import { cloneChar, EffectName, isSpellAvailable, + isThereAWinner, RECHARGE_GAIN, SHIELD_DEFENSE_GAIN, Spell, @@ -275,3 +276,53 @@ describe("applyEffects", function () { }); }); }); + +describe("isThereAWinner", function () { + const playerTemplate: Character = { + hitPoints: 100, + damage: 0, + defense: 0, + mana: 500, + effects: [], + }; + const bossTemplate: Character = { + hitPoints: 200, + damage: 10, + defense: 2, + mana: 0, + effects: [], + }; + it("should return boss if player has no more mana for next spell", function () { + assertEquals( + isThereAWinner({ + ...playerTemplate, + mana: 30, + }, bossTemplate), + "boss", + ); + }); + it("should return boss if player hitPoints are bellow one", function () { + assertEquals( + isThereAWinner({ + ...playerTemplate, + hitPoints: 0, + }, bossTemplate), + "boss", + ); + }); + it("should return player if boss hitPoints are bellow one", function () { + assertEquals( + isThereAWinner(playerTemplate, { + ...bossTemplate, + hitPoints: 0, + }), + "player", + ); + }); + it("should return null otherwise if fight should continue", function () { + assertEquals( + isThereAWinner(playerTemplate, bossTemplate), + null, + ); + }); +}); diff --git a/scripts/aoc2015/day22/utils.ts b/scripts/aoc2015/day22/utils.ts index 43b898b..39c7c79 100644 --- a/scripts/aoc2015/day22/utils.ts +++ b/scripts/aoc2015/day22/utils.ts @@ -178,3 +178,16 @@ export function applyEffects( export function cloneChar(char: Character): Character { return Object.assign({}, char); } + +export function isThereAWinner( + player: Character, + boss: Character, +): "player" | "boss" | null { + if (boss.hitPoints < 1) { + return "player"; + } + if (player.hitPoints < 1 || player.mana < spellCost[Spell.MagicMissile]) { + return "boss"; + } + return null; +} From 3ddfb6d339d69761a15dec743f9325f15cbcf2ab Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 11:52:42 +0200 Subject: [PATCH 22/27] Implement getAvailableSpells --- scripts/aoc2015/day22/utils.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/aoc2015/day22/utils.ts b/scripts/aoc2015/day22/utils.ts index 39c7c79..81ec61c 100644 --- a/scripts/aoc2015/day22/utils.ts +++ b/scripts/aoc2015/day22/utils.ts @@ -79,6 +79,19 @@ export function isSpellAvailable( return true; } +export function getAvailableSpells( + author: Character, + target: Character, +): Spell[] { + return [ + Spell.MagicMissile, + Spell.Drain, + Spell.Shield, + Spell.Poison, + Spell.Recharge, + ].filter((spell) => isSpellAvailable(author, target, spell)); +} + // Magic Missile costs 53 mana. It instantly does 4 damage. // Drain costs 73 mana. It instantly does 2 damage and heals you for 2 hit points. // Shield costs 113 mana. It starts an effect that lasts for 6 turns. While it is active, your armor is increased by 7. From e4b3a5b5c799e10ccf57c70db51bf8ca64783550 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 15:00:01 +0200 Subject: [PATCH 23/27] Solve demo example --- scripts/aoc2015/day21/utils.ts | 2 +- .../aoc2015/day22/{puzzle1.tsx => puzzle1.ts} | 171 +++++++++++++++++- scripts/aoc2015/day22/utils.test.ts | 73 +++++++- scripts/aoc2015/day22/utils.ts | 8 +- 4 files changed, 242 insertions(+), 12 deletions(-) rename scripts/aoc2015/day22/{puzzle1.tsx => puzzle1.ts} (58%) diff --git a/scripts/aoc2015/day21/utils.ts b/scripts/aoc2015/day21/utils.ts index 74dc29a..b80f987 100644 --- a/scripts/aoc2015/day21/utils.ts +++ b/scripts/aoc2015/day21/utils.ts @@ -218,7 +218,7 @@ export function charAAttacksCharB(charA: Character, charB: Character) { } } -enum Turn { +export enum Turn { Player = "player", Boss = "boss", } diff --git a/scripts/aoc2015/day22/puzzle1.tsx b/scripts/aoc2015/day22/puzzle1.ts similarity index 58% rename from scripts/aoc2015/day22/puzzle1.tsx rename to scripts/aoc2015/day22/puzzle1.ts index 07016d4..29b56b0 100644 --- a/scripts/aoc2015/day22/puzzle1.tsx +++ b/scripts/aoc2015/day22/puzzle1.ts @@ -120,20 +120,33 @@ You start with 50 hit points and 500 mana points. The boss's actual stats are in What is the least amount of mana you can spend and still win the fight? (Do not include mana recharge effects as "spending" negative mana.) */ -import { Character } from "../day21/utils.ts"; +import { charAAttacksCharB, Turn } from "../day21/utils.ts"; +import { + applyEffects, + castSpell, + Character, + cloneChar, + getAvailableSpells, + isThereAWinner, + Spell, + spellCost, +} from "./utils.ts"; const boss: Character = { hitPoints: 71, damage: 10, defense: 0, -} + effects: [], + mana: 0, +}; const basicPlayer: Character = { hitPoints: 50, mana: 500, damage: 0, defense: 0, -} + effects: [], +}; // implement effects (after spells) // effects: @@ -160,7 +173,6 @@ const basicPlayer: Character = { // lazy algorithm - always pick the esiest way according to rules // - I don't know, If my rules are right - // ALGORITHM 2 // brute force - try all combinations, pick the best // I will need to run fight simulations @@ -171,7 +183,6 @@ const basicPlayer: Character = { // player dies // there is other scenario with less mana spent - // what describes single step in scenario // inputs: // who will move (Player/ Boss) @@ -183,4 +194,152 @@ const basicPlayer: Character = { // list of combinations of Player and Boss (very likely the count will differ, some combinations will be added, some wil), and mana spent // function getListOfAvailableSpells -// depends on player mana, player effects, boss effects \ No newline at end of file +// depends on player mana, player effects, boss effects + +type Scenario = { + player: Character; + boss: Character; + currentManaSpent: number; + turn: Turn; + spellsList: Spell[]; +}; + +// lets have a mostEfficientlyManaSpent variable for cases, when player wins +let mostEfficientlyManaSpent: null | number = null; +// lets have a list of scenarios with initial scenario (initial stats, player turn) +let activeScenarios: Scenario[] = [{ + player: basicPlayer, + boss: boss, + turn: Turn.Player, + currentManaSpent: 0, + spellsList: [], +}]; + +activeScenarios = [{ + player: { + hitPoints: 10, + mana: 250, + defense: 0, + damage: 0, + effects: [], + }, + boss: { + hitPoints: 13, + mana: 0, + defense: 0, + damage: 8, + effects: [], + }, + turn: Turn.Player, + currentManaSpent: 0, + spellsList: [], +}]; + +function tryToUpdateMostEfficientlyManaSpent(playerManaSpent: number) { + console.log("tryToUpdateMostEfficientlyManaSpent", playerManaSpent); + if ( + mostEfficientlyManaSpent === null || + playerManaSpent < mostEfficientlyManaSpent + ) { + mostEfficientlyManaSpent = playerManaSpent; + } +} + +// for each scenario in list +while (activeScenarios.length > 0) { + // lets have a list with new scenarios + const newScenarios: Scenario[] = []; + for (const scenario of activeScenarios) { + // apply effects + applyEffects(scenario.player, scenario.boss); + const result = isThereAWinner( + scenario.player, + scenario.boss, + scenario.turn === Turn.Player, + ); + console.log("result", result); + // check if we should continue + if (result === "player") { + // if player wins, try to update mostEfficientlyManaSpent + + console.log("player won!"); + tryToUpdateMostEfficientlyManaSpent(scenario.currentManaSpent); + break; + } + // if its boss turn + if (scenario.turn === Turn.Boss) { + // attack by boss + charAAttacksCharB(scenario.boss, scenario.player); + // check if we should continue + const resultAfterAttack = isThereAWinner( + scenario.player, + scenario.boss, + ); + console.log("result", resultAfterAttack); + // if boss wins do nothing + // if null, add a new scenario to the list of new scenarios + if (resultAfterAttack === null) { + newScenarios.push({ + player: cloneChar(scenario.player), + boss: cloneChar(scenario.boss), + currentManaSpent: scenario.currentManaSpent, + turn: Turn.Player, + spellsList: scenario.spellsList, + }); + } + } else { + // scenario player + // get list of available spells + const availableSpells = getAvailableSpells( + scenario.player, + scenario.boss, + ); + console.log("availableSpells", availableSpells); + // loop for each available spell + for (const spell of availableSpells) { + // player casts a spell + const player = cloneChar(scenario.player); + const boss = cloneChar(scenario.boss); + castSpell(player, boss, spell); + const manaSpent = spellCost[spell]; + const result = isThereAWinner(player, boss, false); + console.log("result", result); + if (result === "player") { + // if player wins, try to update mostEfficientlyManaSpent + tryToUpdateMostEfficientlyManaSpent( + scenario.currentManaSpent + manaSpent, + ); + // if null, + } else if (result === null) { + // compute new mana spent + // if mostEfficientlyManaSpent === null or new mana spent < mostEfficientlyManaSpent + if ( + mostEfficientlyManaSpent === null || + scenario.currentManaSpent + manaSpent < mostEfficientlyManaSpent + ) { + // add a new scenario to the list of new scenarios with new mana spent + newScenarios.push({ + player: cloneChar(player), + boss: cloneChar(boss), + currentManaSpent: scenario.currentManaSpent + manaSpent, + turn: Turn.Boss, + spellsList: scenario.spellsList.concat([spell]), + }); + } + } + } + } + } + // console.log("newScenarios", newScenarios); + if (mostEfficientlyManaSpent === null) { + activeScenarios = newScenarios; + } else { + activeScenarios = newScenarios.filter((ns) => + ns.currentManaSpent < (mostEfficientlyManaSpent as number) + ); + } + console.log("activeScenarios", activeScenarios); +} + +console.log("mostEfficientlyManaSpent", mostEfficientlyManaSpent); +// diff --git a/scripts/aoc2015/day22/utils.test.ts b/scripts/aoc2015/day22/utils.test.ts index f3e8cd2..fe1124f 100644 --- a/scripts/aoc2015/day22/utils.test.ts +++ b/scripts/aoc2015/day22/utils.test.ts @@ -1,5 +1,6 @@ import { assertEquals, assertThrows } from "@std/assert"; import { beforeEach, describe, it } from "@std/testing/bdd"; +import { charAAttacksCharB } from "../day21/utils.ts"; import { applyEffects, castSpell, @@ -294,13 +295,29 @@ describe("isThereAWinner", function () { }; it("should return boss if player has no more mana for next spell", function () { assertEquals( - isThereAWinner({ - ...playerTemplate, - mana: 30, - }, bossTemplate), + isThereAWinner( + { + ...playerTemplate, + mana: 30, + }, + bossTemplate, + true, + ), "boss", ); }); + it("should return null if player has no more mana and it's boss turn", function () { + assertEquals( + isThereAWinner( + { + ...playerTemplate, + mana: 30, + }, + bossTemplate, + ), + null, + ); + }); it("should return boss if player hitPoints are bellow one", function () { assertEquals( isThereAWinner({ @@ -326,3 +343,51 @@ describe("isThereAWinner", function () { ); }); }); + +describe("a fight", function () { + const player: Character = { + hitPoints: 10, + mana: 250, + defense: 0, + damage: 0, + effects: [], + }; + const boss: Character = { + hitPoints: 13, + mana: 0, + defense: 0, + damage: 8, + effects: [], + }; + it("player should win", function () { + // Player turn + applyEffects(player, boss); + assertEquals(isThereAWinner(player, boss, true), null); + castSpell(player, boss, Spell.Poison); + isThereAWinner(player, boss, true); + // Boss Turn + applyEffects(player, boss); + assertEquals(isThereAWinner(player, boss, false), null); + charAAttacksCharB(boss, player); + assertEquals(isThereAWinner(player, boss, false), null); + + // Player turn + applyEffects(player, boss); + isThereAWinner( + player, + boss, + true, + ); + castSpell(player, boss, Spell.MagicMissile); + assertEquals(isThereAWinner(player, boss, true), null); + + // Boss Turn + applyEffects(player, boss); + const result = isThereAWinner( + player, + boss, + false, + ); + assertEquals(result, "player"); + }); +}); diff --git a/scripts/aoc2015/day22/utils.ts b/scripts/aoc2015/day22/utils.ts index 81ec61c..83feb09 100644 --- a/scripts/aoc2015/day22/utils.ts +++ b/scripts/aoc2015/day22/utils.ts @@ -1,3 +1,5 @@ +import { Turn } from "../day21/utils.ts"; + export type Character = { hitPoints: number; damage: number; @@ -195,11 +197,15 @@ export function cloneChar(char: Character): Character { export function isThereAWinner( player: Character, boss: Character, + isPlayerTurn?: boolean, ): "player" | "boss" | null { if (boss.hitPoints < 1) { return "player"; } - if (player.hitPoints < 1 || player.mana < spellCost[Spell.MagicMissile]) { + if (player.hitPoints < 1) { + return "boss"; + } + if (isPlayerTurn && player.mana < spellCost[Spell.MagicMissile]) { return "boss"; } return null; From 9118d8796385c75dd46d4105ebbd51f9332d28b6 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 15:29:13 +0200 Subject: [PATCH 24/27] Solve demo example II --- scripts/aoc2015/day22/puzzle1.ts | 25 ++++++- scripts/aoc2015/day22/utils.test.ts | 109 ++++++++++++++++++++++++---- scripts/aoc2015/day22/utils.ts | 3 +- 3 files changed, 116 insertions(+), 21 deletions(-) diff --git a/scripts/aoc2015/day22/puzzle1.ts b/scripts/aoc2015/day22/puzzle1.ts index 29b56b0..f4817d9 100644 --- a/scripts/aoc2015/day22/puzzle1.ts +++ b/scripts/aoc2015/day22/puzzle1.ts @@ -215,6 +215,26 @@ let activeScenarios: Scenario[] = [{ spellsList: [], }]; +// activeScenarios = [{ +// player: { +// hitPoints: 10, +// mana: 250, +// defense: 0, +// damage: 0, +// effects: [], +// }, +// boss: { +// hitPoints: 13, +// mana: 0, +// defense: 0, +// damage: 8, +// effects: [], +// }, +// turn: Turn.Player, +// currentManaSpent: 0, +// spellsList: [], +// }]; + activeScenarios = [{ player: { hitPoints: 10, @@ -224,7 +244,7 @@ activeScenarios = [{ effects: [], }, boss: { - hitPoints: 13, + hitPoints: 14, mana: 0, defense: 0, damage: 8, @@ -236,7 +256,6 @@ activeScenarios = [{ }]; function tryToUpdateMostEfficientlyManaSpent(playerManaSpent: number) { - console.log("tryToUpdateMostEfficientlyManaSpent", playerManaSpent); if ( mostEfficientlyManaSpent === null || playerManaSpent < mostEfficientlyManaSpent @@ -275,7 +294,6 @@ while (activeScenarios.length > 0) { scenario.player, scenario.boss, ); - console.log("result", resultAfterAttack); // if boss wins do nothing // if null, add a new scenario to the list of new scenarios if (resultAfterAttack === null) { @@ -303,7 +321,6 @@ while (activeScenarios.length > 0) { castSpell(player, boss, spell); const manaSpent = spellCost[spell]; const result = isThereAWinner(player, boss, false); - console.log("result", result); if (result === "player") { // if player wins, try to update mostEfficientlyManaSpent tryToUpdateMostEfficientlyManaSpent( diff --git a/scripts/aoc2015/day22/utils.test.ts b/scripts/aoc2015/day22/utils.test.ts index fe1124f..142272d 100644 --- a/scripts/aoc2015/day22/utils.test.ts +++ b/scripts/aoc2015/day22/utils.test.ts @@ -345,26 +345,27 @@ describe("isThereAWinner", function () { }); describe("a fight", function () { - const player: Character = { - hitPoints: 10, - mana: 250, - defense: 0, - damage: 0, - effects: [], - }; - const boss: Character = { - hitPoints: 13, - mana: 0, - defense: 0, - damage: 8, - effects: [], - }; it("player should win", function () { + const player: Character = { + hitPoints: 10, + mana: 250, + defense: 0, + damage: 0, + effects: [], + }; + const boss: Character = { + hitPoints: 13, + mana: 0, + defense: 0, + damage: 8, + effects: [], + }; + // Player turn applyEffects(player, boss); assertEquals(isThereAWinner(player, boss, true), null); castSpell(player, boss, Spell.Poison); - isThereAWinner(player, boss, true); + isThereAWinner(player, boss, false); // Boss Turn applyEffects(player, boss); assertEquals(isThereAWinner(player, boss, false), null); @@ -379,7 +380,7 @@ describe("a fight", function () { true, ); castSpell(player, boss, Spell.MagicMissile); - assertEquals(isThereAWinner(player, boss, true), null); + assertEquals(isThereAWinner(player, boss, false), null); // Boss Turn applyEffects(player, boss); @@ -390,4 +391,80 @@ describe("a fight", function () { ); assertEquals(result, "player"); }); + + it("player should win II", function () { + const player: Character = { + hitPoints: 10, + mana: 250, + defense: 0, + damage: 0, + effects: [], + }; + const boss: Character = { + hitPoints: 13, + mana: 0, + defense: 0, + damage: 8, + effects: [], + }; + // Player turn + applyEffects(player, boss); + assertEquals(isThereAWinner(player, boss, true), null); + castSpell(player, boss, Spell.Recharge); + isThereAWinner(player, boss, false); + + // Boss Turn + applyEffects(player, boss); + assertEquals(isThereAWinner(player, boss, false), null); + charAAttacksCharB(boss, player); + assertEquals(isThereAWinner(player, boss, false), null); + + // Player turn + applyEffects(player, boss); + isThereAWinner(player, boss, true); + castSpell(player, boss, Spell.Shield); + assertEquals(isThereAWinner(player, boss, false), null); + + // Boss Turn + applyEffects(player, boss); + assertEquals(isThereAWinner(player, boss, false), null); + charAAttacksCharB(boss, player); + assertEquals(isThereAWinner(player, boss, false), null); + + // Player turn + applyEffects(player, boss); + isThereAWinner(player, boss, true); + castSpell(player, boss, Spell.Drain); + assertEquals(isThereAWinner(player, boss, false), null); + + // Boss Turn + applyEffects(player, boss); + assertEquals(isThereAWinner(player, boss, false), null); + charAAttacksCharB(boss, player); + assertEquals(isThereAWinner(player, boss, false), null); + + // Player turn + applyEffects(player, boss); + isThereAWinner(player, boss, true); + console.log("player", player); + castSpell(player, boss, Spell.Poison); + assertEquals(isThereAWinner(player, boss, false), null); + + // Boss Turn + applyEffects(player, boss); + assertEquals(isThereAWinner(player, boss, false), null); + charAAttacksCharB(boss, player); + assertEquals(isThereAWinner(player, boss, false), null); + + // Player turn + applyEffects(player, boss); + isThereAWinner(player, boss, true); + castSpell(player, boss, Spell.MagicMissile); + assertEquals(isThereAWinner(player, boss, false), null); + + // Boss Turn + applyEffects(player, boss); + console.log("player", player); + assertEquals(isThereAWinner(player, boss, false), "player"); + }); }); diff --git a/scripts/aoc2015/day22/utils.ts b/scripts/aoc2015/day22/utils.ts index 83feb09..c49a51f 100644 --- a/scripts/aoc2015/day22/utils.ts +++ b/scripts/aoc2015/day22/utils.ts @@ -191,7 +191,8 @@ export function applyEffects( } export function cloneChar(char: Character): Character { - return Object.assign({}, char); + return JSON.parse(JSON.stringify(char)); + // return Object.assign({}, char); } export function isThereAWinner( From 07d4c472b51523e3d48a76553f9e794817cda451 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 15:31:18 +0200 Subject: [PATCH 25/27] Cleanup solve puzzle1 --- scripts/aoc2015/day22/puzzle1.ts | 42 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/scripts/aoc2015/day22/puzzle1.ts b/scripts/aoc2015/day22/puzzle1.ts index f4817d9..57f4ad3 100644 --- a/scripts/aoc2015/day22/puzzle1.ts +++ b/scripts/aoc2015/day22/puzzle1.ts @@ -215,6 +215,7 @@ let activeScenarios: Scenario[] = [{ spellsList: [], }]; +// example I // activeScenarios = [{ // player: { // hitPoints: 10, @@ -235,25 +236,26 @@ let activeScenarios: Scenario[] = [{ // spellsList: [], // }]; -activeScenarios = [{ - player: { - hitPoints: 10, - mana: 250, - defense: 0, - damage: 0, - effects: [], - }, - boss: { - hitPoints: 14, - mana: 0, - defense: 0, - damage: 8, - effects: [], - }, - turn: Turn.Player, - currentManaSpent: 0, - spellsList: [], -}]; +// example II +// activeScenarios = [{ +// player: { +// hitPoints: 10, +// mana: 250, +// defense: 0, +// damage: 0, +// effects: [], +// }, +// boss: { +// hitPoints: 14, +// mana: 0, +// defense: 0, +// damage: 8, +// effects: [], +// }, +// turn: Turn.Player, +// currentManaSpent: 0, +// spellsList: [], +// }]; function tryToUpdateMostEfficientlyManaSpent(playerManaSpent: number) { if ( @@ -276,7 +278,6 @@ while (activeScenarios.length > 0) { scenario.boss, scenario.turn === Turn.Player, ); - console.log("result", result); // check if we should continue if (result === "player") { // if player wins, try to update mostEfficientlyManaSpent @@ -312,7 +313,6 @@ while (activeScenarios.length > 0) { scenario.player, scenario.boss, ); - console.log("availableSpells", availableSpells); // loop for each available spell for (const spell of availableSpells) { // player casts a spell From 701f5d81cc78f622d95026ce281900bc8d2cf5b4 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 15:33:37 +0200 Subject: [PATCH 26/27] Solve puzzle2. --- scripts/aoc2015/day22/puzzle1.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/aoc2015/day22/puzzle1.ts b/scripts/aoc2015/day22/puzzle1.ts index 57f4ad3..8d37e79 100644 --- a/scripts/aoc2015/day22/puzzle1.ts +++ b/scripts/aoc2015/day22/puzzle1.ts @@ -273,6 +273,11 @@ while (activeScenarios.length > 0) { for (const scenario of activeScenarios) { // apply effects applyEffects(scenario.player, scenario.boss); + // PUZZLE2 ONLY START! + if (scenario.turn === Turn.Player) { + scenario.player.hitPoints = scenario.player.hitPoints - 1; + } + // PUZZLE2 ONLY END! const result = isThereAWinner( scenario.player, scenario.boss, From 85a13c5b0cd7ccf143493b004e866ef5717fe478 Mon Sep 17 00:00:00 2001 From: lukasbicus Date: Wed, 9 Oct 2024 15:36:36 +0200 Subject: [PATCH 27/27] Cleanup --- scripts/aoc2015/day22/puzzle1.ts | 3 --- scripts/aoc2015/day22/utils.test.ts | 2 -- 2 files changed, 5 deletions(-) diff --git a/scripts/aoc2015/day22/puzzle1.ts b/scripts/aoc2015/day22/puzzle1.ts index 8d37e79..1bebcda 100644 --- a/scripts/aoc2015/day22/puzzle1.ts +++ b/scripts/aoc2015/day22/puzzle1.ts @@ -287,7 +287,6 @@ while (activeScenarios.length > 0) { if (result === "player") { // if player wins, try to update mostEfficientlyManaSpent - console.log("player won!"); tryToUpdateMostEfficientlyManaSpent(scenario.currentManaSpent); break; } @@ -352,7 +351,6 @@ while (activeScenarios.length > 0) { } } } - // console.log("newScenarios", newScenarios); if (mostEfficientlyManaSpent === null) { activeScenarios = newScenarios; } else { @@ -360,7 +358,6 @@ while (activeScenarios.length > 0) { ns.currentManaSpent < (mostEfficientlyManaSpent as number) ); } - console.log("activeScenarios", activeScenarios); } console.log("mostEfficientlyManaSpent", mostEfficientlyManaSpent); diff --git a/scripts/aoc2015/day22/utils.test.ts b/scripts/aoc2015/day22/utils.test.ts index 142272d..5f6a510 100644 --- a/scripts/aoc2015/day22/utils.test.ts +++ b/scripts/aoc2015/day22/utils.test.ts @@ -446,7 +446,6 @@ describe("a fight", function () { // Player turn applyEffects(player, boss); isThereAWinner(player, boss, true); - console.log("player", player); castSpell(player, boss, Spell.Poison); assertEquals(isThereAWinner(player, boss, false), null); @@ -464,7 +463,6 @@ describe("a fight", function () { // Boss Turn applyEffects(player, boss); - console.log("player", player); assertEquals(isThereAWinner(player, boss, false), "player"); }); });