INTRODUCTION |
Dans certains jeux vidéo, le positionnement des figurines de jeu est restreint aux cases d’une structure en grille qui sont de même taille et arrangées sous forme de matrice. Tenir compte de cette restriction à une structure de grille simplifie beaucoup l’implémentation du jeu. Comme son nom l’indique, la bibliothèque JGameGrid est partifculièrement optimisée pour les jeux sous forme de grille. Dans ce chapitre, nous allons développer par étapes le jeu du solitaire avec la disposition de plateau anglaise. Nous verrons également différentes méthodes applicables à tous les jeux sous forme de grille.
|
INITIALISATION DU PLATEAU, CONTRÔLE AVEC LA SOURIS |
Le but est de se débarrasser de toutes les billes du plateau excepté la dernière. Si la dernière bille se trouve au milieu du plateau en fin de jeu, on considère que la résolution du jeu est particulièrement élégante. Dans son implémentation sur ordinateur, il faudrait pouvoir prendre une bille indiquée par un clic de souris pour la déplacer par glisser-déposer. Lorsque le bouton est relâché, le jeu doit vérifier que le mouvement effectué respecte les règles du jeu. Si les règles sont violées, la bille doit retourner à sa place initiale et, dans le cas contraire, il faut faire apparaître une bille au nouvel emplacement et supprimer celle se trouvant dans le trou initial. La spécification du jeu est ainsi clarifiée et la phase d’implémentation peut débuter. Comme d’habitude, on procède par étapes en s’assurant que le jeu s’exécute convenablement à chaque modification du code. Il est parfaitement naturel d’utiliser une grille de jeu de 7x7 cellules sans pour autant utiliser celles qui se trouvent dans les coins. On commence par dessiner la grille avec la fonction initBoard() en utilisant comme arrière-fond l’image solitaire_board.png disponible dans la distribution TigerJython. On implémente le contrôle à l’aide de la souris en utilisant les gestionnaires d’événements mousePressed, mouseDragged, et mouseReleased. Lors d’un événement Press event, on garde trace de la cellule courante et de la bille qui s’y trouve. On peut obtenir cette bille à l’aide de getOneActorAt()qui retourne None si la cellule est vide. Écrire les informations importantes dans la barre d’état de la fenêtre de jeu ou dans la console permet de faciliter le processus de développement et de résoudre plus facilement les erreurs.
from gamegrid import * def isMarbleLocation(loc): if loc.x < 0 or loc.x > 6 or loc.y < 0 or loc.y > 6: return False if loc.x in [0, 1, 5, 6] and loc.y in [0, 1, 5, 6]: return False return True def initBoard(): for x in range(7): for y in range(7): loc = Location(x, y) if isMarbleLocation(loc): marble = Actor("sprites/marble.png") addActor(marble, loc) removeActorsAt(Location(3, 3)) # Remove marble in center def pressEvent(e): global startLoc, movingMarble startLoc = toLocationInGrid(e.getX(), e.getY()) movingMarble = getOneActorAt(startLoc) if movingMarble == None: setStatusText("Pressed at " + str(startLoc) + ". No marble found") else: setStatusText("Pressed at " + str(startLoc) + ". Marble found") def dragEvent(e): if movingMarble == None: return startPoint = toPoint(startLoc) movingMarble.setLocationOffset(e.getX() - startPoint.x, e.getY() - startPoint.y) def releaseEvent(e): if movingMarble == None: return movingMarble.setLocationOffset(0, 0) makeGameGrid(7, 7, 70, None, "sprites/solitaire_board.png", False, mousePressed = pressEvent, mouseDragged = dragEvent, mouseReleased = releaseEvent) setBgColor(Color(255, 166, 0)) setSimulationPeriod(20) addStatusBar(30) setStatusText("Press-drag-release to make a move.") initBoard() show() doRun()
|
MEMENTO |
Au lieu de réellement déplacer un acteur lors du glisser-déposer, il est souvent préférable de déplacer uniquement son image de sprite à l’aide de setLocationOffset(x, y) par rapport au central de l’acteur. Il est très important de faire une distinction claire entre les coordonnées de la souris et les coordonnées des cellules au sein de la grille lors de la gestion des mouvements de la souris. On utilisera les fonctions toLocationInGrid(pixel_coord) et toPoint(location_coord) pour passer d’un système de coordonnées à l’autre. Si l’on tente de déplacer un acteur depuis une cellule vide, les événements de glisser-déposer vont conduire à un crash du programme causé par un appel à movingMarble.setLocationOffset alors que movingMarble vaut None et ne représente par conséquent pas un objet possédant la méthode setLocationOffset. Pour éviter ce malheureux message d’erreur, on place au début des gestionnaires d’événements un test qui termine la fonction avec return si aucune bille n’est sélectionnée. |
IMPLÉMENTER LES RÈGLES DU JEU |
Comment vérifier que les règles du jeu sont bien respectées ? Il faut certainement avoir connaissance de la bille qui est en train d’être déplacée par l’intermédiaire de ses coordonnées start dans la grille de jeu. Il faut également connaître les coordonnées de destination dest. Les conditions suivantes doivent être réalisées pour que le mouvement soit valide:
Il est judicieux d’implémenter ces conditions dans une fonction getRemoveMarble(start, dest) qui retourne la bille qui doit être supprimée après un mouvement autorisé et qui retourne None en cas de mouvement illégal. De ce fait, il faudrait appeler cette fonction lors du relâchement de la souris et utiliser removeActor() pour supprimer du plateau l’acteur retourné si le mouvement est légal. from gamegrid import * def getRemoveMarble(start, dest): if getOneActorAt(start) == None: return None if getOneActorAt(dest) != None: return None if not isMarbleLocation(dest): return None if dest.x - start.x == 2 and dest.y == start.y: return getOneActorAt(Location(start.x + 1, start.y)) if start.x - dest.x == 2 and dest.y == start.y: return getOneActorAt(Location(start.x - 1, start.y)) if dest.y - start.y == 2 and dest.x == start.x: return getOneActorAt(Location(start.x, start.y + 1)) if start.y - dest.y == 2 and dest.x == start.x: return getOneActorAt(Location(start.x, start.y - 1)) def isMarbleLocation(loc): if loc.x < 0 or loc.x > 6 or loc.y < 0 or loc.y > 6: return False if loc.x in [0, 1, 5, 6] and loc.y in [0, 1, 5, 6]: return False return True def initBoard(): for x in range(7): for y in range(7): loc = Location(x, y) if isMarbleLocation(loc): marble = Actor("sprites/marble.png") addActor(marble, loc) removeActorsAt(Location(3, 3)) # Remove marble in center def pressEvent(e): global startLoc, movingMarble startLoc = toLocationInGrid(e.getX(), e.getY()) movingMarble = getOneActorAt(startLoc) if movingMarble == None: setStatusText("Pressed at " + str(startLoc) + ". No marble found") else: setStatusText("Pressed at " + str(startLoc) + ". Marble found") def dragEvent(e): if movingMarble == None: return startPoint = toPoint(startLoc) movingMarble.setLocationOffset(e.getX() - startPoint.x, e.getY() - startPoint.y) def releaseEvent(e): if movingMarble == None: return destLoc = toLocationInGrid(e.getX(), e.getY()) movingMarble.setLocationOffset(0, 0) removeMarble = getRemoveMarble(startLoc, destLoc) if removeMarble == None: setStatusText("Released at " + str(destLoc) + ". Not a valid move.") else: removeActor(removeMarble) movingMarble.setLocation(destLoc) setStatusText("Released at " + str(destLoc)+ ". Marble removed.") startLoc = None movingMarble = None makeGameGrid(7, 7, 70, None, "sprites/solitaire_board.png", False, mousePressed = pressEvent, mouseDragged = dragEvent, mouseReleased = releaseEvent) setBgColor(Color(255, 166, 0)) setSimulationPeriod(20) addStatusBar(30) setStatusText("Press-drag-release to make a move.") initBoard() show() doRun()
|
MEMENTO |
Au lieu d’utiliser plusieurs tests if et instructions return consécutifs pour quitter la fonction getRemoveMarble(), il serait également possible d’utiliser une combinaison de toutes ces conditions à l’aide d’un opérateur booléen. Aucune des alternatives n’est objectivement meilleure que l’autre : il s’agit plutôt d’une question de goût. |
TESTER LES CONDITIONS DE FIN DE JEU |
À présent, il ne reste plus qu’à tester les conditions de fin de jeu à l’issue de chaque mouvement. Le jeu est certainement terminé s’il ne reste plus qu’une seule boule sur le plateau et, dans ce cas, le but du jeu est atteint.
Pour se tirer d’affaire, on peut définir une fonction isMovePossible(). qui teste pour chaque bille restante si elle peut être utilisée dans un mouvement légal. isMovePossible() va tester pour chaque bille s’il existe une bille adjacente par-dessus laquelle elle peut sauter pour atterrir dans un trou vide juste au-delà [plus...il faut pour cela se contenter d’explorer les trous à proximité de chaque bille ]. from gamegrid import * def checkGameOver(): global isGameOver marbles = getActors() # get remaining marbles if len(marbles) == 1: setStatusText("Game over. You won.") isGameOver = True else: # check if there are any valid moves left if not isMovePossible(): setStatusText("Game over. You lost. (No valid moves available)") isGameOver = True def isMovePossible(): for a in getActors(): # run over all remaining marbles for x in range(7): # run over all holes for y in range(7): loc = Location(x, y) if getOneActorAt(loc) == None and \ getRemoveMarble(a.getLocation(), Location(x, y)) != None: return True return False def getRemoveMarble(start, dest): if getOneActorAt(start) == None: return None if getOneActorAt(dest) != None: return None if not isMarbleLocation(dest): return None if dest.x - start.x == 2 and dest.y == start.y: return getOneActorAt(Location(start.x + 1, start.y)) if start.x - dest.x == 2 and dest.y == start.y: return getOneActorAt(Location(start.x - 1, start.y)) if dest.y - start.y == 2 and dest.x == start.x: return getOneActorAt(Location(start.x, start.y + 1)) if start.y - dest.y == 2 and dest.x == start.x: return getOneActorAt(Location(start.x, start.y - 1)) return None def isMarbleLocation(loc): if loc.x < 0 or loc.x > 6 or loc.y < 0 or loc.y > 6: return False if loc.x in [0, 1, 5, 6] and loc.y in [0, 1, 5, 6]: return False return True def initBoard(): for x in range(7): for y in range(7): loc = Location(x, y) if isMarbleLocation(loc): marble = Actor("sprites/marble.png") addActor(marble, loc) removeActorsAt(Location(3, 3)) # Remove marble in center def pressEvent(e): global startLoc, movingMarble if isGameOver: return startLoc = toLocationInGrid(e.getX(), e.getY()) movingMarble = getOneActorAt(startLoc) if movingMarble == None: setStatusText("Pressed at " + str(startLoc) + ".No marble found") else: setStatusText("Pressed at " + str(startLoc) + ".Marble found") def dragEvent(e): if isGameOver: return if movingMarble == None: return startPoint = toPoint(startLoc) movingMarble.setLocationOffset(e.getX() - startPoint.x, e.getY() - startPoint.y) def releaseEvent(e): if isGameOver: return if movingMarble == None: return destLoc = toLocationInGrid(e.getX(), e.getY()) movingMarble.setLocationOffset(0, 0) removeMarble = getRemoveMarble(startLoc, destLoc) if removeMarble == None: setStatusText("Released at " + str(destLoc) + ". Not a valid move.") else: removeActor(removeMarble) movingMarble.setLocation(destLoc) setStatusText("Released at " + str(destLoc)+ ". Valid move - Marble removed.") checkGameOver() startLoc = None movingMarble = None isGameOver = False makeGameGrid(7, 7, 70, None, "sprites/solitaire_board.png", False, mousePressed = pressEvent, mouseDragged = dragEvent, mouseReleased = releaseEvent) setBgColor(Color(255, 166, 0)) setSimulationPeriod(20) addStatusBar(30) setStatusText("Press-drag-release to make a move.") initBoard() show() doRun()
|
MEMENTO |
Après chaque mouvement, il faut vérifier si le jeu est terminé avec checkGameOver() et, le cas échéant, ajuster le fanion booléen isGameOver = True qui indique en tout temps l’état du jeu. En particulier, il ne faut pas oublier d’empêcher tout déplacement des billes par la souris avec un return conditionnel placé tout au début de chaque gestionnaire d’événements concernant la souris. |
EXERCICES |
|