7.4 GRILLE DE JEU, JEU DE PLATEAU, SOLITAIRE

 

 

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.

CONCEPTS DE PROGRAMMATION: Plateau de jeu, règles du jeu, spécifications, fin de jeu (game over)

 

 

INITIALISATION DU PLATEAU, CONTRÔLE AVEC LA SOURIS

 

Le plateau est formé d’un arrangement régulier de trous dans lesquels sont disposées des billes en marbre ou, dans certaines variantes, des bâtonnets. Le plateau de solitaire le plus connu, le plateau anglais, comporte 33 trous disposés en forme de croix. Au départ, tous les trous sont occupés par une bille sauf le trou central. Comme son nom l’indique, ce jeu est généralement joué en solo.

Les règles sont les suivantes : un mouvement consiste à choisir intelligemment une bille puis la déplacer jusque dans un trou libre en sautant par-dessus une autre bille adjacente, soit horizontalement, soit verticalement. On supprime alors du plateau la bille par-dessus laquelle on a sauté.
 

Un plateau anglais de Solitaire d’Inde, 1830
© 2003 puzzlemuseum.com

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.

Durant la phase de glisser de la sourison déplace l’image de la bille à la position actuelle du curseur de la souris à l’aide de setLocationOffset(). On évite de restreindre le mouvement aux positions centrales des cellules pour véritablement suivre le curseur et donner lieu à un mouvement continu. Pour éviter les problèmes de superposition d’acteurs dans la grille, il est important de ne déplacer que l’image de sprite de l’acteur et non l’acteur-bille en lui-même qui reste dans sa case jusqu’à ce que le mouvement soit validé (d’où le nom de la fonction : Offset = décalage). Dans cette première version du jeu, la bille déplacée doit simplement revenir à sa position initiale après l’événement Release event lors du relâchement de la souris. Ceci se réalise avec l’appel setLocationOffset(0, 0).

 




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()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

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:

1.
2.
3.
4.

5.
La cellule start contient une bille
La cellule dest ne contient pas de bille
dest est une cellule appartenant au plateau du jeu
start et dest ne sont séparées que par une seule cellule qui leur est adjacente à toutes deux horizontalement ou verticalement
Il y a une bille dans la cellule située entre start et end

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()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

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.

Il ne faut cependant pas oublier les nombreuses configurations possibles dans lesquelles le jeu est considéré comme terminé même si le but n’est pas atteint. Cette situation survient lorsqu’il reste plus d’une boule sur le plateau mais qu’il n’est plus possible d’effectuer un mouvement conforme aux règles. On ne sait pas a priori s’il est possible de tomber dans cette situation en ne faisant que des mouvements légaux mais il faut toujours programmer de manière défensive pour se prémunir contre l’imprévu. En programmation, il faut toujours croire à la loi de Murphy : « Lorsqu’il y a un truc qui peut foirer, ça va foirer ».

 

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()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

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

 

1.

Créer un plateau de solitaire français.

 

 

2.

Étendre le solitaire avec un score qui compte et afficher le nombre de mouvements effectués Il faudrait également pouvoir redémarrer le jeu à l’aide de la touche espace du clavier.


3.

Familiarisez-vous avec les stratégies de résolution du solitaire grâce aux conseils d’un connaisseur ou des nombreuses ressources disponibles sur le Web [plus...] Livre conseillé: John Beasley, The Ins and Outs of Peg Solitaire].

 

4.

Créer un plateau de solitaire d’après votre propre imagination.