Doc détaillée¶
Objets de base, lecture/écriture simple¶
Un Board est un tableau à deux dimensions contenant des objets Tile.
La taille (largeur, hauteur) est à spécifier à la création.
>>> from aboard import Board
>>> board = Board(7, 4)
- coordonnées (0, 0) : Tile en haut à gauche,
- coordonnées (1, 0) : Tile juste à droite,
- etc.
- coordonnées (largeur-1, hauteur-1) : dernière Tile tout en bas à droite.
Une Tile possède les attributs suivants :
- board_owner : référence vers l’objet Board contenant la Tile.
- x, y : position de la Tile dans le Board conteneur.
- pos : position, sous forme d’un objet Pos.
- data : string. Donnée quelconque. Initialisée au caractère « . » (un point).
board_owner
, x
, y
et pos
peuvent être lus, mais ne devraient jamais être directement modifiés.
data
peut être lu et modifié.
Pour accéder à une Tile dans un Board, utilisez l’opérateur « [ ] » __getitem__
, en spécifiant les coordonnées x et y.
>>> tile = board[3, 2]
>>> tile.x
3
>>> tile.y
2
>>> tile.data
'.'
L’opérateur « [ ] » accepte également les tuples board[(3, 2)]
, ainsi que les objets Pos
.
La fonction Board.render()
renvoie une string multi-ligne (séparateur : « \n »), représentant le Board.
Dans la configuration de rendering par défaut, chaque Tile est représentée par un seul caractère, égal au premier caractère de l’attribut data.
>>> tile.data = "ABCD"
>>> print(board.render())
.......
.......
...A...
.......
Itérateurs¶
Itérateurs par rectangle¶
Avec l’opérateur « [ ] », remplacez l’une ou les deux coordonnées par un slice pour faire une itération sur une ligne, une colonne, un sous-rectangle, avec une ligne sur deux, de gauche à droite, …
Exemple :
>>> board = Board(6, 4)
>>> for tile in board[1:4, 1]:
... tile.data = "#"
>>> print(board.render())
......
.###..
......
......
>>> for index, tile in enumerate(board[4::-1, ::-2]):
... tile.data = index
>>> print(board.render())
......
98765.
......
43210.
Pour itérer en premier sur les colonnes, puis sur les lignes, ajouter le caractère « y » en troisième paramètre.
>>> for index, tile in enumerate(board[:, :, 'y']):
... tile.data = index if index < 10 else "."
>>> print(board.render())
048...
159...
26....
37....
Les slices renvoient un itérable, mais pas un indexable. Les éléments ne sont donc pas directement accessible. board[1, :][2]
ne fonctionne pas. Mais l’itérable peut être déroulé dans une liste ou un tuple.
>>> board[2, :]
<positions_iterator.BoardIteratorRect object at 0x00BA6590>
>>> list(board[2, :])
[<Tile (2, 0): 8>, <Tile (2, 1): 9>, <Tile (2, 2): .>, <Tile (2, 3): .>]
Le second paramètre peut être omis, il sera remplacé par un slice complet. Dans l’exemple précédent, on aurait pu remplacer board[2, :]
par board[2]
. Cela permet de récupérer directement une colonne.
Le board en lui-même peut-être itéré, pour parcourir toutes les tiles, de haut en bas et de gauche à droite : for tile in board:print(tile)
.
Itérateurs par liste de positions¶
Pour récupérer des Tiles à partir de positions, il suffit d’itérer à partir d’une liste de coordonnées :
for coord in [(0, 0), (2, 0), (3, 1)]: current_tile = board[coord]
.
La fonction Board.iter_positions
permet la même chose, mais en itérant directement sur les Tiles. (Voir Indicateurs d’itérations).
Indicateurs d’itérations¶
Les itérateurs de board possèdent des indicateurs mis à jour automatiquement :
- current_pos : position courante.
- prev_pos : position précédente (vaut None à la première itération).
- jumped : vaut True si la position précédente et la position courante ne sont pas adjacentes.
- changed_direction : vaut True si la direction de déplacement a changé lors de l’itération qui vient d’être effectuée.
- both_coord_changed : vaut True si les deux coordonnées x et y de la position précédente et de la position courante sont différentes.
Pour les itérateurs par rectangle, l’indicateur both_coord_changed
permet de savoir si on vient de changer de ligne.
>>> iter_board = board[:3, :]
>>> for tile in iter_board:
... print(
... "pos:", tile.x, tile.y,
... "newline: ", iter_board.both_coord_changed
... )
pos: 0 0 newline: True
pos: 1 0 newline: False
pos: 2 0 newline: False
pos: 0 1 newline: True
pos: 1 1 newline: False
pos: 2 1 newline: False
pos: 0 2 newline: True
pos: 1 2 newline: False
pos: 2 2 newline: False
pos: 0 3 newline: True
pos: 1 3 newline: False
pos: 2 3 newline: False
>>> positions = [ (0, 0), (1, 0), (2, 0), (4, 0), (4, 1), (3, 3) ]
>>> iter_pos = board.iter_positions(positions)
>>> for tile in iter_pos:
... print(
... "pos:", tile.pos,
... "prev:", iter_pos.prev_pos,
... "indics:",
... "jumped" * iter_pos.jumped,
... "changed_dir" * iter_pos.changed_direction,
... "both_changed" * iter_pos.both_coord_changed
... )
pos: <Pos 0, 0 > prev: None indics: jumped both_changed
pos: <Pos 1, 0 > prev: <Pos 0, 0 > indics:
pos: <Pos 2, 0 > prev: <Pos 1, 0 > indics:
pos: <Pos 4, 0 > prev: <Pos 2, 0 > indics: jumped
pos: <Pos 4, 1 > prev: <Pos 4, 0 > indics: changed_dir
pos: <Pos 3, 3 > prev: <Pos 4, 1 > indics: jumped changed_dir both_changed
Sur_iterators¶
Les sur-itérateurs s’ajoutent après un itérateur de board.
tell_indicators
¶
Il permet de renvoyer directement des indicateurs, durant l’itération.
Les types d’indicateurs renvoyés doivent être spécifiés via des valeurs ItInd.xxx
.
>>> from aboard import ItInd
>>> indics = (ItInd.PREV_POS, ItInd.JUMPED)
>>> bo_iter = board.iter_positions(positions).tell_indicators(indics)
>>> for prev_pos, jumped, tile in bo_iter:
... print(
... "pos:", tile.pos,
... "prev:", prev_pos,
... "jumped:", jumped,
... )
pos: <Pos 0, 0 > prev: None jumped: True
pos: <Pos 1, 0 > prev: <Pos 0, 0 > jumped: False
pos: <Pos 2, 0 > prev: <Pos 1, 0 > jumped: False
pos: <Pos 4, 0 > prev: <Pos 2, 0 > jumped: True
pos: <Pos 4, 1 > prev: <Pos 4, 0 > jumped: False
pos: <Pos 3, 3 > prev: <Pos 4, 1 > jumped: True
group_by
¶
Il permet de renvoyer les tiles par groupe, selon une fonction de groupement à spécifier.
La fonction de groupement a pour paramètre l’itérateur, elle doit renvoyer un booléen. Chaque fois qu’elle renvoie True, le sur-itérateur renvoie le groupe de tile accumulées.
>>> every_4_of_line = lambda iterator: (iterator.current_pos.x % 4) == 0
>>> for tile_group in board[:].group_by(every_4_of_line):
... print([(tile.x, tile.y) for tile in tile_group])
[(0, 0), (1, 0), (2, 0), (3, 0)]
[(4, 0), (5, 0)]
[(0, 1), (1, 1), (2, 1), (3, 1)]
[(4, 1), (5, 1)]
[(0, 2), (1, 2), (2, 2), (3, 2)]
[(4, 2), (5, 2)]
[(0, 3), (1, 3), (2, 3), (3, 3)]
[(4, 3), (5, 3)]
group_by_subcoord
¶
Sur-itérateur de type group_by
, dont la fonction de groupement se base sur both_coord_changed
. Il permet de récupérer les tiles par groupe de lignes ou de colonnes, à partir d’un itérateur par rectangle.
>>> for tile_group_column in board[:, :, 'y'].group_by_subcoord():
... print(tile_group_column)
[<Tile (0, 0): 0>, <Tile (0, 1): 1>, <Tile (0, 2): 2>, <Tile (0, 3): 3>]
[<Tile (1, 0): 4>, <Tile (1, 1): 5>, <Tile (1, 2): 6>, <Tile (1, 3): 7>]
[<Tile (2, 0): 8>, <Tile (2, 1): 9>, <Tile (2, 2): .>, <Tile (2, 3): .>]
[<Tile (3, 0): .>, <Tile (3, 1): .>, <Tile (3, 2): .>, <Tile (3, 3): .>]
[<Tile (4, 0): .>, <Tile (4, 1): .>, <Tile (4, 2): .>, <Tile (4, 3): .>]
[<Tile (5, 0): .>, <Tile (5, 1): .>, <Tile (5, 2): .>, <Tile (5, 3): .>]
Il n’est pas possible d’enchaîner les sur-itérateurs. board[:].tell_indicators(indics).group_by(grouping)
ne fonctionne pas. (Peut-être dans une prochaine version).
Il faut explicitement convertir le board en itérateur pour pouvoir y ajouter un sur-itérateur. board.group_by_subcoord()
ne fonctionne pas. Mais board[:].group_by_subcoord()
fonctionne.
Héritage de la classe Tile¶
Il est possible de créer un board contenant des tiles dont la classe est héritée de la classe de base Tile.
>>> from aboard import Tile
>>> class MyTile(Tile):
... pass
>>> board_with_my_tiles = Board(6, 4, class_tile=MyTile)
Les classes héritées peuvent utiliser d’autres attributs de données, en plus de tile.data.
Il est conseillé d’overrider les fonctions __str__
et __repr__
. Les versions de base affichent uniquement tile.data.
La fonction __eq__
peut être overridée. Elle devrait l’être si on utilise la classe IteratorGetDifferences
(qui n’est pas encore documentée ici).
La fonction __eq__
est supposée comparer uniquement les données de la Tile, et non pas sa position. C’est à dire Tile.data
et les autres variables membres ajoutées, mais pas Tile.pos
, Tile.x
, Tile.y
.
Fonction Tile.render
¶
Cette fonction peut être overridée. Elle est censée renvoyer une string ou une liste de string, qui est ensuite transmise à la fonction board.render
.
Par défaut, chaque tile est affichée sur un seul caractère. Même si Tile.render
en renvoie plus, seul le premier sera utilisé. Ce comportement est modifiable via la configuration des renderers (voir Objet BoardRenderer).
Lorsque la fonction tile.render
est appelée, deux paramètres w
et h
lui sont indiqués, représentant la taille du rectangle de rendu. La fonction est alors censée renvoyer une liste de h
éléments, chacun d’eux devant être une string de w
caractères.
Si ce n’est pas exactement cette structure de donnée qui est renvoyée, elle sera remise en forme. Le renderer coupe des éléments de la liste et des caractères, et/ou ajoute des espaces et des strings.
Objet BoardRenderer¶
Utilisation¶
Il s’agit d’un objet utilisant les données d’un Board, pour générer une string de rendu.
Tous les objets Board possèdent en variable membre un objet BoardRenderer par défaut, qui est utilisé lors de l’appel à Board.render()
.
Il est possible de créer un autre BoardRenderer doté d’une configuration spécifique.
>>> from aboard import BoardRenderer
>>> board = Board(4, 3)
>>> board[1, 1].data = ("ABZZ", "CDZZ", "XXZZ")
>>> my_renderer = BoardRenderer(
... tile_w=2, tile_h=2,
... chr_fill_tile='_',
... tile_padding_w=1, tile_padding_h=0)
>>> print(board.render(renderer=my_renderer))
._ ._ ._ ._
__ __ __ __
._ AB ._ ._
__ CD __ __
._ ._ ._ ._
__ __ __ __
Le renderer par défaut d’un Board peut être défini lors de l’instanciation du Board.
>>> board = Board(4, 3, default_renderer=my_renderer)
Paramètres du BoardRenderer¶
Ils sont à indiquer à son instanciation. Ils ont tous une valeur par défaut, correspondant à celle du renderer par défaut inclus dans chaque Board.
- tile_w, tile_h : largeur et hauteur des tiles. Par défaut : 1.
- chr_fill_tile : caractère utilisé pour compléter les rectangles des Tiles, lorsque la fonction
Tile.render
ne renvoie pas suffisamment de caractères. Par défaut : “ “ (espace).- tile_padding_w, tile_padding_h : nombre de caractère d’espacement (width et height) entre chaque Tile. Par défaut : 0.
- chr_fill_tile_padding : caractère utilisé pour écrire les paddings. Par défaut : “ “ (espace).
Règle d’adjacence¶
La règle d’adjacence a pour but d’indiquer, pour deux Tiles d’un même Board, si elles sont adjacentes ou non.
Elle est utilisée dans les fonctions de pathfinding, de remplissage par propagation et pour les indicateurs d’itération (indicateur « jumped »).
Sélection de la règle¶
Un board possède un objet AdjacencyEvaluator
, définissant sa règle d’adjacence. Par défaut, il s’agit de AdjacencyEvaluatorCross
, qui considère que deux tiles sont adjacentes si elles sont côte à côte, sur la même ligne ou la même colonne, mais pas en diagonale.
Pour utiliser une autre règle d’adjacence, il faut la spécifier lors de la création du board.
>>> from adjacency import AdjacencyEvaluatorCrossDiag
>>> board_adj_diag = Board(
... 4, 3,
... class_adjacency=AdjacencyEvaluatorCrossDiag
... )
La classe AdjacencyEvaluatorCrossDiag
considère que deux tiles sont adjacentes si elles sont côte à côte ou en diagonale.
>>> print(list(board.get_by_pathfinding((0, 1), (1, 2))))
['<Tile (0, 1): .>', '<Tile (1, 1): .>', '<Tile (1, 2): .>']
>>> print(list(board_adj_diag.get_by_pathfinding((0, 1), (1, 2))))
['<Tile (0, 1): .>', '<Tile (1, 2): .>']
Il est également possible de redéfinir l’adjacence par défaut, qui sera utilisée lors de la création de tous les prochains Boards.
>>> from aboard import set_default_adjacency, AdjacencyEvaluatorCrossDiag
>>> set_default_adjacency(AdjacencyEvaluatorCrossDiag)
Création d’une règle d’adjacence customisée¶
Pour créer une autre règle d’adjacence, il faut hériter la classe AdjacencyEvaluator
et surcharger deux fonctions :
is_adjacent(self, pos_1, pos_2)
: renvoie un booléen, indiquant si les deux positions passées en paramètre sont adjacentes.adjacent_positions(self, pos)
: renvoie un itérateur listant toutes les positions adjacentes à celle passée en paramètre.
La classe héritée possède un paramètre board
, correspondant au Board d’appartenance, sur lequel la règle d’adjacence doit s’appliquer.
Exemple de création d’une règle d’adjacence « torique ». Cette règle considère que lorsqu’on sort du plateau, on est téléporté de l’autre côté. Les tiles du bord droit sont adjacentes avec celles du bord gauche, celles du bord inférieur sont adjacentes avec celles du bord supérieur.
>>> from aboard import Pos, AdjacencyEvaluator
>>> class AdjacencyEvaluatorCrossTore(AdjacencyEvaluator):
... def is_adjacent(self, pos_1, pos_2):
... if pos_1.x == pos_2.x:
... if (pos_1.y + 1) % self.board.h == pos_2.y:return True
... if (pos_2.y + 1) % self.board.h == pos_1.y:return True
... if pos_1.y == pos_2.y:
... if (pos_1.x + 1) % self.board.w == pos_2.x:return True
... if (pos_2.x + 1) % self.board.w == pos_1.x:return True
... return False
... def adjacent_positions(self, pos):
... offsets = [ (0, -1), (+1, 0), (0, +1), (-1, 0) ]
... for offset_x, offset_y in offsets:
... x = (pos.x + offset_x + self.board.w) % self.board.w
... y = (pos.y + offset_y + self.board.h) % self.board.h
... yield Pos(x, y)
>>> board_adj_tore = Board(
... 11, 3,
... class_adjacency=AdjacencyEvaluatorCrossTore
... )
>>> for tile in board_adj_tore.get_by_pathfinding((2, 1), (9, 1)):
... tile.data = 'X'
>>> print(board_adj_tore.render())
...........
XXX......XX
...........
Avec cette règle, le chemin le plus court pour aller de (2, 1) à (9, 1) n’est pas un déplacement vers la droite, mais vers la gauche. On est téléporté du côté gauche vers le côté droit.
Fonction de remplissage par propagation¶
La fonction Board.get_by_propagation
effectue une itération à partir d’une tile initiale, et se propage petit à petit vers les tiles adjacentes qui remplissent la « condition de propagation » (paramètre propag_condition
).
Par défaut, cette condition est vraie si data == '.'
pour la tile de destination.
La condition de propagation est une fonction avec deux paramètres : tile_source
(la tile de départ actuelle), tile_dest
(la tile vers laquelle on tente de se propager). Elle doit renvoyer un booléen indiquant si la propagation est possible ou non.
>>> def go_to_rightmost_column(tile_source, tile_dest):
... if tile_dest.x > tile_source.x:return True
... if tile_dest.x == tile_dest.board_owner.w-1:return True
... return False
>>> from aboard import AdjacencyEvaluatorCross
>>> set_default_adjacency(AdjacencyEvaluatorCross)
>>> board = Board(6, 5)
>>> for tile in board.get_by_propagation((1, 2), go_to_rightmost_column):
... tile.data = 'X'
>>> print(board.render())
.....X
.....X
.XXXXX
.....X
.....X
La propagation utilise la règle d’adjacence du board. L’ordre d’itération dépend de l’ordre des tiles renvoyées par la fonction adjacent_positions
.
>>> board = Board(6, 5)
>>> for index, tile in enumerate(
... board.get_by_propagation((1, 2), go_to_rightmost_column)
... ):
... tile.data = index
>>> print(board.render())
.....7
.....5
.01234
.....6
.....8
La règle d’adjacence du board a des conséquences sur la propagation.
>>> board = Board(6, 5, class_adjacency=AdjacencyEvaluatorCrossDiag)
>>> for tile in board.get_by_propagation((1, 2), go_to_rightmost_column):
... tile.data = 'X'
>>> print(board.render())
...XXX
..XXXX
.XXXXX
..XXXX
...XXX
L’itérateur par propagation possède un indicateur spécifique : PROPAG_DIST
, indiquant la distance parcourue depuis la tile initiale jusqu’à la case courante.
>>> board = Board(6, 5)
>>> board_it = board.get_by_propagation((1, 2), go_to_rightmost_column)
>>> for dist, tile in board_it.tell_indicators((ItInd.PROPAG_DIST, )):
... tile.data = dist
>>> print(board.render())
.....6
.....5
.01234
.....5
.....6
Path-finding¶
La fonction Board.get_by_pathfinding
recherche un chemin le plus court entre deux positions, et effectue une itération dessus, à partir de la tile de départ vers la tile d’arrivée.
Cette fonction utilise une « condition de déplacement », similaire à la condition de propagation. Par défaut, le déplacement est possible si la data
de la tile de destination vaut “.”. Il est possible de la redéfinir via le paramètre pass_through_condition
.
Le path-finding utilise la règle d’adjacence du board.
Lorsqu’il existe plusieurs possibilités de chemin le plus court, c’est le premier trouvé qui est sélectionné. Cette sélection dépend de l’ordre des tiles renvoyées par la fonction adjacent_positions
.
La fonction pass_through_condition
fonctionne de la même manière que propag_condition
. Elle possède deux paramètres : tile_source
(la tile de départ actuelle), tile_dest
(la tile vers laquelle on tente de se déplacer), et doit renvoyer un booléen, indiquant si le déplacement est possible ou non.
Le path-finding déclenche une exception ValueError
s’il n’existe aucun chemin possible.
>>> board = Board(9, 7)
>>> for tile in board[2:7, 2]:
... tile.data = '#'
>>> for tile in board[2, 3:6]:
... tile.data = '#'
>>> for tile in board[6, 3:6]:
... tile.data = '>'
>>> for tile in board[2:7, 5]:
... tile.data = '#'
>>> print(board.render())
.........
.........
..#####..
..#...>..
..#...>..
..#####..
.........
>>> for tile in board.get_by_pathfinding((3, 4), (0, 0)):
... print(tile)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/path/to/git/aboard/code/propagation_iterator.py",
line 121, in __iter__
raise ValueError("Impossible de trouver un chemin")
ValueError: Impossible de trouver un chemin
>>> def my_pass_through_condition(tile_source, tile_dest):
... tile_datas = (tile_source.data, tile_dest.data)
... if tile_datas == ('.', '.'):
... return True
... if tile_datas in (('.', '>'), ('>', '.')):
... return tile_source.x <= tile_dest.x
... return False
...
>>> for tile in board.get_by_pathfinding(
... (3, 4), (0, 0),
... my_pass_through_condition
... ):
... if tile.data != '>':
... tile.data = '*'
>>> print(board.render())
********.
.......*.
..#####*.
..#...>*.
..#***>*.
..#####..
.........
Le chemin aurait été un peu différent avec une règle d’adjacence autorisant les diagonales.
Échanges et permutations circulaires de tiles¶
Chaque case d’un Board ne doit contenir rien d’autre qu’une Tile (pas de None, pas de liste de Tile, etc.). Les Tiles ne sont pas supposées se déplacer dans le Board. Pour représenter des déplacement d’éléments, il faut modifier la variable tile.data
, ou utiliser des MobileItems
(voir Mobile Items (en construction)).
Cependant, comme les MobileItem
ne sont pas terminés, la fonction board.replace_tile
a été ajoutée. Elle permet de remplacer la tile d’un board par une autre tile créée en-dehors du board.
>>> board = Board(3, 2)
>>> new_t = Tile()
>>> new_t.data = 'A'
>>> print(new_t)
<Tile (None, None): A>
>>> board.replace_tile(new_t, Pos(0, 1))
>>> print(board.render())
...
A..
>>> print(new_t)
<Tile (0, 1): A>
Il est conseillé de ne pas remplacer manuellement les tiles, mais d’utiliser cette fonction, car elle met à jour automatiquement les variables tile.pos
, tile.x
et tile.y
.
La fonction board.circular_permute_tiles
permet de déplacer plusieurs tiles d’un même board en une seule opération de permutation circulaire.
>>> board = Board(6, 3)
>>> for index, tile in enumerate(board[:, 1]):
... tile.data = index
>>> print(board.render())
......
012345
......
>>> positions = [ Pos(tile) for tile in board[1:5, 1] ]
>>> board.circular_permute_tiles(positions)
>>> print(board.render())
......
023415
......
build pour codingame¶
La librairie aboard est compilée vers un fichier de code unique : code/builder/aboard_standalone.py
, permettant d’être utilisé des contextes spécifiques. Par exemple : copier-coller son contenu dans un puzzle ou un challenge du site codingame.com.
Le début du fichier stand-alone indique la version et le commit git utilisés pour le générer.
Le script code/builder/builder.py
permet de regénérer manuellement ce fichier à partir du code actuel.
Mobile Items (en construction)¶
Ça fonctionne mais ce n’est vraiment pas pratique et il n’y a pas beaucoup de fonctions pour les manipuler, les déplacer, etc.
Cette partie sera détaillé plus tard.
>>> from mobitem import MobileItem
>>> board = Board(2, 2)
>>> mobitem = MobileItem(tile_owner=board[0, 0])
>>> print(board.render())
#.
..
>>> mobitem.move(x=1, y=0)
>>> mobitem.data = 'M'
>>> print(board.render())
.M
..
Exemple complet¶
Exemple inspiré du challenge codingame « Xmas Rush », lui-même inspiré du jeu de plateau « Labyrinthe ».
Chaque Tile possède deux attributs spécifiques :
mid_marker
: string de un caractère.roads
: dictionnaire contenant 4 éléments, correspondant aux 4 directions. La valeur de chaque clé est un booléen, indiquant si la tile possède une ouverture dans la direction donnée.
Une Tile est rendu sur un carré de 3x3 caractères, avec l’affichage des chemins ouverts et le mid_marker
écrit au milieu.
La règle d’adjacence est celle par défaut : les 4 directions, mais pas de diagonale.
L’initialisation du board est effectuée par un tableau de caractère, chacun d’eux permet de déduire le contenu du dictionnaire roads
de la Tile concernée.
from aboard import Board, Tile, Dir, BoardRenderer, compute_direction
class XmasTile(Tile):
"""
Tile spécifique, pour l'exemple inspiré de Xmas Rush.
Variables membres :
mid_marker : string de un seul caractère, qui sera affiché lors
du rendu de la tile.
C'est l'équivalent de tile.data dans la classe de base.
roads : dictionnaire avec 4 éléments. Les clés du dictionnaire
sont les 4 directions Dir.UP, Dir.RIGHT, Dir.DOWN, Dir.LEFT.
Chaque valeur est un booléen, indiquant si le chemin de
cette tile dans la direction indiqué par la clé est ouvert.
Les roads de la tile peuvent être initialisées à l'aide d'un
caractère (voir fonction Tile.dirs_from_input).
"""
# Liste des caractères "roadful" permettant d'initialiser les roads.
# Il n'y a pas toutes les combinaisons possibles,
# car on n'en a pas besoin.
# clé : un caractère. (Sa représentation visuelle correspond
# à peu près aux roads ouvertes).
# valeur : une liste de directions, indiquant les roads ouvertes
# correspondantes. Celles qui ne sont pas dans la liste doivent
# rester fermées.
# Par exemple : la lettre L équivaut à une road ouverte vers le haut,
# et une vers la droite.
DICT_ROADFUL_DIRS_FROM_CHAR = {
"-": (Dir.LEFT, Dir.RIGHT),
"|": (Dir.UP, Dir.DOWN),
"L": (Dir.UP, Dir.RIGHT),
"/": (Dir.DOWN, Dir.RIGHT),
"7": (Dir.DOWN, Dir.LEFT),
"J": (Dir.UP, Dir.LEFT),
"+": (Dir.LEFT, Dir.RIGHT, Dir.UP, Dir.DOWN),
" ": (),
}
def __init__(self, x=None, y=None, board_owner=None):
super().__init__(x, y, board_owner)
self.roads = {
Dir.UP: False,
Dir.RIGHT: False,
Dir.DOWN: False,
Dir.LEFT: False
}
self.mid_marker = " "
def dirs_from_input(self, char_roadful):
"""
Initialise les routes ouvertes de la tile (dict self.roads),
à partir du caractère "roadful" passé en paramètre.
"""
for dir_ in XmasTile.DICT_ROADFUL_DIRS_FROM_CHAR[char_roadful]:
self.roads[dir_] = True
def render(self, w=3, h=3):
"""
Renvoie la représentation graphique de la tile. Le rendu par
défaut est sur un carré de 3x3 caractères. Les roads ouvertes
sont affichées par des tirets et des traits verticaux.
La variable mid_marker est affichée au milieu du rendu.
Exemple :
Avec un self.roads dans lequel les directions Dir.UP et Dir.RIGHT
sont à True, et les deux autres sont à False.
Avec un self.mid_marker = "A".
Le rendu renverra la liste de string suivantes :
[
' | ',
' A-',
' ',
]
"""
path_up = "|" if self.roads[Dir.UP] else " "
path_left = "-" if self.roads[Dir.LEFT] else " "
path_right = "-" if self.roads[Dir.RIGHT] else " "
path_down = "|" if self.roads[Dir.DOWN] else " "
template = " %s \n%s%s%s\n %s "
data = (
path_up, path_left,
self.mid_marker[:1].rjust(1),
path_right, path_down
)
str_result = template % data
return str_result.split("\n")
def pass_through_xmas(tile_source, tile_dest):
"""
Fonction utilisée comme condition de déplacement pour le path-finding.
On considère qu'il est possible de passer d'une tile à l'autre si la
route adéquate est ouverte à la fois dans la tile source et dans la
tile dest. La route adéquate dépend de la position relative de
tile_source et tile_dest.
Exemple :
Avec tile_1.roads[Dir.RIGHT] = True
Avec tile_2.roads[Dir.LEFT] = True
C'est à dire, que les rendu seraient les suivants :
tile_1 = [
' ',
' .-',
' ',
]
tile_2 = [
' ',
'-. ',
' ',
]
Le déplacement est possible si tile_1 est à gauche de tile_2.
Dans les autres cas de positions relatives, le déplacement n'est
pas possible.
"""
dir_ = compute_direction(tile_source, tile_dest)
roads_to_check = {
Dir.UP: (Dir.UP, Dir.DOWN),
Dir.DOWN: (Dir.DOWN, Dir.UP),
Dir.LEFT: (Dir.LEFT, Dir.RIGHT),
Dir.RIGHT: (Dir.RIGHT, Dir.LEFT),
}
road_to_check = roads_to_check.get(dir_)
if road_to_check is None:
# Not supposed to happen
return False
road_source, road_dest = road_to_check
return tile_source.roads[road_source] and tile_dest.roads[road_dest]
renderer = BoardRenderer(
tile_w=3, tile_h=3,
tile_padding_w=1, tile_padding_h=1,
chr_fill_tile_padding="."
)
board = Board(6, 4, class_tile=XmasTile, default_renderer=renderer)
# Plan du board. (Une multi-line string de 6 colonnes et 4 lignes, comme
# les dimensions du board). Chaque caractère est un "roadful" char,
# définissant les directions ouvertes de la tile correspondante.
# Certains chemins sont des culs-de-sac, et il y a deux chemins
# possibles pour aller d'une tile du bas vers une tile du haut.
# C'est fait exprès pour le test.
BOARD_MAP = """
/---7
/+7 |
|| |
L----J
"""
board_map = BOARD_MAP.replace("\n", "")
# Initialisation des routes du board, à partir de BOARD_MAP.
for tile, char_roadful in zip(board, board_map):
tile.dirs_from_input(char_roadful)
# Recherche du chemin le plus court. Marquage de ce chemin en définissant
# les mid_marker des tiles par lesquelles ont passe.
# Départ = 0. Tile suivante = 1. Ainsi de suite.
for index, tile in enumerate(
board.get_by_pathfinding((0, 3), (2, 0), pass_through_xmas)
):
tile.mid_marker = str(index)
# Affichage du rendu, permettant de vérifier le plan du board,
# ainsi que le chemin trouvé.
print(board.render())
expected_result = """
. . . . .
. 4-.-5-.- -.- -.-
. | . . . . |
.......................
. | . . . . |
2-.-3-.- . . .
| . | . | . . . |
.......................
| . | . . . . |
1 . . . . .
| . | . . . . |
.......................
| . . . . . |
0-.- -.- -.- -.- -.-
. . . . .
"""