Advent of Code 2023 Day 07: Camel Cards

Cover Photo

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 de A -> 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 input
  • bid: as apostas daquela mão presentes no input
  • strength: a “força” daquela mão
  • sortCallbackFn: 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:

Métodos Array:

Métodos String: