const sample = await Deno.readTextFile("sample.txt"); const input = await Deno.readTextFile("input.txt"); abstract class HandType { abstract get isMatch(): boolean; abstract get name(): string; abstract get weight(): number; } class FiveOfAKind extends HandType { private _isMatch: boolean; constructor(hand: Hand) { super(); this._isMatch = hand.doesHaveSameCardCountIncludedJokers(5); } get weight() { return 1; } get isMatch() { return this._isMatch; } get name() { return "Five of a Kind"; } } class FourOfAKind extends HandType { private _isMatch: boolean; constructor(hand: Hand) { super(); this._isMatch = hand.doesHaveSameCardCountIncludedJokers(4, 1); } get weight() { return 2; } get isMatch() { return this._isMatch; } get name() { return "Four of a Kind"; } } class FullHouse extends HandType { private _isMatch: boolean; constructor(hand: Hand) { super(); this._isMatch = hand.doesHaveSameCardCountIncludedJokers(3, 2); } get weight() { return 3; } get isMatch() { return this._isMatch; } get name() { return "Full House"; } } class ThreeOfAKind extends HandType { private _isMatch: boolean; constructor(hand: Hand) { super(); this._isMatch = hand.doesHaveSameCardCountIncludedJokers(3, 1, 1); } get weight() { return 4; } get isMatch() { return this._isMatch; } get name() { return "Three of a Kind"; } } class TwoPair extends HandType { private _isMatch: boolean; constructor(hand: Hand) { super(); this._isMatch = hand.doesHaveSameCardCountIncludedJokers(2, 2, 1); } get weight() { return 5; } get isMatch() { return this._isMatch; } get name() { return "Two Pair"; } } class OnePair extends HandType { private _isMatch: boolean; constructor(hand: Hand) { super(); this._isMatch = hand.doesHaveSameCardCountIncludedJokers(2, 1, 1, 1); } get weight() { return 6; } get isMatch() { return this._isMatch; } get name() { return "One Pair"; } } class HighCard extends HandType { private _isMatch: boolean; constructor(hand: Hand) { super(); this._isMatch = hand.doesHaveSameCardCountIncludedJokers(1, 1, 1, 1, 1); } get weight() { return 7; } get isMatch() { return this._isMatch; } get name() { return "High Card"; } } class Card { private _label: string; private _weight: number; private _isJoker: boolean; constructor( label: string, cardsWeights: Readonly, includeJokers: boolean, ) { this._label = label; this._weight = cardsWeights.indexOf(label) + 1; this._isJoker = includeJokers && label === "J"; } get label() { return this._label; } get weight() { return this._weight; } get isJoker() { return this._isJoker; } } class Hand { private _card1: Card; private _card2: Card; private _card3: Card; private _card4: Card; private _card5: Card; private _handTypes: HandType[]; private _handType: HandType | undefined; private _groupedHand: string[][]; constructor( public hand: string, cardsWeights: Readonly, includeJokers: boolean, ) { this._card1 = new Card(this.hand[0], cardsWeights, includeJokers); this._card2 = new Card(this.hand[1], cardsWeights, includeJokers); this._card3 = new Card(this.hand[2], cardsWeights, includeJokers); this._card4 = new Card(this.hand[3], cardsWeights, includeJokers); this._card5 = new Card(this.hand[4], cardsWeights, includeJokers); this._groupedHand = this.cards.reduce((acc, card): string[][] => { if (acc.flatMap((entry) => entry).includes(card.label)) { return acc.map((entry): string[] => entry[0] === card.label ? [...entry, entry[0]] : entry ); } if (includeJokers && card.isJoker) return acc; return [...acc, [card.label]]; }, [] as string[][]).sort((a, b) => b.length - a.length); this._handTypes = [ new FiveOfAKind(this), new FourOfAKind(this), new FullHouse(this), new ThreeOfAKind(this), new TwoPair(this), new OnePair(this), new HighCard(this), ]; this._handType = this._handTypes .filter((handType) => handType.isMatch) .sort((a, b) => a.weight - b.weight) .find((handType) => handType.isMatch); } get handType() { return this._handType; } get cards() { return [this._card1, this._card2, this._card3, this._card4, this._card5]; } get jokerCount() { return this.cards.filter((card) => card.isJoker).length; } get groupedHand() { return this._groupedHand; } doesHaveSameCardCountIncludedJokers(...cardCounts: number[]): boolean { const sum = cardCounts.reduce((acc, cardCount) => acc + cardCount, 0); if (sum !== 5) throw new Error("Invalid card counts"); let jokerCount = this.jokerCount; const groupedHand = this.groupedHand.slice(); for (const cardCount of cardCounts) { for (const group of groupedHand) { if (group.length === cardCount) { groupedHand.splice(groupedHand.indexOf(group), 1); cardCounts.splice(cardCounts.indexOf(cardCount), 1); } } } for (const cardCount of cardCounts) { for (const group of groupedHand) { if (group.length <= cardCount) { const d = cardCount - group.length; if (d <= jokerCount) { jokerCount -= d; groupedHand.splice(groupedHand.indexOf(group), 1); cardCounts.splice(cardCounts.indexOf(cardCount), 1); } } } } if (cardCounts.length === groupedHand.flatMap((entry) => entry).length) { return true; } if (cardCounts.length === 0 && groupedHand.length === 0) return true; if (jokerCount === 0) return false; const remainingSum = cardCounts.reduce( (acc, cardCount) => acc + cardCount, 0, ); if (remainingSum === jokerCount) { return true; } return false; } get card1() { return this._card1; } get card2() { return this._card2; } get card3() { return this._card3; } get card4() { return this._card4; } get card5() { return this._card5; } } class Line { private _hand: Hand; private _bid: number; constructor( public line: string, cardsWeights: Readonly, includeJokers: boolean, ) { this._hand = new Hand(this.line.split(" ")[0], cardsWeights, includeJokers); this._bid = Number(this.line.split(" ")[1]); } get hand() { return this._hand; } get bid() { return this._bid; } } class Game { private _lines: Line[]; constructor( public input: string, cardsWeights: Readonly, includeJokers: boolean = false, ) { this._lines = input.split("\n").filter(Boolean).map((line) => new Line(line, cardsWeights, includeJokers) ); } get lines() { return this._lines; } } const solvePart1 = (data: string): number => { const cardsWeights = [ "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A", ] as const; const game = new Game(data, cardsWeights); return game.lines.sort((a, b) => { const weightA = a.hand.handType?.weight || 0; const weightB = b.hand.handType?.weight || 0; if (weightA > weightB) return -1; if (weightA < weightB) return 1; for (let i = 0; i < 5; i += 1) { const valueA = a.hand.cards[i].weight; const valueB = b.hand.cards[i].weight; if (valueA > valueB) return 1; if (valueA < valueB) return -1; } return 0; }) .reduce((sum, line, index) => { return sum + ((index + 1) * line.bid); }, 0); }; console.log("Sample:", solvePart1(sample)); // 6440 console.log("Input", solvePart1(input)); // 251927063 const solvePart2 = (data: string): number => { const cardsWeights = [ "J", "2", "3", "4", "5", "6", "7", "8", "9", "T", "Q", "K", "A", ] as const; const game = new Game(data, cardsWeights, true); return game.lines.sort((a, b) => { const weightA = a.hand.handType?.weight || 0; const weightB = b.hand.handType?.weight || 0; if (weightA > weightB) return -1; if (weightA < weightB) return 1; for (let i = 0; i < 5; i += 1) { const valueA = a.hand.cards[i].weight; const valueB = b.hand.cards[i].weight; if (valueA > valueB) return 1; if (valueA < valueB) return -1; } return 0; }) .reduce((sum, line, index) => { return sum + ((index + 1) * line.bid); }, 0); }; console.log("Sample:", solvePart2(sample)); // 5905 console.log("Input", solvePart2(input)); // 255632664