Advent of Code 2023 Day 07: Camel Cards
Foto de Capa gerada por IA
A viagem com tudo pago da última corrida de barcos foi rápida e direta: apenas 5 minutos em um Airship(ou balão dirigível).
Uma Elfa te recepciona e espera que tenha consigo (supostamente) as “partes” para arrumar o fornecimento de areia e vocês já sobem em um camelo para onde estão as máquinas. Após uma breve explicação sobre os motivos do fornecimento de areia terem pausado, uma conversa sobre máquinas quebradas a Elfa menciona que a viagem vai durar alguns dias.
Para passar o tempo, ela propõe um jogo chamado Camel Cards que se assemelha muito ao Poker porém, mais simples para poder ser jogado andando de camelo.
Contexto específico
O Jogo Camel Cards funciona de forma muito similar ao Poker mas sem contar com os naipes. As regras são:
- Cada mão (
hand
) tem um tipo baseado em quantas cartas se agrupam - Cada carta da mão tem uma força (
strength
) na ordem deA
->2
(2
é o menor valor) - Não existem pontos para sequências (como o straight flush)
Caso uma mão não tenha tenha nenhuma combinação, ela leva o menor tipo (HIGH_CARD
).
As mãos (hands
) são ordenadas primeiro por tipo (type
). Caso duas mãos possuam o mesmo tipo, elas então são ordenadas por maior carta sequencialmente em cada mão na ordem que elas aparecem.
Para encontrar a resposta para o seu desafio, você deve usar o input onde as linhas possuem sequência de mãos (hands
) e apostas (bids
). É preciso multiplicar cada aposta pelo rank
da mão (baseado na pontuação dela) e somar todos os valores de todas as mãos.
Resolução Parte 1
Caso queira resolver antes de ler a respeito de minha solução, esse é o momento!
Para esse desafio percebi que havia bastante lógica relacionada a abstração de uma mão e resolvi criar uma estrutura chamada Hand
(melhor descrita mais a frente) que fornece para cada item:
cards
: as cartas daquela mão presentes no inputbid
: as apostas daquela mão presentes no inputstrength
: a “força” daquela mãosortCallbackFn
: uma função de callback que deve ser usada para ordenar a lista de mãos
Usando essa estrutura, o código para resolver o desafio fica da seguinte forma
const getTotalWinnings = (lines) => {
const handsWithStrengths = getHandsWithStrengths(lines)
const sortedHands = [...handsWithStrengths].sort((handA, handB) =>
handA.sortCallbackFn(handB)
)
const totalWinnings = getTotalWinnings(sortedHands)
return totalWinnings
}
A primeira função auxiliar getHandsWithStrengths()
fornece uma lista de instâncias da estrutura Hand
contendo todas as mãos presentes no input
const getHandsWithStrengths = (lines) => {
const hands = []
for (const line of lines) {
const [cards, bid] = parseLineOfCardsAndBids(line)
const hand = Hand({ cards, bid })
hands.push(hand)
}
return hands
}
Visto que a função sort()
realiza mutações no Array
original, para evitar mutações na lista handsWithStrengths
a mesma foi copiada com o uso do operador spread
fazendo com que as mutações acontecessem nesse Array
duplicado.
Já a função auxiliar getTotalWinnings()
realiza a soma dos valores das mãos, multiplicados usando um reduce()
entre a aposta (bid
) com o rank
de cada mão.
const getTotalWinnings = (hands) =>
hands.reduce((acc, hand, i) => acc + hand.bid * (i + 1), 0)
Estrutura Hand
Falando um pouco sobre a estrutura Hand
, sua API já foi comentada mas a lógica interna para criar cada strength
de forma única envolve uma complexidade que depende de alguns fatores: os tipos de mãos (HAND_TYPES
) e a conversão da contagem de cartas na mão para o tipo da mão COUNT_TO_HAND_TYPE
.
Uma lista ordenada das cartas em sequência foi criada para fornecer a pontuação daquela carta perante as outras (CARDS
). Também para auxiliar, foram criadas algumas validações como isTwoPairs()
e isFullHouse()
para isolar essa lógica. Essas estruturas foram omitidas neste artigo mas podem ser encontradas (algumas com outros nomes após refatorar o código para a parte 2) no arquivo do dia 07 dentro do repositório do GitHub.
const Hand = ({ cards, bid }) => {
const getCardCountsStrength = (cardCounts) => {
const [firstCount, secondCount] = [...cardCounts.values()].sort(
(a, b) => b - a
)
if (isTwoPairs(firstCount, secondCount)) {
return HAND_TYPES.TWO_PAIR.strength
}
if (isFullHouse(firstCount, secondCount)) {
return HAND_TYPES.FULL_HOUSE.strength
}
const type = COUNT_TO_HAND_TYPE[firstCount]
return HAND_TYPES[type].strength
}
const getHandStrength = (cards) => {
// ...
const handStrength = getCardCountsStrength(cardCounts)
return handStrength
}
const strength = getHandStrength(cards)
const sortCallbackFn = (otherHand) => {
// ...
}
return {
cards,
bid,
strength,
sortCallbackFn,
}
}
Partes do código foram omitidas aqui também mas dentre as funções importantes mantidas é posssível notar o uso das estruturas mencionadas anteriormente e funções auxiliares. Essa estrutura é criada utilizando o padrão de Módulo no JavaScript, muito utilizado para criar virtualmente funções e variáveis privadas que existem apenas dentro do escopo do Módulo. É um padrão possível como consequência do conceito de Closures
em JavaScript e é comum a combinação dele com o uso de funções auto-invocadas (IIFE
s). Links para referências estão ao final do artigo!
A solução para o desafio é o valor totalWinnings
encontrado ao final de todas as operações!
E para tornar o jogo um pouco mais divertido a Elfa agora introduz a exisência do Coringa (Joker
), onde ele possui o menor valor de strength
, abaixo do número 2
mas conta como uma carta que melhora o rank
da mão, sendo combinada com qualquer outra carta!
Resolução Parte 2
Novamente, Caso queira resolver a segunda parte antes de ler a respeito de minha solução, interrompa sua leitura aqui mesmo!
Para auxiliar na segunda parte, modifiquei a estrutura Hand
para que ela incluísse uma propriedade withJoker
. Essa propriedade é usada para avaliar aquela mão de formas diferentes, já que a lógica para avaliar a força (strength
) estava interna ao Módulo.
Também foram criadas algumas funções internas novas como buildSortedCardCountsWithoutJoker()
e getCardCountsStrengthWithJoker()
.
const Hand = ({ cards, bid, withJoker = false }) => {
// ...
const buildSortedCardCountsWithoutJoker = (cardCounts) =>
[...cardCounts]
.filter((card) => card[0] !== JOKER)
.sort((a, b) => {
const countResult = b[1] - a[1]
return countResult === 0
? CARDS_WITHOUT_JOKER.indexOf(b[0]) - CARDS_WITHOUT_JOKER.indexOf(a[0])
: countResult
})
const getCardCountsStrengthWithJoker = (cardCounts) => {
if (!cardCounts.has(JOKER)) {
return getCardCountsStrength(cardCounts)
}
const jokerCount = cardCounts.get(JOKER)
if (jokerCount === 5) {
return HAND_TYPES.FIVE.strength
}
// filter JOKER from cardCounts
const sortedCardCounts = buildSortedCardCountsWithoutJoker(cardCounts)
const updatedCardCounts = new Map(sortedCardCounts)
const [firstCardName, firstCardCount] = sortedCardCounts[0]
updatedCardCounts.set(firstCardName, firstCardCount + jokerCount)
return getCardCountsStrength(updatedCardCounts)
}
const getHandStrength = (cards) => {
// ...
const handStrength = withJoker
? getCardCountsStrengthWithJoker(cardCounts)
: getCardCountsStrength(cardCounts)
return handStrength
}
// ...
return {
cards,
bid,
strength,
sortCallbackFn,
}
}
Ao iniciar a função getCardCountsStrengthWithJoker()
é validado se a carta Joker
existe na mão. Caso não exista, basta usar a função já existente.
Caso exista e ela seja repetida 5
vezes, ela terá o strength
respectivo.
Na sequência, é criada uma nova lista sem a presença da carta Joker
e o valor de ocorrências dela é adicionada a carta de maior prioridade, aumentando assim a “força” daquela mão da forma mais otimizada possível, perante o jogo.
E com essas adições de código foi possível calcular o novo valor de totalWinnings
, usando o Joker
.
const solveWithJoker = (lines) => {
const withJoker = true
const handsWithStrengthsAndWithJoker = getHandsWithStrengths(lines, withJoker)
const sortedHandsWithJoker = [...handsWithStrengthsAndWithJoker].sort(
(handA, handB) => handA.sortCallbackFn(handB)
)
const totalWinningsWithJoker = getTotalWinnings(sortedHandsWithJoker)
return totalWinningsWithJoker
}
A resposta para a segunda parte do desafio é o valor final de totalWinningsWithJoker
!
Referências
O código final esta disponível no repositório do GitHub. Esses são alguns links que podem te auxiliar a compreender melhor o código e cada detalhe que mencionei ou esqueci de comentar a respeito de minha solução:
Map
Object- Documentação a respeito de
Closures
- Parágrafo sobre o padrão de Módulo dentro da documentação de
IIFE
Métodos Array:
Métodos String: