🚲 Tanguy Ortolo a écrit 12191 commentaires

  • # Imprimante à réservoir d'encre

    Posté par  (site web personnel) . En réponse au message Imprimante. Évalué à 5.

    Après des années à utiliser des pilotes spécifiques, les fabricants d'imprimantes de sont enfin mis ensemble pour concevoir une, enfin deux normes inspirées de CUPS. Une le la compatibilité avec les appareils Apple et une pour Android. CUPS prend en charge les deux.

    Concrètement, ça veut dire que si tu prends une imprimante qui prend en charge AirPrint ou Mopria, ça marchera juste sans rien avoir à faire. Et c'est le cas de la grande majorité des imprimantes récentes, donc tu es assez libre dans ton choix.

    Maintenant, si par hasard tu veux éviter de contribuer au modèle de financement par le consommable, avec des imprimantes vendues presque à perte et de l'encre vendue plus cher que le Chanel n°5, tu peux choisir une imprimante à réservoir d'encre. Ça coûte plus cher à l'achat, mais l'encre fournie avec correspond à un volume d'impression qui, en cartouches, coûterait déjà plus cher que le prix total de l'imprimante !

  • [^] # Re: casse tête

    Posté par  (site web personnel) . En réponse au journal devoir hacker un train !. Évalué à 3.

    Question bête : n'existe-t-il pas des casses-têtes légaux ? Quand une compagnie vous vend un produit rendu volontairement défectueux, n'a-t-on donc aucune loi qui puisse la faire condamner ?

    Ça s'appelle un vice caché. Dans le cas présent, ce serait reconnu sans aucune difficulté. Avec en prime une mauvaise foi patente de la part du fabricant. Bref, si c'était en France, la SNCF pourrait le contraindre à payer des dommages-intérêts suffisants pour les mettre en faillite.

  • [^] # Re: Pas bien compliqué… en reformulant

    Posté par  (site web personnel) . En réponse au message Advent of Code, jour 13. Évalué à 3.

    Allez, voici le code :

    from collections.abc import Iterable
    from typing import Optional, Self
    
    from enum import Enum
    import io
    
    import aoc
    
    
    Coords = tuple[int, int]
    
    
    class Terrain(Enum):
        ASH = '.'
        ROCK = '#'
    
        def __str__(self) -> str:
            if self is self.ASH:
                return ' '
            if self is self.ROCK:
                return '▒'
            raise ValueError('invalid terrain value')
    
    
    class Pattern:
        def __init__(self, array: Iterable[Iterable[Terrain]]):
            self.matrix = list(list(line) for line in array)
            self.ly = len(self.matrix)
            self.lx = len(self.matrix[0])
    
        @classmethod
        def import_lines(cls, lines: Iterable[str]) -> Self:
            return cls((Terrain(char) for char in line.rstrip()) for line in lines)
    
        def __getitem__(self, coords: Coords) -> Terrain:
            y, x = coords
            return self.matrix[y][x]
    
        def __str__(self) -> str:
            s = io.StringIO()
            for line in self.matrix:
                for terrain in line:
                    s.write(str(terrain))
                s.write('\n')
            return s.getvalue()
    
        def y_reflection_errors(self, y: int) -> int:
            """Count the number of errors in a horizontal reflection between y - 1
            and y. Return 0, 1 or 2 (meaning many)."""
            errors = 0
            for i in range(min(y, self.ly - y)):
                y1 = y - 1 - i
                y2 = y + i
                for x in range(self.lx):
                    errors += self[y1, x] != self[y2, x]
                    if errors >= 2:
                        return errors
            return errors
    
        def x_reflection_errors(self, x: int) -> int:
            """Count the number of errors in a vertical reflection between x - 1
            and x. Return 0, 1 or 2 (meaning many)."""
            errors = 0
            for i in range(min(x, self.lx - x)):
                x1 = x - 1 - i
                x2 = x + i
                for y in range(self.ly):
                    errors += self[y, x1] != self[y, x2]
                    if errors >= 2:
                        return errors
            return errors
    
        def y_reflection1(self) -> Optional[int]:
            for y in range(1, self.ly):
                if self.y_reflection_errors(y) == 0:
                    return y
            return None
    
        def x_reflection1(self) -> Optional[int]:
            for x in range(1, self.lx):
                if self.x_reflection_errors(x) == 0:
                    return x
            return None
    
        def y_reflection2(self) -> Optional[int]:
            for y in range(1, self.ly):
                if self.y_reflection_errors(y) == 1:
                    return y
            return None
    
        def x_reflection2(self) -> Optional[int]:
            for x in range(1, self.lx):
                if self.x_reflection_errors(x) == 1:
                    return x
            return None
    
    
    def part1(lines: aoc.Data) -> int:
        """Solve puzzle part 1: determine the sum of perfect reflection columns and
        100× that of perfect reflection lines."""
        total = 0
        for i, group in enumerate(aoc.group_lines(lines)):
            pattern = Pattern.import_lines(group)
            if (x := pattern.x_reflection1()) is not None:
                total += x
            elif (y := pattern.y_reflection1()) is not None:
                total += 100 * y
            else:
                raise ValueError('pattern without reflection‽')
        return total
    
    
    def part2(lines: aoc.Data) -> int:
        """Solve puzzle part 1: determine the sum of imperfect reflection columns
        and 100× that of imperfect reflection lines."""
        total = 0
        for i, group in enumerate(aoc.group_lines(lines)):
            pattern = Pattern.import_lines(group)
            if (x := pattern.x_reflection2()) is not None:
                total += x
            elif (y := pattern.y_reflection2()) is not None:
                total += 100 * y
            else:
                raise ValueError('pattern without reflection‽')
        return total
  • # Pas bien compliqué… en reformulant

    Posté par  (site web personnel) . En réponse au message Advent of Code, jour 13. Évalué à 3.

    La première partie ne pose aucune difficulté particulière. Pour la seconde, l'énoncé suggère presque une implémentation directe : essayer de changer les points, un par un, et chercher à chaque fois si on trouve une réflexion. Inutile de dire que ça risque de prendre un peu trop de temps.

    En reformulant le problème, on peut trouver une solution plus intelligente : on ne cherche plus une réflexion parfaite mais une réflexion avec exactement une erreur. Voilà, vous avez l'algorithme, je vous laisse. :-)

  • [^] # Re: Rien de vraiment compliqué, il faut juste utiliser tout ce qu'on sait faire.

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023, jour 12. Évalué à 5.

    Ah, je crois que j'ai trouvé ce qui consomme de la mémoire dans ton cache. Et qui doit peut-être pas mal le ralentir aussi. Tu as self parmi les arguments de la fonction sur laquelle le cache travaille.

    Intuitivement, je dirais que pour un maximum d'efficacité, il faut minimiser les arguments d'une fonction sur laquelle on veut appliquer un cache. Quitte à définir une fonction auxiliaire locale et que ce soit celle-là qui soit cachée.

  • [^] # Re: Rien de vraiment compliqué, il faut juste utiliser tout ce qu'on sait faire.

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023, jour 12. Évalué à 4.

    Et donc voici ma solution, en python, et là encore, de façon surprenante, PyPy est quasiment équivalent à CPython, malgré les gros gros calculs !

    Ça ne m'étonne pas trop. Les gros calculs en question sont surtout des appels de fonctions, des branchements, des tests et des additions d'entiers. Je doute que ça soit les points forts de PyPy, si ?

  • [^] # Re: Rien de vraiment compliqué, il faut juste utiliser tout ce qu'on sait faire.

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023, jour 12. Évalué à 4. Dernière modification le 12 décembre 2023 à 14:28.

    Bon, voilà quoi. J'ai utilisé un cache, évidemment.

    Pour info, avec un coefficient de pliage de 10 et en utilisant CPython, j'obtiens une réponse 1 498 094 344 344 361 041 914 985 222 578 (1,5×10³⁰ soit 1,5 millier de milliards de milliards de milliards) en 3,3 secondes et une consommation mémoire de 22 Mio :

    % /usr/bin/time -v bin/12.py -2 < in/12.in 
    Solving part 2:
    1498094344344361041914985222578
        Command being timed: "bin/12.py -2"
        User time (seconds): 2.86
        System time (seconds): 0.41
        Percent of CPU this job got: 99%
        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:03.29
        Average shared text size (kbytes): 0
        Average unshared data size (kbytes): 0
        Average stack size (kbytes): 0
        Average total size (kbytes): 0
        Maximum resident set size (kbytes): 22772
        Average resident set size (kbytes): 0
        Major (requiring I/O) page faults: 0
        Minor (reclaiming a frame) page faults: 111390
        Voluntary context switches: 1
        Involuntary context switches: 305
        Swaps: 0
        File system inputs: 0
        File system outputs: 0
        Socket messages sent: 0
        Socket messages received: 0
        Signals delivered: 0
        Page size (bytes): 4096
        Exit status: 0
    
  • [^] # Re: Rien de vraiment compliqué, il faut juste utiliser tout ce qu'on sait faire.

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023, jour 12. Évalué à 4.

    Et si à chaque branchement :

    • on teste avec une source en bon état.
    • si on a des solutions, alors on sait que l'indice suivant pouvait être décalé d'un cran > vers la gauche, donc on peut multiplier par deux !
    • sinon on teste le chemin avec une source endommagée.

    Ça va beaucoup plus vite, beaucoup beaucoup.
    Mais c'est faux, il va falloir encore plus d'intelligence, de ruse, pour comprendre les effets de bords, pourquoi ça ne fonctionne pas.

    En fait je ne vois pas pourquoi ce serait vrai. Si tu as des solutions avec une source inconnue donnée considérée comme en bon état, il n'y a aucune raison qu'en prenant ces solutions, et en les décalant d'un cran vers la gauche, ça corresponde toujours au schéma de la ligne donnée.

  • [^] # Re: Rien de vraiment compliqué, il faut juste utiliser tout ce qu'on sait faire.

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023, jour 12. Évalué à 4. Dernière modification le 12 décembre 2023 à 13:51.

    Ayé, j'ai résolu la partie 2. Mais je ne suis pas du tout satisfait de ma solution, c'est super efficace mais j'ai l'impression d'avoir triché.

    Je vous explique. Pour la partie 1, j'ai écrit une fonction récursive qui donne le nombre de possibilités d'arrangements restants étant donné des choix déjà faits sur un certain nombre de sources. Elle prend en entrée un état d'avancement, qui indique :

    • l'état de fonctionnement de la dernière source considérée (fonctionnelle ou hors service, pas inconnue, comme on va le voir.) ;
    • la position de la dernière source considérée ;
    • l'index du groupe courant de sources hors service ;
    • le nombre de sources hors service actuellement comptabilisée dans le groupe courant.

    Si on a considéré toutes les sources, ça renvoie directement 1 si le compte est bon (on a atteint le dernier groupe de sources cassées et il est exactement rempli) et 0 sinon.

    Si on n'a pas encore considéré toutes les sources, ça regarde la suivante et ça itère sur ses états possibles (une source en état ne peut être qu'en état, une source cassée ne peut être que cassée et une source en état inconnu peut être les deux, évidemment) :
    * pour un état hors service, si la dernière source considérée était en état, ça ouvre un nouveau groupe… si possible, c'est à dire s'il reste des groupes non considérés, sinon on est dans une impasse et ça continue ;
    * pour un état en état (hmm…), si la dernière source considérée était hors service, ça vérifie si le compte de sources hors service correspond au groupe courant et si ce n'est pas le cas on est dans une impasse et ça continue ;
    * dans les cas où on n'a pas continueé, on appelle la même fonction récursive pour l'étape d'après et on ajoute sa valeur de retour à celle qu'on va renvoyer.

    Ce sera peut-être plus clair avec le code :

    from collections.abc import Iterable, Iterator
    from typing import Optional, Self
    from enum import Enum
    
    class Condition(Enum):
        OPE='.'
        BRK='#'
        UNK='?'
    
        def states(self) -> Iterator[Self]:
            if self is self.OPE:
                yield self
            elif self is self.BRK:
                yield self
            else:
                yield self.OPE  # type: ignore
                yield self.BRK  # type: ignore
    
    class Condition(Enum):
        OPE='.'
        BRK='#'
        UNK='?'
    
        def states(self) -> Iterator[Self]:
            if self is self.OPE:
                yield self
            elif self is self.BRK:
                yield self
            else:
                yield self.OPE  # type: ignore
                yield self.BRK  # type: ignore
    
        def arrangements(self) -> int:
            def aux(condition: Condition, spring: int, group: int, count: int) -> int:
                if spring == len(self.springs) - 1:
                    # Last spring has just been accounted for, time to check:
                    # - we are at last group;
                    # - that group is full.
                    if (group == len(self.groups) - 1
                        and count == self.groups[-1]):
                        return 1
                    return 0
                # Some springs have not been checked yet.
                next_spring = spring + 1
                next_group = group
                next_count = count
                possibilities = 0
                for next_condition in self.springs[next_spring].states():
                    if next_condition is Condition.BRK:
                        if condition is Condition.OPE:
                            # Broken spring after an operational one opens
                            # new group… if possible.
                            if group == len(self.groups) - 1:
                                # Last group has already been checked and closed:
                                # dead end.
                                continue
                            # We can open next group
                            next_group += 1
                            next_count = 0
                        # Regardless of previous spring condition, increase current
                        # group count.
                        next_count += 1
                        if next_count > self.groups[next_group]:
                            # New current group is overfull: dead end.
                            continue
                    if next_condition is Condition.OPE:
                        if condition is Condition.BRK:
                            # Operational spring after a broken one closes current
                            # group, time to check group count.
                            if count != self.groups[group]:
                                # Dead end
                                continue
                    possibilities += aux(next_condition, next_spring, next_group, next_count)
                return possibilities
            return aux(Condition.OPE, -1, -1, 0)

    Bon, pour la partie 2, c'est beaucoup trop long, évidemment. Et donc, faute de trouver une astuce, vu le genre de paramètres de la fonction, j'ai bêtement utilisé un…

  • [^] # Re: Simple et qui rappelle des souvenirs.

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023, jour 11. Évalué à 3.

    C'est… bizarre comme façon de penser je trouve. Non ?

  • [^] # Re: Pas besoin de tout stocker !

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023 : Day 9. Évalué à 3. Dernière modification le 11 décembre 2023 à 17:50.

    Trève de blabla, l'implémentation :

    class History:
        def __init__(self, values: Iterable[int]):
            self.firsts: list[int] = []
            self.lasts: list[int] = []
            current_values = list(values)
            while any(value != 0 for value in current_values):
                self.firsts.append(current_values[0])
                self.lasts.append(current_values[-1])
                next_values: list[int] = []
                for i in range(len(current_values) - 1):
                    next_values.append(current_values[i + 1] - current_values[i])
                current_values = next_values
    
        @classmethod
        def import_line(cls, line: str) -> Self:
            return cls(int(word) for word in line.split())
    
        def prev(self) -> int:
            return sum((-1)**i * value for (i, value) in enumerate(self.firsts))
    
        def next(self) -> int:
            return sum(value for value in self.lasts)
  • # Pas besoin de tout stocker !

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023 : Day 9. Évalué à 3.

    En fait, si on y réfléchit un peu, on s'aperçoit qu'il n'est pas utile de stocker toute la liste, ni toute la liste des dérivées, ni toute la liste des dérivées secondes, etc. Tout ce dont on a besoin, c'est :

    • pendant la phase de calcul des dérivées, la liste des valeurs de la dernière dérivée calculée, pour pouvoir vérifier si elle sont toutes nulles ;
    • pour pouvoir extrapoler, les dernières valeurs – et pour la seconde partie, les premières valeur – de chaque dérivée jusqu'à la dernière significative.

    Par exemple, en cours de calcul :

    10  13  16  21  30  45  68 ← données d'origines
    10                      68 ← données stockées
       3                  23
         0   2   4   6   8
           ?   ?   ?   ?
    

    En fin de calcul :

    10                      68 ← données stockées
       3                  23
         0               8
           2           2
    

    Pour extrapoler à droite, si vous prenez le temps de vérifier ce qui se passe, il suffit en fait de faire la somme de toutes les dernières valeurs.

    Et pour extrapoler à gauche, la somme de toutes les valeurs, en passant en négatif celles d'une ligne sur deux. Ou, pour le dire autrement, la somme du produit des premières valeurs multipliées par moins un élevé à une puissance égale au rang de la ligne d'où vient la valeur courante.

  • # Strike

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023, jour 11. Évalué à 3. Dernière modification le 11 décembre 2023 à 16:37.

    Bon, aujourd'hui nous sommes dans le cas d'un problème avec une solution naïve qui ne passe pas à l'échelle. Mais personnellement, comme l'optimisation de la première partie était quand même assez simple, j'ai fait un strike, la deuxième partie était résolue en changeant simplement une constante.

    import itertools
    
    from collections.abc import Iterable
    from typing import Self
    
    import aoc
    
    
    Coords = tuple[int, int]
    
    
    class Image:
        def __init__(self, galaxies: Iterable[Coords], factor: int):
            ys: set[int] = set()
            xs: set[int] = set()
            self.galaxies: set[Coords] = set()
            for galaxy in galaxies:
                self.galaxies.add(galaxy)
                ys.add(galaxy[0])
                xs.add(galaxy[1])
            # Space expansion factor: 2 everywhere…
            self.yfactor = [factor] * (max(ys) + 1)
            self.xfactor = [factor] * (max(xs) + 1)
            # … except where galaxies were found.
            for y in ys:
                self.yfactor[y] = 1
            for x in xs:
                self.xfactor[x] = 1
    
        def distance(self, g1: Coords, g2: Coords) -> int:
            y1, x1 = g1
            y2, x2 = g2
            ymin, ymax = min(y1, y2), max(y1, y2)
            xmin, xmax = min(x1, x2), max(x1, x2)
            return (sum(self.yfactor[y] for y in range(ymin, ymax))
                    + sum(self.xfactor[x] for x in range(xmin, xmax)))
    
        @classmethod
        def import_lines(cls, lines: Iterable[str], factor: int) -> Self:
            return cls(((y, x)
                        for y, line in enumerate(lines)
                        for x, char in enumerate(line.rstrip())
                        if char == '#'),
                       factor)
    
    
    def part1(lines: aoc.Data) -> int:
        """Solve puzzle part 1: determine total galaxy distances considering an
        expansion factor of 2."""
        image = Image.import_lines(lines, 2)
        total = 0
        for g1, g2 in itertools.combinations(image.galaxies, 2):
            total += image.distance(g1, g2)
        return total
    
    def part2(lines: aoc.Data) -> int:
        """Solve puzzle part 2: determine total galaxy distances considering an
        expansion factor of 1000000."""
        image = Image.import_lines(lines, 1000000)
        total = 0
        for g1, g2 in itertools.combinations(image.galaxies, 2):
            total += image.distance(g1, g2)
        return total
  • # Deuxième partie

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023, jour 11. Évalué à 3.

    Attention, spoiler, ne lisez pas si vous en êtes à la première partie et que vous ne voulez pas vous gâcher le plaisir de devoir peut-être réfléchir à nouveau pour implémenter la deuxième.

    Deuxième partie

    En fait l'espace s'étend plus vite que vous ne l'imaginiez. Les lignes et colonnes qui ne contiennent aucune galaxie ne prennent pas deux fois plus de place, mais un million. Bonne chance et attention à l'OOM killer.

  • [^] # Re: Première partie

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023 : Jour 10. Évalué à 3.

    Et pour la deuxième partie, résolue grâce à l'idée de Pierre :

    class Map:
        def simplify(self) -> None:
            connecteds: npt.NDArray[np.bool_] = np.zeros(self.matrix.shape, dtype=np.bool_)
            currents: set[Coords] = {self.start}
            while len(currents) > 0:
                nexts: set[Coords] = set()
                for current in currents:
                    if connecteds[current]:
                        continue
                    connecteds[current] = True
                    nexts.update(self.neighs(current))
                currents = nexts
            for index, connected in np.ndenumerate(connecteds):
                if not connected:
                    self.matrix[index] = Tile(0)
    
        def area(self) -> int:
            total = 0
            for line in self.matrix:
                inside: bool = False
                for tile in line:
                    if inside and tile is Tile(0):
                        total += 1
                    if Tile.NORTH in tile:
                        inside = not inside
            return total

    Première étape, simplifier la carte en virant tous les tuyaux inutiles. Comme pour la première partie, c'est basé sur un parcours de proche en proche pour identifier les tuyaux connectés au point de départ. Ensuite, on parcours tout, puis on met à zéro les tuiles qui contenaient des tuyaux non connectés.

    En fait, on parcours chaque ligne en gardant en mémoire si on est à l'intérieur de la boucle de tuyaux ou non. Seulement, à l'intérieur ou à l'extérieur, c'est ambigu lorsqu'on est justement sur un tuyau qui fait partie du réseau. Donc, pour être plus précis, on s'intéresse au fait que la zone en haut à gauche de chaque tuile est à l'intérieur ou non. Et vous pouvez faire tous les schémas que vous voulez, la seule chose qui fait changer l'état intérieur/extérieur de la zone en haut à gauche de la tuile suivante, c'est la présente d'un tuyau connecté au nord.

    Quant à l'aire, les tuiles qui y contribuent sont celles qui sont à l'intérieur et qui sont vides (après élimination de tuyaux inutiles). Voilà !

  • # Première partie

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023 : Jour 10. Évalué à 3.

    C'est pour moi l'occasion d'utiliser enum.Flag qui correspond très bien à des trucs qui peuvent avoir une superposition d'états, ici des tuyaux connectés dans différentes directions.

    from collections.abc import Iterable, Sequence
    from typing import Optional, Self
    
    import enum
    import io
    
    import numpy as np
    import numpy.typing as npt
    
    import aoc
    
    
    Coords = tuple[int, int]
    
    
    class Tile(enum.Flag):
        NORTH = enum.auto()
        SOUTH = enum.auto()
        EAST = enum.auto()
        WEST = enum.auto()
    
        def __str__(self) -> str:
            if self is self.__class__(0):
                return ' '
            # if self is self.EAST:
            #     return '╶'
            # if self is self.WEST:
            #     return '╴'
            if self.NORTH in self:  # type: ignore
                if self.EAST in self:  # type: ignore
                    return '╚'
                if self.WEST in self:  # type: ignore
                    return '╝'
                if self.SOUTH in self:  # type: ignore
                    return '║'
                # if self is self.NORTH:
                #     return '╵'
            if self.SOUTH in self:  # type: ignore
                if self.EAST in self:  # type: ignore
                    return '╔'
                if self.WEST in self:  # type: ignore
                    return '╗'
                # if self is self.SOUTH:
                #     return '╷'
            if self.EAST in self:  # type: ignore
                if self.WEST in self:  # type: ignore
                    return '═'
            # if self is self.WEST:
            #     return '╴'
            raise ValueError('unexpected value %s' % repr(self))
    
        def vector(self) -> Coords:
            if self is self.NORTH:
                return (-1, 0)
            if self is self.SOUTH:
                return (1, 0)
            if self is self.EAST:
                return (0, 1)
            if self is self.WEST:
                return (0, -1)
            raise ValueError('cannot convert multiple directions to vector')
    
        @classmethod
        def import_char(cls, char: str) -> Self:
            if char == '.' or char == ' ':
                return cls(0)
            if char == '|':
                return cls.NORTH | cls.SOUTH  # type: ignore
            if char == '-':
                return cls.EAST | cls.WEST  # type: ignore
            if char == 'L':
                return cls.NORTH | cls.EAST  # type: ignore
            if char == 'J':
                return cls.NORTH | cls.WEST  # type: ignore
            if char == '7':
                return cls.SOUTH | cls.WEST  # type: ignore
            if char == 'F':
                return cls.SOUTH | cls.EAST  # type: ignore
            raise ValueError('unsupported pipe description %s' % char)

    Et la carte maintenant :

    class Map:
        def __init__(self, array: Sequence[Sequence[Tile]], start: Coords):
            self.matrix: npt.NDArray[np.object_] = np.array(array)
            self.ly, self.lx = self.matrix.shape
            self.start = start
    
        @classmethod
        def import_lines(cls, lines: Iterable[str]) -> Self:
            start: Optional[Coords] = None
    
            array: list[list[Tile]] = []
            for y, line in enumerate(lines):
                array.append([])
                for x, char in enumerate(line.rstrip()):
                    if char == 'S':
                        start = (y, x)
                        array[-1].append(Tile(0))
                    else:
                        array[-1].append(Tile.import_char(char))
    
            map_ = cls(array, (0, 0))
            if start is not None:
                y, x = start
                connections = Tile(0)
                if y >= 1 and Tile.SOUTH in map_[y - 1, x]:
                    connections |= Tile.NORTH
                if y < map_.ly - 1 and Tile.NORTH in map_[y + 1, x]:
                    connections |= Tile.SOUTH
                if x >= 1 and Tile.EAST in map_[y, x - 1]:
                    connections |= Tile.WEST
                if x < map_.lx - 1 and Tile.WEST in map_[y, x + 1]:
                    connections |= Tile.EAST
                map_[y, x] = connections
                map_.start = start
                return map_
            else:
                raise ValueError('start position not found')
    
        def neighs(self, coords: Coords) -> Iterable[Coords]:
            y, x = coords
            for direction in self[coords]:
                dy, dx = direction.vector()
                y_, x_ = y + dy, x + dx
                if y_ >= 0 and y_ < self.ly and x_ >= 0 and x_ < self.lx:
                    yield y_, x_
    
        def __getitem__(self, coords: Coords) -> Tile:
            return self.matrix[coords]
    
        def __setitem__(self, coords: Coords, value: Tile):
            self.matrix[coords] = value
    
        def __str__(self):
            s = io.StringIO()
            for line in self.matrix:
                for tile in line:
                    s.write(str(tile))
                s.write('\n')
            return s.getvalue()
    
        def farthest(self) -> int:
            distances: npt.NDArray[np.int_] = np.full(self.matrix.shape, -1, dtype=np.int_)
            distance = 0
            currents: set[Coords] = {self.start}
            while len(currents) > 0:
                nexts: set[Coords] = set()
                for current in currents:
                    if distances[current] >= 0:
                        continue
                    distances[current] = distance
                    nexts.update(self.neighs(current))
                currents = nexts
                distance += 1
            return distances.max()

    La solution est donnée par la méthode Map.farthest(). C'est un parcours itératif de proche en proche :

    1. on construit une matrice de distances au point de départ, initialisée avec des -1 partout ;
    2. on commence avec une distance courante de zéro et le point de départ comme unique point courant, mais plus tard on en aura plusieurs (en pratique, deux, mais ça pourrait être plus si on avait des tuyaux trifides) ;
    3. pour chaque point courant si aucune distance n'a été précédemment relevée, on note la distance courante et on place ses voisins reliés dans l'ensemble des prochains points courants ;
    4. on continue, et on ne s'arrête que quand il n'y a plus aucun point courant.
  • [^] # Re: MicroG ?

    Posté par  (site web personnel) . En réponse au message Signature électronique qualifiée. Évalué à 4.

    Ah, et une petite précision, les services Google Play, c'est un ensemble de logiciels propriétaires, de démons vraisemblablement, qui implémentent des trucs pas présents dans Android Open Source. Et pas mal de logiciels propriétaires pour Android, et même quelques logiciels libres, dépendend de ses fonctionnalités. :-(

    D'où, donc, le besoin, lorsqu'on veut utiliser un système d'exploitation plus ou moins libre genre LineageOS, d'installer soit les services Google Play, soit microG.

  • [^] # Re: MicroG ?

    Posté par  (site web personnel) . En réponse au message Signature électronique qualifiée. Évalué à 5.

    Oui alors comme je disais, c'est plus compliqué que ça. microG est là pour remplacer les services Google Play. Attention, j'ai bien dit les services Google Play, ce qui n'est pas Google Play Store.

    Bref. Ça implique deux choses :

    • ne pas avoir déjà les services Google Play qui tournent : quand on installe LineageOS sans installer en plus les applications Google, on est dans ce cas-là ;
    • que microG puisse se faire passer pour les services Google Play.

    Et c'est ce dernier point qui complique tout, parce que les logiciels pour Android ont un genre de signature qui prouve leur identité. Je ne connais pas bien le sujet, mais en tout cas, pour que microG puisse se faire passer pour les services Google Play, il faut que le système d'exploitation accepte de faire croire qu'il a la signature de ces derniers. Par conséquent, il faut un système d'exploitation qui prenne en charge la falsification de signature (en fait c'est plutôt une falsification de vérification de signature je pense).

    Donc, pour que microG fonctionne, inutile d'essayer simplement de l'installer depuis F-Droid, ça ne marchera pas. Deux possibilités :

    • soit installer un système d'exploitation qui prend directement ça en charge, et qui intègre déjà microG tant qu'à faire : c'est le cas de LineageOS for microG, une variante de LineageOS qui ajoute précisément cela ;
    • soit être root et installer des trucs qui permettront cette falsification de signature.
  • [^] # Re: MicroG ?

    Posté par  (site web personnel) . En réponse au message Signature électronique qualifiée. Évalué à 8.

    C'est un petit peu plus compliqué que ça. microG, c'est une implémentation libre des services Google. Installer des logiciels depuis Google Play sans compte Google ne fait pas partie de ses fonctionnalités.

    Installer des logiciels du Google Play Store sans Google Play, ça se fait avec Aurora Store, un logiciel disponible dans F-Droid. Rien à voir avec microG à priori.

    Seulement, le problème, c'est que l'Identité Numérique La Poste inclut un piège, à son lancement, qui fait en gros :

    1. récupérer le nom du logiciel avec lequel j'ai été installé ;
    2. si ce logiciel n'est pas « Google Play » :
      1. afficher « Il faut m'installer avec Google Play, connard. D'accord ? »
      2. se terminer.

    Du coup, pour pouvoir l'utiliser, il faut soit l'installer avec Google Play, soit trouver un truc qui ne déclenche pas ce piège. Par exemple, j'ai trouvé qu'en l'installant avec Aurora Store en tant que root, ça empêche L'Identité Numérique La Poste de déterminer comment elle a été installée (ça doit récupérer un truc genre chaîne vide), ce qui suffit à ne pas le déclencher. Seulement pour ça, il faut être root sur son téléphone. Et ça, l'Identité Numérique La Poste n'aime pas non plus, il y a aussi un piège pour ça. Du coup, il faut un moyen de cacher qu'on est root. On atteint des niveaux de bidouille assez hauts.

  • [^] # Re: Tu n'es pas seul

    Posté par  (site web personnel) . En réponse au message Signature électronique qualifiée. Évalué à 5.

    Autant que je sache, le seul moyen consiste à bidouiller franchement :

    https://linuxfr.org/users/elessar/journaux/utiliser-l-identite-numerique-la-poste-sans-google-play

  • # Partie 2

    Posté par  (site web personnel) . En réponse au journal Advent of Code 2023, day 8. Évalué à 5.

    Juste un mot sur la partie 2. En force brute évidemment, ça prendrait des mois. Il y a une astuce évidemment, seulement avec l'énoncé seul, je ne crois pas qu'il y ait moyen de soupçonner de quoi il s'agit.

    En fait l'astuce ne vient pas des règles du jeu, mais des données elles-mêmes, qui sont conçues pour que les fantômes tournent en rond. Je ne vous en dit pas plus.

  • # Tu n'es pas seul

    Posté par  (site web personnel) . En réponse au message Signature électronique qualifiée. Évalué à 4.

  • [^] # Re: Besoin d'énumérations ordonnées

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023, day 7. Évalué à 3.

    Une énumération n'est pas nativement utilisable dans des ensembles ou en clef de dictionnaire ???

  • [^] # Re: Besoin d'énumérations ordonnées

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023, day 7. Évalué à 3.

  • # Besoin d'énumérations ordonnées

    Posté par  (site web personnel) . En réponse au message Advent of Code 2023, day 7. Évalué à 3.

    Comme j'aime bien modéliser, c'est un sujet où j'utilise naturellement des énumérations. Pour les cartes, comme pour les types de mains.

    class Card(Enum):
        ONE   = '1'
        TWO   = '2'
        THREE = '3'
        
        TEN   = 'T'
        JACK  = 'J'
        QUEEN = 'Q'
        KING  = 'K'

    L'avantage en donnant aux cartes la valeur correspondant au caractère qui les représente, c'est que ça permet de les parser très facilement :

    card = Card('A')

    Le problème, c'est que je veux comparer les cartes. Et des énumérations, eh bien ça ne se compare pas. Pas sans implémenter au moins une fonction de comparaison. Et ça, c'est… pénible.

    Je rêve donc d'une solution propre pour définir une classé énumérée dont les membres se comparent, simplement avec leur ordre de définition.