clavier ouvert

Blog d'enseignement d'Adrien Foucart.
Toutes les opinions présentées ici n'engagent que moi. Blog garanti sans pub, sans traqueurs, et 100% rédigé par un humain.

2026.05.18
Spirales et chevaliers | 3. deux joueurs, et plus?

Dans la première partie, nous avons dessiné une spirale carrée. Dans la seconde, nous avons reproduit le motif de la première visualisation montrée dans la vidéo de Numberphile qui a inspiré ce petit projet. Essayons maintenant de généraliser notre code pour arriver aux formes plus complexes, avec plusieurs couleurs et déplacements.

Plusieurs joueurs

Première chose à faire: que notre générateur de points accepte une liste de joueurs. On va essayer d’avancer pas à pas, en gardant le code fonctionnel à chaque étape, donc on ne change pour l’instant rien au reste du code et on ne traite que le premier joueur de la liste:

# spiral.py
def valid_points_generator(board: Board, max_points: int, players: Iterable[Player]) -> Generator[tuple[int, int, Player], None, None]:
    n_points = 0
    player = players[0]
    # ...

if __name__ == "__main__":
    board = Board()
    player = Player(color='b', offsets=[(1, 2), (-1, 2), (2, 1), (-2, 1), (1, -2), (-1, -2), (2, -1), (-2, -1)])
    show_points(valid_points_generator(board, 1000, [player]))

Détachons ensuite la logique de “trouver le prochain point pour un joueur”, pour pouvoir itérer sur la liste des joueurs dans le générateur. Chaque joueur devra avoir son générateur de prochain point potentiel indépendant: ce n’est pas parce qu’on a passé un point et qu’il n’était pas valable pour le joueur 1 qu’il ne sera pas valable pour les autres.

# spiral.py
def _next_valid_point_for_player(points: Iterable[tuple[int, int]],
                                 board: Board,
                                 player: Player) -> tuple[int, int]:
    for x, y in points:
        if not board.is_available((x, y)): 
            continue
        if board.is_visible_from((x, y), player):
            continue
        return x, y

def valid_points_generator(board: Board, max_points: int, players: Iterable[Player]) -> Generator[tuple[int, int, Player], None, None]:
    n_points = 0
    generators = [(player, position_generator()) for player in players]
    while True:
        for player, generator in generators:
            x, y = _next_valid_point_for_player(generator, board, player)
            board.put((x, y), player)
            n_points += 1
            yield x, y, player
            if n_points >= max_points:
                return

Tout fonctionne toujours, mais le board.is_visible_from((x, y), player) n’est pas juste. Ou plutôt, ce ne sera pas juste dans la version avec plusieurs joueurs, puisqu’il faudra vérifier si la case est vue par l’un des autres joueurs.

On doit donc passer une liste de joueurs à next_valid..., et vérifier si l’on est visible de n’importe lequel d’entre eux:

# spiral.py
def _next_valid_point_for_player(points: Iterable[tuple[int, int]],
                                 board: Board,
                                 players: Iterable[Player]) -> tuple[int, int]:
    for x, y in points:
        if not board.is_available((x, y)): 
            continue
        if any(board.is_visible_from((x, y), player) for player in players):
            continue
        return x, y

Et dans le générateur, on distinguera le cas un ou plusieurs joueurs. Dans le premier cas, le point est valide s’il n’est pas visible par les cases déjà coloriée par le joueur lui-même. Dans le second, s’il n’est pas visible par les cases coloriées par l’un des autres.

# spiral.py
def valid_points_generator(board: Board, max_points: int, players: Iterable[Player]) -> Generator[tuple[int, int, Player], None, None]:
    n_points = 0
    generators = [(player, position_generator()) for player in players]
    while True:
        for player, generator in generators:
            if len(players) == 1:
                x, y = _next_valid_point_for_player(generator, board, player)
            else:
                x, y = _next_valid_point_for_player(generator, board, [p for p in players if p != player])
            board.put((x, y), player)
            n_points += 1
            yield x, y, player
            if n_points >= max_points:
                return

Mettons maintenant deux joueurs:

# spiral.py
if __name__ == "__main__":
    board = Board()
    players = [
        Player(color='r', offsets=[(1, 2), (-1, 2), (2, 1), (-2, 1), (1, -2), (-1, -2), (2, -1), (-2, -1)]),
        Player(color='k', offsets=[(1, 2), (-1, 2), (2, 1), (-2, 1), (1, -2), (-1, -2), (2, -1), (-2, -1)])
    ]
    show_points(valid_points_generator(board, 1000, players))

Et on obtient:

1000 premiers points, deux joueurs, cavalier pour les deux

Ce qui, mis à part une inversion des couleurs, me semble bien correspondre à l’exemple montré dans la vidéo de Numberphile et repris sur le site de l’OEIS.

Un solide point d’arrêt pour cet article, même s’il reste du boulot.

Commentaires, remarques, erreurs qu'il faut absolument me faire remarquer? Contactez-moi sur Mastodon ou par mail (adrien@adfoucart.be)