• # Moi, aujourd'hui, je modélise tout propre tout joli !

    Posté par (Mastodon) . En réponse au message Advent of Code 2023, day 7. Évalué à 2.

    Surtout que quand j'ai voulu faire mon malin, j'avais une erreur que je n'ai jamais trouvée, mais qui s'est corrigée d'elle-même en remplaçant des chiffres par des trucs lisibles.
    Comme quoi coder propre, contrairement à la rumeur, ben c'est utile même pour soi-même aujourd'hui.

    Voilà, avec ça on corrige des bugs, et on supprime plein de commentaires devenus inutiles aussi :

    from enum import IntEnum
    class HandType(IntEnum):
     HighCard = 1
     OnePair = 2
     TwoPairs = 3
     ThreeOfAKind = 4
     FullHouse = 5
     FourOfAKind = 6
     FiveOfAKind = 7

    Comme ça on peut comparer le HandType entre deux mains, et le plus grand est plus fort.
    L'autre étape c'est de renommer les cartes pour pouvoir comparer deux mains du même type :

    data = sys.stdin.read().strip().splitlines()
    hands1 = (
     Hand(a.translate(str.maketrans("TJQKA", "abcde")), int(b))
     for line in data
     for a, b in [line.split()]
    )

    Et pour l'exercice 2, on initialise les données (hands2) tout pareil sauf qu'on fait : str.maketrans("TJQKA", "a0cde") comme ça le Joker a une valeur plus petite que les autres cartes lors de la comparaison. Et en plus on va pouvoir bosser avec la même classe Hand, et compter les Jokers, donc les "0", et comme il n'y en a pas dans les données transformées pour l'exercice 1, ça va fonctionner pour les deux !

    Maintenant notre classe Hand peut être initialisée avec les données : hand et bid, et on la décore pour pouvoir ordonner directement une liste de mains. Le cached_property permet de s'assurer qu'on ne recalculera jamais deux fois le score d'une main lors de l'exécution (an pratique ça divise le temps d'exécution par 2 environ, ça aurait pu être bien pire...). On utilise une frozen dataclass parce qu'on ne modifie pas une main, là aussi c'est de l'optimisation qui peut s'avérer déterminante en cas de problème vraiment gros.

    from functools import total_ordering, cached_property
    from dataclasses import dataclass
    @dataclass(frozen=True)
    @total_ordering
    class Hand:
     hand: str = ""
     bid: int = 0
     @cached_property
     def score(self):
     return some magic
     def __lt__(self, other):
     return (self.hand < other.hand) if self.score == other.score else (self.score < other.score)
     def __eq__(self, other):
     return False

    Ouaip, on simplifie en disant que deux mains sont toujours différentes, à toutes fins utiles, ça va plus vite, hein ?
    Parce qu'on suppose, vu l'exercice, que toutes les mains fournies sont différentes, sinon le calcul final donnera un résultat différent (sauf si elles ont le même « bid », auquel cas osef).
    De plus cut -d\ -f1 < 07.input | sort -u | wc -l; wc -l 07.input nous retourne 1000 lignes pour chaque, on a donc bien des mains uniques dans les données en entrée.
    En pratique on ne gagne rien, avec la cached_property sur le score, de toute façon on va calculer le score une seule fois par main, et ça coûte pas lourd comme comparaisons. Mais là aussi c'est lié finalement à la toute petite quantité de données.

    Bref, il ne nous reste plus qu'à résoudre, avec la fonction sorted() qui va faire tout le boulot à notre place, magique !

    ex1 = sum((i + 1) * hand.bid for i, hand in enumerate(sorted(hands1)))
    ex2 = sum((i + 1) * hand.bid for i, hand in enumerate(sorted(hands2)))

    Et enfin, voilà le code de la méthode Hand.score, là où on se rend compte que tous les magiciens ont leur « trucs ». Je ne sais pas si j'aurais pu faire plus « intelligent », mais c'est assez clair je pense.

     @cached_property
     def score(self):
     cards = set(self.hand) # all kind of cards in hand
     nb = len(cards) # number of different cards, including jokers
     jokers = self.hand.count("0") # number of jokers in hand
     if nb == 5: # abcde, abcd*
     return HandType.OnePair if jokers else HandType.HighCard
     if nb == 4: # aabcd, aabc*, **abc
     return HandType.ThreeOfAKind if jokers else HandType.OnePair
     if nb == 3: # aaabc, aabbc, aaab*, aabb*, aa**c, ***bc
     cards.discard("0") # Jokers are already counted
     card = cards.pop() # draw a non-joker card
     c = self.hand.count(card)
     if c == 2: # aabbc, aabb*, aa**c
     return [HandType.TwoPairs, HandType.FullHouse, HandType.FourOfAKind][jokers]
     if c == 3: # aaabc, aaab*
     return HandType.FourOfAKind if jokers else HandType.ThreeOfAKind
     # non-joker card U is unique: aaabU, aabbU, aaaU*, aa**U, ***bU
     card = cards.pop()
     if self.hand.count(card) == 2: # aabbU, aa**U
     return HandType.FourOfAKind if jokers else HandType.TwoPairs
     # aaabU, aaaU*, ***bU
     return HandType.FourOfAKind if jokers else HandType.ThreeOfAKind
     if nb == 2: # aaa**, ***aa, aaaa*, ****a
     if jokers: # whatever, jokers makes it Five of a kind
     return HandType.FiveOfAKind
     card = cards.pop()
     if self.hand.count(card) in (2, 3): # aaabb
     return HandType.FullHouse
     # aaaab
     return HandType.FourOfAKind
     return HandType.FiveOfAKind

    Et voilà :)
    Et une fois n'est pas coutume : PyPy est largement moins efficace que Python là-dessus, on passe de 70 à 370 millisecondes, même avec les 150ms de démarrage de PyPy, c'est amplement trois fois moins bon pour l'exécution en elle-même.
    Dans tous les cas : pas de quoi faire chauffer la pièce à coups de CPU.

    • Yth.