Perfect Aim est composé :
- D'un plateau de jeu
- De 2 à 4 joueurs
- D'objets bonus et malus
- De boules de feu
Le plateau est une matrice bidimensionnelle.
Les cases vides dans lesquelles les joueurs peuvent aller.
Les cases dans lesquelles les joueurs ne peuvent pas aller.
Les cases qui tuent tout joueur qui s'y rend.
Le sol qui va devenir de la lave dans les 5 secondes.
Voici un plateau de jeu avec tous ces éléments, ainsi que des objets et des joueurs :
Le sol commence à s'endommager par l'extérieur à partir de 35 secondes de jeu. Toutes les 10 secondes la zone de jeu est réduite d'une case depuis tous les côtés. Tout le terrain est recouvert de lave au bout de 2 min.
Au début de la partie, le plateau est parfaitement symétrique.
Un joueur est une entité dont les actions sont déterminées par les programmes des participants. Au début de la partie, un joueur effectue une action toutes les secondes. Cette cadence peut être accélérée ou ralentie grâce à des objets.
Une action d'un joueur est : se déplacer dans une des 4 directions, attaquer dans une des 4 directions, ou attendre sur sa case.
Deux joueurs ne peuvent pas se trouver sur la même case en même temps. Si deux joueurs essaient de se rendre sur la même case, un des deux joueurs restera immobile.
- Lorsqu'un joueur se trouve sur une case contenant un objet, il le ramasse obligatoirement.
- Des objets sont régénérés sur le plateau s'il n'y en a plus.
- Il y a au plus un objet par case.
Le ramasser confère un bonus de 0.25 action par seconde en plus, permanent. Les bonus et malus de vitesse sont cumulables.
Le ramasser confère un malus de 0.25 action par seconde en moins, permanent. Par conséquent, il annule un bonus de vitesse. La vitesse minimale d'un joueur est 0.5 action par seconde.
Une pièce d'or, qui n'offre aucun avantage au joueur. Elles sont utilisées pour comparer deux équipes ayant un même nombre de victoires pendant le tournoi.
Un sort qui permet de lancer 4 boules de feu dans toutes les directions. Ce sort est cumulable. Si un joueur possède un ou plusieurs sorts de super boule de feu, un sera obligatoirement consommé à la prochaine attaque.
Un bouclier, qui protège de la prochaine boule de feu reçue. Non cumulable.
L'attaque du jeu !
Voici les règles associées aux boules de feu :
- Elles se déplacent à la vitesse de 4 cases / seconde.
- Elles se déplacent en ligne droite jusqu'au premier mur rencontré.
- Elles éliminent un joueur sans bouclier, et sont éliminées par un joueur avec bouclier.
- Elles n'intéragissent pas avec les objets au sol et les autres boules de feu.
- Deux joueurs sont susceptibles de s'entretuer s'ils s'attaquent en même temps.
- Un joueur ne peut attaquer que s'il n'y a plus de boules de feu qu'il a envoyées sur le terrain, ou qu'il a un sort de super boule de feu.
Les programmes des participants sont à écrire dans le dossier ./players. Ce dossier contient des stratégies d'exemple qui permettent d'avoir des exemples concrets de l'utilisation de l'API du jeu.
La méthode play est appelée à intervalle régulier pour demander au joueur quelle action jouer.
class BestPlayer(Player):
NAME = "Les Meilleurs"
def play(self, game: Game) -> Action:
action = Action.WAIT
# ...votre stratégie...
return actionLes paramètres d'appel sont :
selfle joueur, instance de la classePlayer.gamele jeu, instance de la classeGame.
Les valeurs de retour possibles sont WAIT, MOVE_UP, MOVE_DOWN, MOVE_LEFT, MOVE_RIGHT, ATTACK_UP, ATTACK_DOWN, ATTACK_LEFT, ATTACK_RIGHT. (À préfixer avec Action..)
Les informations disponibles sont les suivantes :
xety (int)les coordonnées du joueur, respectivement horizontale et verticale.(0, 0)est le mur en haut à gauche.speed (Fraction)la vitesse du joueur, qui correspond au nombre d'actions que votre joueur effectue par seconde (1.0au début du jeu). Cette valeur est incrémentée de0.25à chaque bonus de vitesse collecté. (Et-0.25par malus.)coins (int)le nombre de pièces du joueur.super_fireballs (int)le nombre de super boules de feu collectées. La prochaine attaque en consommera une s'il y en a une disponible.shield (bool)la présence d'un bouclier. Vrai si le joueur est protégé de la prochaine boule de feu qu'il reçoit.action (Action)la dernière action jouée, une constante parmi les 9.color (Tile)la couleur du joueur, une constante parmiTile.PLAYER_RED,_BLUE,_YELLOW,_GREEN.
Par exemple :
play(self, game: Game) -> Action:
print(f"Je suis le joueur {self.color.name}")
print(f"Je suis en (x, y) = ({self.x}, {self.y})")
if self.shield:
print("J'ai un bouclier !")
if self.speed >= 2.0:
print("J'ai ramassé 4 bonus de vitesse")
return Action.WAITMéthodes disponibles :
is_action_valid(action: Action) -> bool: renvoie vrai si l'action est valide.can_attack() -> bool: renvoie vrai si on peut attaquer.
if self.can_attack():
return Action.ATTACK_UP
elif self.is_action_valid(Action.MOVE_UP):
print("Cap au Nord capitaine !")
return Action.MOVE_UPLes actions possèdent des méthodes pour les manipuler facilement :
apply(coords: Tuple[int, int]) -> Tuple[int, int]oùcoordsest une paire de coordonnées : applique un déplacement aux coordonnéesswap() -> Action: donne la direction inverseto_attack() -> Action: transforme un déplacement en attaqueto_movement() -> Action: transforme une attaque en déplacementis_attack() -> bool: vrai si c'est une attaqueis_movement() -> bool: devine ;)
Par exemple :
coords = (1, 1)
# Action.MOVE_RIGHT.apply(coords) == (1, 2)
# Action.MOVE_RIGHT.apply(Action.MOVE_RIGHT.apply(coords)) == (1, 3)
a = Action.ATTACK_UP
# a.swap() == Action.ATTACK_DOWN
# a.to_movement() == Action.MOVE_UP
# a.is_movement() == False
# a.swap().to_movement() == Action.MOVE_DOWN
# == a.to_movement().swap()
# a.swap().to_movement().apply(coords) == (2, 1)Le paramètre game est un object complexe qui représente l'état du jeu.
Valeurs simples :
t (Fraction)le temps de jeu écoulé depuis le début de la partie, en secondes.size (int)la dimension de la grille.
On a game.size == 21.
La représentation la plus simple du jeu est tile_grid (List[List[Tile]]). C'est une matrice bidimensionnelle de Tile où Tile est l'énumération de tous les objets du jeu possibles.
Par exemple, comme le jeu est toujours entouré d'un mur, on a game.tile_grid[0][0] == Tile.WALL.
[[WALL, WALL, WALL, WALL, ...],
[WALL, PLAYER_RED, FLOOR, COIN, ...],
[WALL, FLOOR, WALL, FLOOR, ...],
[WALL, FLOOR, WALL, FLOOR, ...],
... ]
# Première ligne
# game.tile_grid[1] == [WALL, PLAYER_RED, FLOOR, COIN, ...]
# Accès aux cases
# game.tile_grid[0][0] == Tile.WALL
# game.tile_grid[1][1] == Tile.PLAYER_RED
# game.tile_grid[1][3] == Tile.COIN
# Point de départ du joueur bleu
# game.tile_grid[game.size - 1][game.size - 1] == Tile.PLAYER_BLUELes valeurs possibles de Tile sont :
FLOORune case vide où il est possible de se rendreWALLLAVAune case de lave qui tue tous les joueurs y entrantDAMAGED_FLOORune case qui va devenir de la lave dans un futur proche (moins de 5 secondes)SPEEDBOOSTSPEEDPENALTYCOINSUPER_FIREBALLSHIELDPLAYER_REDPLAYER_BLUEPLAYER_YELLOWPLAYER_GREENFIREBALLune boule de feu
S'il y a une superposition (par exemple DAMAGED_FLOOR, SPEEDBOOST, FIREBALL sur la même case), c'est le dernier objet dans l'ordre de la liste au dessus qui est enregistré dans tile_grid (donc dans cet exemple FIREBALL).
Il existe des méthodes pour savoir rapidement si un élément de la classe Tile est dans une certaine catégorie :
is_floor() -> bool:FLOORetDAMAGED_FLOORis_background() -> bool:FLOOR,WALL,LAVAetDAMAGED_FLOORis_collectible() -> bool: un des 5 objets ramassablesis_bonus() -> bool:SPEEDBOOST,SUPER_FIREBALLetSHIELDis_player() -> bool: un des 4 joueursis_dangerous() -> bool:LAVA,DAMAGED_FLOOR, les joueurs etFIREBALL
Par exemple :
a = Tile.FLOOR
b = Tile.SHIELD
# a.is_background() == True
# a.is_bonus() == False
# b.is_background() == False
# b.is_bonus() == TrueVoici des exemples plus complets :
def play(self, game: Game) -> Action:
# ...
if game.tile_grid[self.y][self.x + 1].is_dangerous():
print("Je ne dois pas aller à droite, c'est dangereux !")
# ...Pour se repérer dans une matrice bidimensionnelle :
- Au dessus :
g[y - 1][x]. - En dessous :
g[y + 1][x]. - À gauche :
g[y][x - 1]. - À droite :
g[y][x + 1].
def play(self, game: Game) -> Action:
# On regarde dans les 4 directions
for a in (Action.MOVE_UP, Action.MOVE_DOWN,
Action.MOVE_LEFT, Action.MOVE_RIGHT):
# La case adjacente au joueur dans la direction `a`
coords = (self.x, self.y)
x, y = a.apply(coords)
# S'il y a un joueur sur cette case, on attaque dans cette direction
if game.tile_grid[y][x].is_player():
return a.to_attack()
# ...Pour avoir une représentation complète du jeu, il faut utiliser conjointement background (List[List[Tile]]) et entity_grid (List[List[Set[Entity]]]). Ce sont deux matrices bidimensionnelles, comme tile_grid, mais elles permettent de connaître les éléments superposés, ainsi que des détails sur eux.
background est une matrice qui ne contient que des éléments du fond : FLOOR, WALL, LAVA, DAMAGED_FLOOR.
Cela permet par exemple de savoir quelle est la case sous son joueur :
# tile_grid ne permet pas de savoir sur quelle case on est, car on a toujours
# game.tile_grid[self.y][self.x].is_player() == True
# car PLAYER_* est après FLOOR et DAMAGED_FLOOR dans la liste donnée plus haut
if game.background[self.y][self.x] == Tile.DAMAGED_FLOOR:
print("Je ferais mieux d'aller ailleurs")Pour connaître les entités sur un case il faut alors utiliser entity_grid, qui est une matrice bidimensionnelle d'ensembles d'entités. Les entités sont des sous-classes de entities.Entity.
L'arborescence est la suivante :
EntityMovingEntityPlayerEntityRedPlayerBluePlayerYellowPlayerGreenPlayer
Fireball
CollectableEntityCoinSpeedBoostSpeedPenaltySuperFireballShield
Les attributs des instances de ces classes sont les suivants :
- Toutes les entités possèdent des coordonnées
xety (int). - Toutes les entités possèdent une constante
TILE (Tile), ce qui permet d'utiliserTILE.is_*()par exemple. - Les entités mobiles ont en plus
speed (Fraction),action (Action)etaction_progress (Fraction), respectivement la vitesse, l'action en cours et l'avancement de l'action en cours (entre 0 et 1). - Les joueurs ont en plus une couleur
color (Tile), une constante parmiTile.PLAYER_RED,_BLUE,_YELLOW,_GREEN.
Par exemple, regardons à droite jusqu'au bout du couloir :
# Coordonnées de la case voisine
x, y = Action.MOVE_RIGHT.apply((self.x, self.y))
# On regarde jusqu'au prochain mur
while game.background[y][x] != Tile.WALL:
# On regarde l'ensemble des entités sur la case
for entity in game.entity_grid[y][x]:
# Quelques exemples
if isinstance(entity, entities.PlayerEntity):
# On attaque un joueur, on affiche sa couleur
print(f"À l'attaque de {entity.color.name} !")
return Action.ATTACK_RIGHT
elif (
isinstance(entity, entities.Fireball)
# C'est une entité mobile, on peut savoir où elle va
and entity.action == Action.MOVE_LEFT
):
print("On se fait attaquer !")
return Action.MOVE_LEFT
elif (
isinstance(entity, entities.CollectableEntity)
# Est-ce que c'est un bonus ?
and entity.TILE.is_bonus()
):
print("Oh un bonus :)")
return Action.MOVE_RIGHT
# On va sur la case suivante
x, y = Action.MOVE_RIGHT.apply((x, y))Enfin, il est possible de parcourir l'ensemble de toutes les entités du jeu avec game.entities (Set[Entity]) :
p = 0
b = 0
for entity in game.entities:
if isinstance(entity, entities.PlayerEntity):
p += 1
elif entity.TILE.is_bonus():
b += 1
print(f"Il reste {p} joueurs et {b} bonus en jeu.")Avec cette doc vous savez tout ce qu'il faut pour gagner ! Vous pouvez lire le code des stratégie d'exemple, comme IndianaJones, qui est une bonne base pour commencer si vous ne savez pas où aller.
Code :
- Code du jeu par Gautier Ben Aïm, sous licence MIT
- Code des stratégies des participants : consulter les licences dans les fichiers du dossier ./players/
Ressources graphiques :
- Instant Dungeon! v1.4 Art Pack (Jeu d'origine) par Scott Matott, Voytek Falendysz et José Luis Peiró Lima, sous licence OGA-BY-3.0
- Magma / Lava Tileset 8x8 par GoodClover, sous licence CC-BY-4.0















