Advent of Code 2023 Day 07: Camel Cards
December 7, 2023 • 6 minutes reading time

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 (IIFEs). 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:
MapObject- 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: