imported from svn

This commit is contained in:
Quentin Legot 2021-03-23 15:38:37 +01:00
commit 24b2b96360
76 changed files with 2996 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea
.svn
__pychache/

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
Rapport/Rapport.odt Normal file

Binary file not shown.

BIN
Rapport/rapport.pdf Normal file

Binary file not shown.

512
Rapport/rapport.tex Normal file
View File

@ -0,0 +1,512 @@
\documentclass[a4paper,12pt]{article}
\usepackage[utf8]{inputenc}
\usepackage[french]{babel}
\usepackage[T1]{fontenc}
\usepackage[top=2cm,bottom=2cm,left=2cm,right=2cm]{geometry}
\usepackage{graphicx}
\usepackage{wrapfig}
\usepackage{float}
\usepackage{enumitem}
\usepackage{pifont}
\usepackage{fancyhdr}
\usepackage{hyperref}
\usepackage{xcolor}
\usepackage{titling}
\usepackage{amsmath}
\usepackage{algpseudocode}
\usepackage{algorithm}
\usepackage{float}
\definecolor{light-gray}{gray}{0.85}
\newcommand{\subtitle}[1]{
\posttitle{
\par\end{center}
\begin{center}\large#1\end{center}
\vskip0.5em}
}
\algnewcommand\algorithmicforeach{\textbf{for each}}
\algdef{S}[FOR]{ForEach}[1]{\algorithmicforeach\ #1\ \algorithmicdo}
\title{Rapport de projet}
\subtitle{Sokoban}
\author{Legot Quentin 06.13.45.06.86,\\
Page Arthur 07.68.88.61.20,\\
Besevic Ivan 06.37.72.82.73,\\
Couture Thorkil}
\pagestyle{fancy}
\renewcommand\footrulewidth{1pt}
\fancyfoot[L]{}
\fancyfoot[C]{}
\fancyfoot[R]{Page \thepage}
\begin{document}
\begin{figure}
\centering
\includegraphics[width=0.7\linewidth]{Illustrations/unicaen.png}
\end{figure}
\maketitle
\begin{center}
\href{http://www.unicaen.fr}{\textbf{Université de Caen Normandie}}
\end{center}
\newpage
\tableofcontents
\newpage
\section{Introduction}
\subsection{Explication du projet}
Le but de notre projet était de créer un jeu de type « Sokoban » à la fois fonctionnel et riche. Une fois le jeu de base terminé, nous avons décidé dajouter diverses fonctionnalités telles que le jeu contre un ordinateur, la génération aléatoire de niveaux et le comptage des points. Nous avons par ailleurs mis en place des niveaux de difficulté pour rendre le jeu accessible à tous et ainsi éviter davoir un contenu trop monotone. Nous avons partagé le travail à quatre pour que chaque membre puisse ajouter sa touche personnelle au jeu. Nous allons donc ici vous parler du jeu en lui-même (ses principes de base et ses mécaniques), des étapes de la conception et des différentes fonctionnalités que nous avons ajouté au jeu de base.
\subsection{Le Sokoban, quest-ce que cest ?}
Créé au Japon en 1982, le Sokoban est un jeu vidéo de type puzzle à un joueur dans lequel on incarne un ouvrier devant ranger des caisses dans un entrepôt. Le joueur peut se déplacer dans toutes les directions et peut seulement pousser une caisse à la fois. Les déplacements en diagonale ne sont pas possibles et le joueur ne peut pas tirer de caisses. Dans lentrepôt, outre les caisses et les murs, se trouvent aussi des points. Pour gagner et finir le niveau, il faut que tous les points soient recouverts par des caisses. Suivant la spécificité du niveau le joueur peut se retrouver bloqué en poussant une caisse dans un coin. Le but du jeu est donc de finir le niveau en effectuant le moins de mouvements possibles.
\begin{figure}[H]
\includegraphics{./Illustrations/Original.png}
\caption{Premier niveau du jeu original de 1982}
\end{figure}
\newpage
\section{Manuel du jeu}
\subsection{Préambule}
Notre jeu a été développé pour les versions de Python supérieures à la 3.6, aucun test n'a été fait sur les précédentes versions.\\
Le jeu fonctionne sur Linux avec xorg et Windows 10, aucun test n'a été effectué sur Mac OS X (il peut y avoir des problèmes de dispositions clavier, appuyez sur W au lieu de Z par exemple).\\
\subsection{Lancement du jeu}
Tout d'abord, pour lancer le jeu, sachez qu'il vous faut avoir installé Python ainsi que la bibliothèque Pygame.
Une fois ceci fait, voici les étapes à suivre pour lancer le jeu:\\
\begin{itemize}
\item Placez vous dans le répertoire racine du jeu, où se trouve le fichier \textit{main.py}.
\item Ouvrez un terminal et tapez "\textit{python3 main.py}".
\item Le jeu se lancera.
\end{itemize}
Le jeu étant lancé, une fenêtre s'ouvrira, vous menant directement au menu principal du jeu.\\
\begin{figure}[H]
\begin{center}
\includegraphics{./Illustrations/menu.png}
\caption{Menu principal du jeu.}
\end{center}
\end{figure}
\subsection{Navigation dans les menus}
La navigation dans les menus se fait à la souris ou au clavier. Si vous utilisez la souris, cliquez simplement sur les lignes voulues; pour le clavier, il faut appuyer sur les touches indiquées à gauche des lignes. Pour les nombres, utilisez le pavé numérique ou faites \textit{shift} + \textit{NUM} si votre PC n'en est pas équipé.\\
Appuyez sur \textit{ECHAP} ou sur la flèche qui va vers la gauche pour revenir en arrière ou quitter le jeu si vous êtes sur le premier écran. Vous pouvez appuyez sur la croix de la fenêtre pour quitter le jeu à tout moment.
\subsection{Choix du niveau}
Vous voulez jouez tout de suite ? Pas de problème, suivez simplement les étapes suivantes:
\begin{enumerate}
\item Choisissez le mode de jeu le plus adapaté ( \textit{Beginner , Casual , Elite} ou \textit{Random}).
\item Pour les trois premier choix, plusieurs niveaux s'offrent à vous, classés du plus simple au plus compliqué.
\item Pour le quatrième choix (\textit{Random}), une grille sera crée selon vos préférences, vous n'avez qu'à entrer le nombres de caisses avec lesquelles vous souhaitez jouer (de 1 à 9).
\item Dernière étape enfin, vous devrez choisir si vous jouerez contre un ami (\textit{Versus Player}), ou contre l'ordinateur (\textit{Versus Computer}).
\end{enumerate}
\begin{figure}[H]
\begin{center}
\includegraphics[scale=0.4]{./Illustrations/splitscreen.png}
\caption{Début de partie en mode deux joueurs. Difficulté: \textit{Beginner}. Niveau: \it1}
\end{center}
\end{figure}
\subsection{Comment jouer ?}
Si vous avez suivi les étapes précédentes, vous devriez voir apparaître la grille de jeu. Nous arrivons donc enfin à la partie la plus intéressante, comment jouer.
\begin{itemize}
\item Si vous jouer contre un ami, sachez que le joueur de gauche déplacera son personnage grâce au touches "\textit{z,q,s,d}", respectivement "\textit{haut, gauche,bas,droite}", et que le joueur de droite se servira des flèches du clavier. Le but, en mode deux joueurs est donc de placer toutes les caisses sur les trous en essayant de se déplacer au minimum. Ainsi le joueur ayant effectué le moins de coups pour placer ses caisses gagne la partie. Une fois la partie terminée, le nom du vainqueur est affiché et vous retournez automatiquement au menu principal.
\item Si vous jouez contre l'ordinateur, vous devrez utiliser les touches "\textit{z,q,s,d}" pour vous déplacer. L'ordinateur joue en même temps que vous (il se déplace quand vous vous déplacez) mais si jamais vous avez fini avant lui de placer toutes vos caisses, alors il finira sa partie automatiquement après vous.\\
\item Sachez que si vous en avez assez de perdre ou tout simplement que le jeu vous ennuie, vous pouvez quitter à tout moment la partie grâce à la touche "\textit{echap}" de votre clavier.
\end{itemize}
\subsection{"\textit{Generate levels}"}
Vous avez sûrement dû vous rendre compte qu'une rubrique "\textit{Generate levels}" est présente dans le menu du jeu. Vous pouvez y accéder en cliquant dessus ou en pressant la touche "\textit{g}" de votre clavier.\\
Cette option permet de renouveler les niveaux du jeu entièrement. La méthode est la suivante:
\begin{itemize}
\item Tous les niveaux du jeu seront supprimés.
\item Des niveaux seront créés grâce au générateur de niveaux (cf. Générateur \ref{gen}).
\item Ses niveaux seront ensuite testés par le solveur pour voir si il peut les résoudre (cf. Solveur \ref{section::solveur}).
\item Si les niveaux sont résolvables par le solveur, alors ils prennent la place des anciens niveaux du jeu.
\end{itemize}
Tous les niveaux du jeux étant recrées, l'opération nécessite un certain temps. Nous vous conseillons donc de n'utiliser cette fonctions qu'en dernier lieu, lorsque vous vous serez lassé des niveaux actuels du jeux.
\newpage
\section{Conception du jeu}
\subsection{Organisation du projet}
Voici la manière dont nous nous sommes réparties les tâches du projet. L'un de nos membres (COUTURE Torkil) nous ayant quitté en cours de projet, nous n'avons pas pu mentionner son nom dans ce tableau.\\
\begin{center}
\begin{tabular}{|c|c|c|c|c|}\hline
Fonctionnalités principales&Affichage&Solveur&Générateur\\ \hline\hline
Ivan& Ivan& Quentin& Arthur \\\hline
\end{tabular}
\end{center}
\subsection{Fonctionnalités}
\subsubsection{Les états du programme}
Pour que lenchaînement des différentes mises en page du sokoban soient cohérentes, j'ai décidé de créer un fichier logic/state.py qui posséderait toutes les informations liées à l'état du programme avec des fonctions permettant de manipuler ces états.\\
Ce programme possède quatre situations possibles :
\begin{figure}[H]
\includegraphics[width=\linewidth]{./Illustrations/state_1.png}
\caption{Principales situations.}
\end{figure}
Ces situations se suivent de manière logique :\\
\begin{figure}[H]
\includegraphics[width=\linewidth]{./Illustrations/state_2.png}
\caption{Suivi des situations.}
\end{figure}
C'est la classe de state.py qui analyse les événements Pygame afin de prendre les bonnes décisions pour modifier l'état du jeu.\\
Ces événements Pygame vont donc modifier des variables python représentant l'état du jeu en fonction de la situation.\\
Celles-ci sont représentées par le schéma suivant :
\begin{figure}[H]
\includegraphics[width=\linewidth, height=3in]{./Illustrations/state_3.png}
\caption{Variables python modifiées selon la situation.}
\end{figure}
Chaque situation écoute des événements Pygame qui leur sont propres.\\
Dans notre jeu, nous écoutons uniquement les touches du clavier et de la souris (en prenant en compte la position pour la souris).
Les événements écoutés sont représentés par le schéma suivant :
\begin{figure}[H]
\includegraphics[width=\linewidth]{./Illustrations/state_4.png}
\caption{Les événements écoutés selon la situation.}
\end{figure}
\newpage
\subsubsection{Le rendu du jeu}
Une fois que l'utilisateur arrive dans la situation de jeu,
la classe de logic/state.py va demander la génération des classes players de logic/player.py.\\
Cette classe player va à son tour demander la création d'une grille spécifique aux joueurs.\\
Dans le programme on a donc une grille indépendante par joueur.\\
Tout d'abord une grille de lettres va être créée à partir d'une génération automatique (cf. La partie d'Arthur Page \ref{gen}) ou d'un fichier .sok.\\
Dans ma partie, je vais vous parler de la création de la grille à partir des fichiers .sok.\\
Voici le contenu d'un fichier .sok :
\begin{figure}[H]
\begin{center}
\includegraphics[width=2.5in, height=2.5in]{./Illustrations/map_1.png}
\end{center}
\caption{Contenu d'un fichier .sok.}
\end{figure}
Celui-ci va être converti en grille de lettres :
\begin{figure}[H]
\begin{center}
\includegraphics[width=5in]{./Illustrations/map_2.png}
\end{center}
\caption{Conversion fichier .sok en grille python.}
\end{figure}
Ensuite chaque lettre de la grille va être convertie en un sprite Pygame qui lui est associé.\\
\begin{figure}[H]
\begin{center}
\includegraphics[width=7in, height=3.7in]{./Illustrations/map_3.png}
\end{center}
\caption{Conversion des lettres de la grille en sprites.}
\end{figure}
On a deux types de sprite Pygame dans le programme :
\begin{itemize}
\item Les sprites superposables [joueur(@), boites(\$), trous(.), air( )]
\item Les sprites non superposables [mur(\#)]
\end{itemize}
\vspace{5mm}
Pour positionner les sprites sur la fenêtre Pygame, nous avons dû effectuer des mesures sur la/les grilles.
\begin{figure}[H]
\begin{center}
\includegraphics[width=6in, height=3.6in]{./Illustrations/map_4.png}
\end{center}
\caption{Mesures effectuées pour chaques sprites de la grille.}
\end{figure}
Une fois que tout est en place le programme situé dans interface/screen.py peut afficher le rendu des sprites à partir de leur positions calculés.
\subsubsection{Mouvement du personnage dans la grille}
Le personnage se déplace uniquement de une case, en verticalement ou horizontalement.
Les mouvements du personnage sont restreints par des règles.
Voici un contexte, représenté par le schéma suivant :
\begin{figure}[H]
\begin{center}
\includegraphics[width=6in, height=3.6in]{./Illustrations/mouv_1.png}
\end{center}
\caption{Les mouvements possibles du personnage dans la grille.}
\end{figure}
La logique liée au mouvement respecte le pseudo-code suivant :
\begin{figure}[H]
\includegraphics[width=\linewidth]{./Illustrations/mouv_2.png}
\caption{Pseudo-code du déplacement du personnage dans la grille.}
\end{figure}
\subsubsection{Solveur}
\label{section::solveur}
Le solveur se découpe en 2 parties: l'algorithme A* qui permet de trouver le chemin le plus court en partant d'un point A pour aller à un point B et la classe solver en elle-même qui va essayer de résoudre le niveau.\\
Dans un premier temps, le solveur essaie de chercher la caisse qui se trouve la plus proche du joueur et va essayer de trouver un chemin vers le point de validation(traits gris) puis on calcule le chemin de la caisse vers le point grâce à lalgorithme A* pour vérifier que la caisse peut effectivement se placer dessus(traits verts).\\
S'il y arrive on va réutiliser l'algorithme A* pour déterminer le chemin que doit parcourir le personnage pour aller jusqu'à la caisse (traits jaunes). On calcule ensuite le chemin que doit parcourir le personnage pour pouvoir pousser la caisse jusqu'au point (traits rouges), et on ajoute l'emplacement de la caisse en mémoire pour que l'algorithme A* soit informé qu'il ne peut plus traverser cet emplacement.
\begin{figure}[H]
\begin{center}
\includegraphics[width=0.6\linewidth]{./Illustrations/solver1.jpg}
\caption{Tout les chemins / calculs effectués par le solveur pour placer la première caisse}
\end{center}
\end{figure}
En effet, lalgorithme ne change pas la disposition du terrain au fur et à mesure de son exécution afin que ce changement se fasse en temps réel, quand le joueur joue et se déplace. Nous stockons donc le terrain de base ainsi que 2 listes de Tuples qui servent à dire à lalgorithme A* qu'il peut traverser une caisse (étant donné que nous ne changeons pas la disposition du terrain s'il bouge la caisse il peut à nouveau se poser sur cet emplacement) et l'autre qui sert au contraire à dire à l'algorithme A* qu'il ne peut plus traverser tel ou tel emplacement.\\
Dès que la caisse se trouve à lemplacement voulu on cherche à nouveau la caisse la plus proche et le point de validation le plus proche de cette caisse (traits gris), on calcule le chemin de la caisse(traits verts). On approche ensuite le joueur de la caisse(trait jaune), on détermine le chemin à suivre grâce à l'algorithme(traits rouges) et on répète ses étapes jusqu'à ce qu'il ne reste plus un seul point de validation valide.
\begin{figure}[H]
\begin{center}
\includegraphics[width=0.6\linewidth]{./Illustrations/solver2.jpg}
\caption{Calculs effectués pour déplacer la seconde caisse}
\end{center}
\end{figure}
\begin{figure}[H]
\begin{algorithmic}
\Function{solveur}{grid, From:Node, Goal:Node}\\
\State $closedList = []$
\State $openList = []$ \Comment{File prioritaire}
\State $came\_from = \{\}$
\State $openList.add(From)$
\While{openList is not empty}
\State $current\_node = openList.popTHeSmallestItem()$
\If {$current\_node['x'] == NoeudArrive['x'] $ }
\State \Return constructPath()
\EndIf
\ForEach{$current\_node\: neighbor \in grid$}
\If{$not(neighbor\in closedList\:\|\:(neighbor \in openList\:\&\:neighbor.cost \leq getNeighborFromOpenList().cost))$}
\State $neighbor.cost = current_node[''cost''] + 1$
\State $neighbor.heuristic = neighbor.cost * (|neighbor.x - Goal.x| + |neighbor.y - Goal.y|)$
\State $current\_node.append(neighbor)$
\EndIf
\EndFor
\State $closedList.append(current_node)$
\EndWhile
\State Raise Exception
\EndFunction
\end{algorithmic}
\caption{Algorithme A*}
\end{figure}
% https://en.wikibooks.org/wiki/LaTeX/Algorithms#Typesetting_using_the_algorithmicx_package
\subsubsection{Générateur}
\label{gen}
Pour éviter de rendre le jeu lassant et augmenter sa durée de vie, il nous est venu comme idée d'implémenter un générateur de niveaux. Il sera expliqué dans cette section le fonctionnement du générateur ainsi que ses particularités. \\
Le générateur peut se configurer de plusieurs manières. On peut ainsi choisir un nombre de caisses maximum et un nombre de caisses minimum, ainsi que la taille de la grille et la difficulté. Nous allons donc voir étape par étape en quoi chacun de ses paramètres intervient dans la génération d'une grille.\\
On notera que pour des raisons de simplicité, toutes les grilles générées sont des grilles carrées.\\
Voici comment le générateur construit une nouvelle grille:
\begin{enumerate}
\item Création de la grille de base.
\item Remplissage de la grille.
\item Vérifications.
\end{enumerate}
\paragraph{Création de la grille de base}
\paragraph{}
Le générateur créé tout d'abord une grille carrée de la taille demandée. Il fait en sorte que cette grille soit seulement constituée de murs en faisant le tour; le reste de la grille étant vide.\\
Il initialise aussi trois dictionnaires dans lesquels seront stockées les positions du joueurs, des caisses et des trous.
\paragraph{Remplissage}
\paragraph{}
Une fois cette grille créée, le générateur doit la remplir. Il affecte à des variables le nombre de caisses et de trous qu'il doit placer. Une variable contenant le nombre de murs est aussi créée, elle contient un nombre correspondant au à la moitié de la taille de la grille multiplié par la difficulté. Plus un niveau est difficile, plus il contient donc de murs.\\
Le générateur remplit ensuite la grille en excluant le deux premières lignes (\textbf{resp.} colonnes) de chaque colonnes (\textbf{resp.} colonnes). L'exclusion de ses cases permet d'éviter que les caisses ne se retrouvent accolées aux murs extérieurs de la grille (ce qui rendrait la génération des trous plus contraignante).\\
Le générateur utilise la fonction "\textit{randrange}" du module "\textit{random}" de Python pour choisir au hasard où il doit placer une caisse un mur ou un trou. Si la case choisie contient déjà un objet, le générateur en choisit une autre, et continue de choisir au hasard des cases jusqu'à ce que tous les objets soient placés.\\
Le générateur place dans cet ordre, le joueur, puis les murs, suivis des caisses et en dernier les trous.\\
Une fois placé, un objet voit sa position ajoutée dans son dictionnaire correspondant pour faciliter l'étape de vérification.
On obtient donc un algorithme de ce genre pour un objet :\\
\begin{algorithmic}
\While{$nombre d' objet \neq 0$}
\State $x\gets randrange(2,tailledelagrille -3)$
\State $y\gets randrange(2,tailledelagrille -3)$
\If {$grille[x][y] = empty$}
\State $grille[x][y] \gets objet$
\State $nombred'objet \gets nombred'objet - 1 $
\State $DicObjet[objet] \gets [x,y]$
\EndIf
\EndWhile
\end{algorithmic}
\paragraph{Vérification}
Une fois tous les objets placés sur le grille, le générateur procède à plusieurs vérifications pour voir si la grille est conforme à certains critères. Ainsi si une case vide est entourée de murs, elle sera transformée en mur.\\
D'autres test sont rédhibitoires. Si ils ne sont pas valides, le générateur recommence à générer une grille depuis le début (retour à la création de la grille de base).\\
On distingue trois de ces tests:
\begin{itemize}
\item Si une caisse est entourée par plus de deux murs.
\item Si il n'y a pas autant de caisses que de trous.
\item Si il n'y a pas exactement un joueur.
\item Si il y a moins de boîtes que demandé en appel de la classe.
\end{itemize}
Pour rendre l'exécution des vérifications plus rapide, les dictionnaires contenant les positions des objets de la grille sont utilisés, pour éviter de devoir parcourir la grille afin de les trouver.\\
Si tous ces tests renvoient la valeur "\textit{True}", alors la grille est considérée comme valide et la classe \textit{Generate} la retourne.
\begin{figure}[H]
\center
\includegraphics[scale = 0.90]{./Illustrations/generate.png}
\caption{Fonctionnement de la classe Generate.}
\end{figure}
\newpage
\section{Éléments techniques}
\subsection{Structure de données}
Explication de l'organisation des dossiers :
\begin{itemize}
\item{Config}
\begin{itemize}
\item{Contient les constantes utile au fonctionnement du jeu.}
\item{Le chemin absolu du répertoire du dossier parent afin de pouvoir récupérer facilement les fichiers .sok qui contiennent les schémas des labyrinthes.}
\item{Le titre et les dimensions de la fenêtre.}
\item{L'identifiant de notre Event pygame personnalisé.}
\end{itemize}
\item{Images}
\begin{itemize}
\item{Contient les sprites affichés à l'écran.}
\end{itemize}
\item{interface}
\begin{itemize}
\item{Contient la logique de la répartition des sprites ou des textes affichés à l'écran.}
\item{La logique du menu de sélection des niveaux.}
\end{itemize}
\item{levels}
\begin{itemize}
\item{Contient les fichiers .sok qui possèdent les schémas des labyrinthes du jeu.}
\end{itemize}
\item{logic}
\begin{itemize}
\item{Contient la logique de démarrage du programme.}
\item{Contient la logique du bon enchaînement des processus entre le menu et le jeu en lui-même, en tenant compte du type de l'adversaire dans le jeu.}
\item{Contient la logique du jeu en lui-même. Les déplacements possibles et la réussite du niveau.}
\end{itemize}
\item{map}
\begin{itemize}
\item{Contient les différents sprites (sous-classes de Pygame) qui sont affichées dans le jeu. Il y a une distinction entre les sprites qui peuvent être en mouvement (les boites) et les sprites statiques (les murs).}
\item{Contient la logique de la création de la grille de jeu.}
\end{itemize}
\item{Solver}
\begin{itemize}
\item{Contient la logique de l'algorithme A*.}
\item{Contient la logique de l'intelligence artificielle du jeu shokoban.}
\end{itemize}
\item{Generator}
\begin{itemize}
\item{Contient le générateur de niveaux aléatoire.}
\item{Contient le vérificateur permettant de valider ou non une grille générée.}
\end{itemize}
\end{itemize}
L'ensemble des fichiers et leurs relations : Voir la figure \ref{fig::graph}
% graph
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth, height=2in]{Illustrations/graphJetBrains.png}
\caption{Graphique de structure de données}
\label{fig::graph}
\end{figure}
\subsection{Exécution}
\begin{figure}[H]
\includegraphics[width=\linewidth]{./Illustrations/execution.png}
\caption{Croquis de l'exécution du programme.}
\end{figure}
\subsection{Bibliothèques utilisées}
Voici la liste des bibliothèques externes utilisés :
\begin{itemize}
\item{heapq}
\begin{itemize}
\item{Permet de créer une file prioritaire pour l'algorithme A*}
\end{itemize}
\item{Pygame}
\begin{itemize}
\item{Moteur graphique utilisé dans le projet.}
\end{itemize}
\end{itemize}
\newpage
\section{Expérimentations}
\subsection{Mesures de performances}
Nous avons utilisé plusieurs outils pour mesurer les performances de notre programme, dans un premier temps, nous avons vérifié, quelles fonctions demandent le plus de temps de calcul grâce à un outil intégré à pyCharm.\\
Ce graphique (figure \ref{fig::graph}) nous permet de voir que la fonction keep\_fps (interface/screen.py) nous prend beaucoup de temps de calcul. Nous avions décidé de la supprimer mais cela faisait monter énormément l'utilisation processeur par notre programme(voir figure \ref{figure::forteconso}).
\begin{figure}[H]
\centering
\includegraphics{Illustrations/gestionnaire_tache_forte_conso.png}
\caption{Utilisation du cœur après suppression de keep\_fps}
\label{figure::forteconso}
\end{figure}
Les mesures de performances ont été effectuées en obligeant le programme à utiliser un cœur précis (le numéro 7 par exemple) du processeur afin d'être le plus précis possible.\\
Cela demandait 60\% à 70\% des ressources d'un cœur ayant une vitesse de base de 3.6 Ghz.\\
Nous avons donc réintégré cette fonction à notre programme, et il est maintenant compliqué de constater une différence d'utilisation du processeur au moment où on lance le programme. En effet , le but de keep\_fps était de bloquer de nombre d'images par secondes à 20, il est donc normal que son temps dexécution soit le plus long du programme. Au niveau de l'utilisation de la mémoire vive, le programme ne dépasse pas les 35 Mo.
Par conséquent, tout pc pouvant afficher un bureau est capable de lancer de manière fluide Sokoban.
\subsection{Difficultés rencontrés}
Le générateur de niveau et le solveur furent les parties les plus compliquées à réaliser.
Pour le générateur, il ne s'agit pas de placer des éléments de décor au hasard car il faut que chaque niveau soit réalisable que ce soit par le joueur que par le solveur ainsi que le personnage ou les caisses n'apparaissent pas dans un endroit ou ils seraient bloqués. Pour plus d'informations sur son fonctionnement, referez-vous à la partie dédiée: (\ref{gen}).
Concernant le solveur, il fallait trouver une solution complète sans changer la disposition du niveau, nous vous en parlons en détail dans la partie \ref{section::solveur}.
\newpage
\section{Conclusion}
\subsection{Récapitulatif}
Le jeu terminé, nous étions plutôt contents de nous. Nous avons en effet réussi à atteindre l'objectif fixé, mis à part quelques petits inconvénients (cf \ref{ameliorations}. Nous avons en effet réussi à produire un jeu totalement fonctionnel. Il est non seulement possible de jouer à deux mais aussi de jouer contre son propre ordinateur. Certaines fonctionnalités telles que le comptage des points ou bien la sélection possible de plusieurs niveaux a aussi permis d'ajouter beaucoup de contenu au jeu.Bien que lent pour la génération de certains niveaux aléatoires, le jeu tourne très bien. Un ordinateur peu puissant est en mesure de l'exécuter sans problèmes de performances. L'objectif principal a donc été largement atteint puisque le jeu est parfaitement fonctionnel et que différentes fonctionnalités supplémentaires on pu être ajoutées. Ce fut donc en somme un très bonne expérience qui nous a permis à tous de mettre nos connaissances en commun, et de progresser aussi bien individuellement que collectivement.
\subsection{Améliorations possibles}
\label{Améliorations}
Nous allons répertorier ici les améliorations auxquelles nous avons pensé mais qui n'ont pas pu se réaliser, notamment par manque de moyens.
\begin{itemize}
\item Un solveur plus efficace.\\
Le solveur que nous avons conçu prend en charge un certain nombre de niveaux mais ne peut pas tous les résoudre. Nous avons fait en sorte que tous les niveaux du jeux soient résolvables pour pallier à ce problème. Malgré tout, il aurait été appréciable que le solveur puisse être en mesure de résoudre n'importe quel niveau qui lui soit présenté. De plus il est possible sur certains niveaux (comme le niveau 2) que le joueur réussisse à faire moins de déplacement que le solveur, il y a donc quelques améliorations possibles.
\item Un générateur plus rapide\\
Le générateur quand à lui peut générer n'importe quelle grille jouable en suivant certains paramètres. Le problème réside dans le temps qu'il lui faut pour arriver à créer une grille jouable. On pourrait éviter ce problème en générant une grille suivant des patterns (motif prédéfinis de grille). Ainsi une grille se générerait plus rapidement mais ne serait plus vraiment aléatoire puisque le fait d'ajouter des patterns sous-entend que ceux-ci on déjà été défini par avance. De plus, on pourrait se retrouver en présence de grilles lassantes puisqu'un même pattern pourrait apparaître plusieurs fois. Une connaissance plus poussée de Python aurait donc sûrement pu permettre d'optimiser le générateur.
\end{itemize}
\appendix
\end{document}

0
config/__init__.py Normal file
View File

14
config/constants.py Normal file
View File

@ -0,0 +1,14 @@
import os
import pygame
"""Give the absolute path of the directory"""
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# pygame config
FPS = 20
TITLE_WINDOW = "Sokoban"
SIDE_WINDOW = 550
PRINTER_SURFACE_HEIGHT = 45
COMPUTER_EVENT = pygame.USEREVENT + 1

BIN
images/box.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
images/dark_box.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

BIN
images/dot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
images/floor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

BIN
images/player.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
images/wall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

0
interface/__init__.py Normal file
View File

13
interface/goback.py Normal file
View File

@ -0,0 +1,13 @@
import pygame
class GoBack(pygame.sprite.Sprite):
"""Create a sprite that will allow the user to get back in the menu"""
def __init__(self, x, y):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((60, 20))
pygame.draw.rect(self.image, pygame.Color(255, 255, 255, 255), pygame.Rect(20, 5, 40, 10))
pygame.draw.polygon(self.image, pygame.Color(255, 255, 255, 255), [(0, 10), (20, 0), (20, 20)])
self.rect = self.image.get_rect()
self.rect = self.rect.move(x, y)

41
interface/menu.py Normal file
View File

@ -0,0 +1,41 @@
import typing
import interface.screen
class Menu:
def __init__(self):
self.screen: typing.Type[interface.screen.Screen]
self.difficulty = None
self.map_file = None
def get_difficulty(self):
"""Enable to show and choose a level difficulty"""
self.screen.print_sentence_on_display(
"Welcome to Sokoban !\nPlease select the game difficulty:\nB : Beginner\nC : Casual \nE : Elite\nA : Random\nG : Generate levels")
def get_versus_mode(self):
"""Enable to choose your game mode (versus the computer or versus another player"""
self.screen.print_sentence_on_display(
"Choose your versus mode :\nA : Versus Computer\nB : Versus Player")
def ask_exit(self):
"""A confirmation to close the program"""
self.screen.print_sentence_on_display("Close the program ? (0/1) ")
def get_map_file(self, difficulty: typing.Optional[str]):
"""Verify if the difficulty selected exists and then, depending on the difficulty selected, lead to the level selection"""
if difficulty not in ("b", "c", "e", "a"):
raise Exception("Unknown difficulty : " + difficulty)
if difficulty == "b":
self.screen.print_sentence_on_display("A newbie !\n Now select your level :"
" \n1 : level 1 \n2 : level 2\n3 : level 3\n")
if difficulty == "c":
self.screen.print_sentence_on_display(
"So that's not your first game....\n Now select your level :"
" \n 4 : level 4 \n 5 : level 5\n 6 : level 6")
if difficulty == "e":
self.screen.print_sentence_on_display("Hell begins !\n Now select your level :"
" \n7 : level 7 \n8 : level 8\n9 : level 9")
if difficulty == "a":
self.screen.print_sentence_on_display("Hell begins !\nChoose the number of box.\nEnter a number\nbeetween 1 and 9")

252
interface/screen.py Normal file
View File

@ -0,0 +1,252 @@
import typing
import pygame
import pygame.locals as pg_var
import config.constants
import interface.goback
import interface.texte
import logic.state
class Screen:
_init = False
state = logic.state.State
def __init__(self):
self.clock = None
self.display = None
self.printer_surface = None
self.printer_left = None
self.printer_right = None
self.surface_game = None
self.font = None
self.textes = pygame.sprite.Group()
def init(self, ratio_x=1):
"""Inintialize the game window, creating areas were we can print
content. The font is also set"""
if not self._init:
self.clock = pygame.time.Clock()
# initialization of the main display
width_display = config.constants.SIDE_WINDOW * ratio_x
self.display = pygame.display.set_mode(
(width_display,
config.constants.SIDE_WINDOW + config.constants.PRINTER_SURFACE_HEIGHT * 2))
pygame.display.set_caption(config.constants.TITLE_WINDOW)
# initialization of the subwindows with its width and height
self.printer_surface = pygame.Surface((width_display, config.constants.PRINTER_SURFACE_HEIGHT))
self.printer_left = pygame.Surface((width_display // 2, config.constants.PRINTER_SURFACE_HEIGHT))
self.printer_right = pygame.Surface((width_display // 2, config.constants.PRINTER_SURFACE_HEIGHT))
self.surface_game = pygame.Surface((width_display, config.constants.SIDE_WINDOW))
self.font = pygame.font.SysFont("arial", int(config.constants.SIDE_WINDOW * 0.04))
pygame.key.set_repeat(400,100)
def blit_surface_game(self):
"""Set the position of the game surface created above"""
self.display.blit(self.surface_game, (0, config.constants.PRINTER_SURFACE_HEIGHT * 2))
def blit_printer_surface(self):
"""Set the position of the printer surface (were we show informations like the number of strokes)
created above"""
self.display.blit(self.printer_surface, (0, 0))
def blit_printer_left(self):
"""Set the position of the left display surface created above"""
self.display.blit(self.printer_left, (0, config.constants.PRINTER_SURFACE_HEIGHT))
def blit_printer_right(self):
"""Set the position of the right display surface created above"""
self.display.blit(self.printer_right, (config.constants.SIDE_WINDOW, config.constants.PRINTER_SURFACE_HEIGHT))
def refresh(self):
"""Refresh the screen"""
pygame.display.flip()
def close(self):
"""Enable to close the window"""
pygame.key.set_repeat(0, 0)
pygame.display.quit()
def wait(self, ms):
"""Freeze the screen for a certain duration"""
pygame.time.wait(ms)
def start(self):
"""Enable to start the game"""
pygame.init()
def stop(self):
"""Enable to end the game"""
pygame.quit()
def clean_display(self):
"""Fill the display with black (clean it)"""
self.display.fill(pg_var.color.THECOLORS['black'])
def clean_printer_surface(self):
"""Fill the printer surface with black (clean it)"""
self.printer_surface.fill(pg_var.color.THECOLORS['black'])
def clean_printer_left(self):
"""Fill the printer left surface with black (clean it)"""
self.printer_left.fill(pg_var.color.THECOLORS['black'])
def clean_printer_right(self):
"""Fill the printer right surface with black (clean it)"""
self.printer_right.fill(pg_var.color.THECOLORS['black'])
def clean_surface_game(self):
"""Fill the game surface with black (clean it)"""
self.surface_game.fill(pg_var.color.THECOLORS['black'])
def draw_sprites_on_surface_game(self, sprites_group):
"""Enable to show a group of sprites on the screen"""
for sprite in sprites_group.sprites():
sprite.draw(self.surface_game)
self.blit_surface_game()
self.refresh()
def print_sentence_on_display(self, question: str, coord_x: typing.Optional[int] = None,
coord_y: typing.Optional[int] = None):
"""Enable to print a sentence on the display surface considering some coordinates"""
self.textes.empty()
for line in question.splitlines():
if coord_x is not None and coord_y is not None:
params = (coord_x, coord_y)
else:
params = (self.display.get_rect().centerx // 2, self.display.get_rect().centery // 2)
text = interface.texte.Texte(self.font, line, *params)
self.textes.add(text)
for i, text in enumerate(self.textes.sprites()):
text.dy = 30 * i
text.update()
text.dy = 0
self.display.blit(text.image, text.rect)
def print_sentence_on_surface_printer(self, sentence: str, coord_x: typing.Optional[int] = None,
coord_y: typing.Optional[int] = None):
"""Enable to print a sentence on the printer surface considering some coordinates"""
self.print_sentence_on_printer(self.printer_surface, sentence, coord_x, coord_y)
self.blit_printer_surface()
def print_sentence_on_printer_left(self, sentence: str, coord_x: typing.Optional[int] = None,
coord_y: typing.Optional[int] = None):
"""Enable to print a sentence on the left printer surface considering some coordinates"""
self.print_sentence_on_printer(self.printer_left, sentence, coord_x, coord_y)
self.blit_printer_left()
def print_sentence_on_printer_right(self, sentence: str, coord_x: typing.Optional[int] = None,
coord_y: typing.Optional[int] = None):
"""Enable to print a sentence on the right printer surface considering some coordinates"""
self.print_sentence_on_printer(self.printer_right, sentence, coord_x, coord_y)
self.blit_printer_right()
def print_sentence_on_printer(self, surface, sentence: str, coord_x: typing.Optional[int] = None,
coord_y: typing.Optional[int] = None):
"""Enable to print a sentence on the printer surface considering some coordinates"""
self.textes.empty()
if coord_x is not None and coord_y is not None:
params = (coord_x, coord_y)
else:
params = (surface.get_rect().centerx // 2, surface.get_rect().centery // 2)
text = interface.texte.Texte(self.font, sentence, *params)
self.textes.add(text)
for i, text in enumerate(self.textes.sprites()):
text.dy = 30 * i
text.update()
text.dy = 0
surface.blit(text.image, text.rect)
def add_goback(self):
"""Print the goback arrow on the screen that allows the player to go back in the menu
(this arrow is a sprite already created in interface/goback.Goback)"""
goback = interface.goback.GoBack(100, 100)
self.textes.add(goback)
self.display.blit(goback.image, goback.rect)
def print_loading(self):
"""Print a "loading" message in the display surface when the game is loading (using
the 'print_sentence_on_display' function"""
self.print_sentence_on_display("Loading...")
self.refresh()
def ask_command(self, menu):
"""Clean the screen and move in the menu depending on the program state (see "state.py")"""
if self.state.situations[self.state.situation] == "select_difficult":
self.clean_display()
menu.get_difficulty()
self.add_goback()
elif self.state.situations[self.state.situation] == "select_level":
self.clean_display()
menu.get_map_file(self.state.difficulty)
self.add_goback()
elif self.state.situations[self.state.situation] == "select_versus_mode":
self.clean_display()
menu.get_versus_mode()
self.add_goback()
elif self.state.situations[self.state.situation] == "game":
pass
self.refresh()
def analize_state(self):
"""Print a message depending on the program state (see "state.py")"""
if self.state.situations[self.state.situation] == "select_versus_mode" and self.state.next_state:
self.close()
self.init(2)
for player in self.state.players:
self.draw_sprites_on_surface_game(player.entity_mapper.sprites)
self.refresh()
elif self.state.situations[self.state.situation] == "game":
self.game_traitment()
if self.state.next_state:
for player in self.state.players:
self.draw_sprites_on_surface_game(player.entity_mapper.sprites)
if self.state.players[0].blow_counter > self.state.players[1].blow_counter:
sentence_win = "Player 2 has win."
if self.state.versus == "a":
sentence_win = "Computer has win."
elif self.state.players[1].blow_counter > self.state.players[0].blow_counter:
sentence_win = "Player 1 has win."
else:
sentence_win = "Equality"
self.print_sentence_on_surface_printer(sentence_win)
self.refresh()
self.wait(2000)
if self.state.next_state or self.state.state_go_back:
self.close()
self.init(1)
def game_traitment(self):
"""Print on the screen the numbers of strokes and the "impossible movement" message
if the movement the player want to do is impossible"""
for player in self.state.players:
if player.wanted_move and player.had_moved or player.restarted:
if player.entity_mapper.position == 0:
self.clean_printer_left()
self.print_sentence_on_printer_left(str(player.blow_counter) + " stroke(s)", 80, 0)
else:
self.clean_printer_right()
self.print_sentence_on_printer_right(str(player.blow_counter) + " stroke(s)", 80, 0)
elif player.wanted_move and not player.had_moved:
if player.entity_mapper.position == 0:
self.print_sentence_on_printer_left("impossible movement", 240, 0)
else:
self.print_sentence_on_printer_right("impossible movement", 240, 0)
self.draw_sprites_on_surface_game(player.entity_mapper.sprites)
self.refresh()
def keep_fps(self):
"""A limit to force the computer staying at a refresh rate <= 20 fps"""
self.clock.tick(config.constants.FPS)

18
interface/texte.py Normal file
View File

@ -0,0 +1,18 @@
import pygame
class Texte(pygame.sprite.Sprite):
def __init__(self, font, texte, x, y):
super(Texte, self).__init__()
self.texte = texte
self.font = font
self.image = self.font.render(texte, True, pygame.color.THECOLORS['white'])
self.rect = self.image.get_rect()
self.rect = self.rect.move(x, y)
self.dx = 0
self.dy = 0
def update(self):
"""Update the text"""
self.rect = self.rect.move(self.dx, self.dy)

8
levels/level1.sok Normal file
View File

@ -0,0 +1,8 @@
########
# ###
# # . #
# @ . #
# $$ #
## #
# #
########

9
levels/level2.sok Normal file
View File

@ -0,0 +1,9 @@
#########
# ###
# # ##
# @$. # #
# .$ #
# $. #
# ##
## ###
#########

10
levels/level3.sok Normal file
View File

@ -0,0 +1,10 @@
##########
## # ##
# # . #
# $ #
# .$$ #
# @ . ##
# $. #
# # ###
# #
##########

15
levels/level4.sok Normal file
View File

@ -0,0 +1,15 @@
###############
# # #
# # @ # ##
# #
# # $. #
# . #
# # $ # #
# . . #
# $$ #
# # . #
# . # #
# # $$ #
## # ##
# # #
###############

17
levels/level5.sok Normal file
View File

@ -0,0 +1,17 @@
#################
### #
## #
# # #
# $ . ##
# #
# .$ # #
# # ## . #
# $ #
# @ . #
# # $ $ #
## . $ #
# # #
# .. $ # #
# # # #
# # #
#################

18
levels/level6.sok Normal file
View File

@ -0,0 +1,18 @@
##################
# #
# . #
# . #
# . #
# $ $ ##
## . . # #
# $ @ #
# #
# . $ #
## $ $ ##
# $ . # #
# . #
# $ $. #
# #
# # #
# # # #
##################

20
levels/level7.sok Normal file
View File

@ -0,0 +1,20 @@
####################
# # ## #
# #
# ## #
## # # #
# @ #
# . $ . #
# # # #
# # . #
# # #
# #
# #
# $ $ . # #
# $ . #
# $ . # #
## $ #
# # #
# # # #
# # #
####################

25
levels/level8.sok Normal file
View File

@ -0,0 +1,25 @@
#########################
# # #
# . #
# . # . #
# @ # $ # #
# # #
# # #
# # $ # #
# # $ $ #
# # # $ . #
# # $ #
# # . #
# # #
# # # #
# $ #
# # # #
# # #
# # # #
# . # ##
# . # # #
# # # #
# # # # #
# # # # # #
# # #
#########################

30
levels/level9.sok Normal file
View File

@ -0,0 +1,30 @@
##############################
# #
# . # # #
# # # #
# # # # #
# $ #
# # #
# # # #
# # $ #
# #
# # # $ #
## # $@ # #
# . # ##
# # #
# . # #
# # # # $ #
# # # #
# # . . # #
## # # # #
# # ## ##
# # # #
## # # ##
# # $ #
# ### #
# . . # # #
# # # ##
## $ # #
# # # # #
# # # #
##############################

0
logic/__init__.py Normal file
View File

35
logic/context.py Normal file
View File

@ -0,0 +1,35 @@
import typing
import interface.screen
import logic.listener
class Context:
screen: interface.screen.Screen
listener: typing.Type[logic.listener.Listener]
type: str
pygame_launched: bool = False
@classmethod
def get_context(cls):
"""Get the actual context (what's in the screen, in the listener) and store it in the 'Context' class"""
cls.screen = interface.screen.Screen()
cls.screen.start()
cls.screen.init()
cls.listener = logic.listener.Listener
cls.type = "pygame"
@classmethod
def leave_context(cls):
"""Leave the actual context"""
try:
if cls.type == "pygame":
cls.listener.clean()
cls.screen.close()
if cls.pygame_launched:
cls.screen.stop()
cls.pygame_launched = False
except KeyError:
pass
return None

58
logic/game.py Normal file
View File

@ -0,0 +1,58 @@
import interface.menu
import logic.context
import logic.state
import logic.generate
import logic.player
class Game:
state = logic.state.State
def __init__(self):
self.menu = interface.menu.Menu()
self.context = logic.context.Context
def config_context(self):
"Set the context"""
self.context.get_context()
self.menu.screen = self.context.screen
def start(self):
"""This is the function that start the game"""
game_loop = True
self.config_context()
while game_loop:
if self.state.state_go_back:
self.state.go_back()
elif self.state.next_state:
self.state.leave_situation(1)
self.state.next_state = False
elif self.state.quit:
game_loop = False
else:
self.context.screen.ask_command(self.menu)
events = self.context.listener.listen_event()
self.state.analizer(events, self.context.screen.textes)
loading_conditions = self.state.situations[self.state.situation] == "select_difficult" and self.state.do_generate_levels
loading_conditions = loading_conditions or self.state.situations[self.state.situation] == "select_versus_mode"
if loading_conditions:
if self.state.do_generate_levels or self.state.versus is not None:
self.context.screen.clean_display()
self.context.screen.print_loading()
self.context.screen.refresh()
if self.state.do_generate_levels:
self.state.generate_levels()
if self.state.versus is not None:
self.state.select_game_traitment()
if not self.state.quit:
self.context.screen.analize_state()
self.context.screen.keep_fps()
self.context.leave_context()

196
logic/generate.py Normal file
View File

@ -0,0 +1,196 @@
from random import randrange as r
import map.air
import map.box
import map.dot
import map.player
import map.wall
class Generate:
"""Declaring variables that will be used several times in the Generate class"""
grid = []
coordb = {}
coordh = {}
coordp = {}
size = 0
@classmethod
def base(cls,minsize,maxsize):
"""Create an empty grid"""
cls.grid = []
cls.coordb = {}
cls.coordh = {}
cls.coordp = {}
cls.size = r(minsize,maxsize+1)
for i in range(cls.size):
cls.grid.append([])
for i in range(cls.size):
cls.grid[0].insert(i,map.wall.Wall._character)
cls.grid[cls.size-1].insert(i,map.wall.Wall._character)
for i in range(1,cls.size-1):
for z in range(1,cls.size-1):
cls.grid[i].insert(z,map.air.Air._character)
for i in range(1,cls.size-1):
cls.grid[i].insert(0,map.wall.Wall._character)
cls.grid[i].insert(cls.size-1,map.wall.Wall._character)
@classmethod
def fill(cls,difficulty,minb,maxb):
"""Fill the empty grid created above with randoms probabilities"""
holes = 0
boxes = r(minb,maxb+1)
size = len(cls.grid)
x = r(2,size-3)
y = r(2,size-3)
cls.grid[x][y] = map.player.Player._character
cls.coordp["player"] = [x,y]
if size%2 == 0:
wall = size/2
else:
wall = (size-1)/2
wall = wall*difficulty
while wall != 0:
x = r(1,size-1)
y = r(1,size-1)
if cls.grid[x][y] != map.player.Player._character:
cls.grid[x][y] = map.wall.Wall._character
wall -= 1
cpt = 0
while boxes != 0:
x = r(2,size-3)
y = r(2,size-3)
if cls.grid[x][y] != map.player.Player._character and cls.grid[x][y] != map.wall.Wall._character:
cls.grid[x][y] = map.box.Box._character
cpt +=1
cls.dicFill(x,y,cpt,2)
boxes -= 1
holes += 1
cpt = 0
while holes != 0:
x = r(2,size-3)
y = r(2,size-3)
if cls.grid[x][y] != map.player.Player._character and cls.grid[x][y] != map.wall.Wall._character and cls.grid[x][y] != map.box.Box._character:
cls.grid[x][y] = map.dot.Dot._character
cpt += 1
cls.dicFill(x,y,cpt,3)
holes -= 1
@classmethod
def dicFill(cls,x,y,cpt,a):
"""Fill the boxes and holes dictionnaries (easier to get their positions after)"""
if a == 2:
box = 'box'+str(cpt)
cls.coordb[box] = [x,y]
if a == 3:
hole = 'hole'+str(cpt)
cls.coordh[hole] = [x,y]
@classmethod
def verify(cls,minb):
"""Check if the grid is playable"""
cptb = 0
cpth = 0
cptp = 0
for i in range(len(cls.grid)):
for y in range(len(cls.grid)):
if cls.grid[i][y] == map.box.Box._character:
cptb += 1
if cls.grid[i][y] == map.dot.Dot._character:
cpth += 1
if cls.grid[i][y] == map.player.Player._character:
cptp += 1
if cptp != 1:
return False
if cpth != cptb:
return False
if minb > cptb:
return False
if cls.verifAround(map.wall.Wall._character,cls.coordb) == False:
return False
if cls.verifAround(map.wall.Wall._character,cls.coordb,2) == False:
return False
if cls.verifAround(map.wall.Wall._character,cls.coordh) == False:
return False
if cls.verifAround(map.wall.Wall._character,cls.coordp) == False:
return False
cls.verifAir()
return True
@classmethod
def verifAir(cls):
"""Check if there's an air place surrounded by walls"""
for x in range(len(cls.grid)):
for y in range(len(cls.grid[x])):
if cls.grid[x][y] == map.air.Air._character:
cpt = 0
if cls.grid[x][y+1] == map.wall.Wall._character:
cpt += 1
if cls.grid[x][y-1] == map.wall.Wall._character:
cpt += 1
if cls.grid[x+1][y] == map.wall.Wall._character:
cpt += 1
if cls.grid[x-1][y] == map.wall.Wall._character:
cpt += 1
if cpt == 4:
cls.grid[x][y] = map.wall.Wall._character
@classmethod
def verifAround(cls,char,d,rng=1):
"""Check if a box or a hole is surrounded by walls"""
cpt = 0
for i in d.values():
x = i[0]
y = i[1]
if cls.grid[x][y+rng] == char:
cpt += 1
if cls.grid[x][y-rng] == char:
cpt += 1
if cls.grid[x+rng][y] == char:
cpt += 1
if cls.grid[x+rng][y+rng] == char:
cpt += 1
if cls.grid[x+rng][y-rng] == char:
cpt += 1
if cls.grid[x-rng][y] == char:
cpt += 1
if cls.grid[x-rng][y+rng] == char:
cpt += 1
if cls.grid[x-rng][y-rng] == char:
cpt += 1
if cpt > 1 :
return False
return True
@classmethod
def gridGen(cls,minsize,maxsize,minb,maxb,difficulty):
"""The main function of the class which return a random generated grid"""
c = False
while c == False:
cls.base(minsize,maxsize)
cls.fill(difficulty,minb,maxb)
c = cls.verify(minb)
return cls.grid

58
logic/keyboardTools.py Normal file
View File

@ -0,0 +1,58 @@
import typing
class KeyboardTools:
@classmethod
def check_integer_keyboard(cls, key: str):
"""Return the integer value of a keyboard key or none if the kay has no integer value"""
try:
return int(key[-1])
except ValueError:
return None
except IndexError:
return None
@classmethod
def get_str_from_keyboard_event(cls, key):
"""Return the key value as a string, if the key value is a number, the number is converted into a
string before being returned"""
number = cls.check_integer_keyboard(key)
key_name = key.split("_")[1]
if number is None:
return key_name
else:
return str(number)
@classmethod
def get_keys_event_from_choices(cls, choices: typing.List[str]):
"""Raise an error if the list 'choices' is not full of string types objects. This function
also get all the caracthers in the list lower (no capital letters are returned). Then it return the list """
choices = choices[:]
i = 0
while i < len(choices):
if isinstance(choices[i], str):
if len(choices[i]) == 1:
choices[i] = "K_" + choices[i].lower()
else:
choices[i] = "K_" + choices[i].upper()
else:
raise ValueError("The choices must be a list of str.")
i += 1
return choices
@classmethod
def get_keys_event_from_range(cls, start: int, end: int):
"""Create a keys list with a length higher than the 'start' variable and lower than
the 'end' variable"""
if not 0 <= start <= end <= 9:
raise ValueError(
"start and end have to respect : 0 <= start <= end <= 9.")
keys = []
for i in range(start, end + 1):
keys.append("K_KP" + str(i))
keys.append("K_" + str(i))
return keys

27
logic/listener.py Normal file
View File

@ -0,0 +1,27 @@
import typing
import pygame
class Listener:
azerty_conversion = {"K_w": ["K_z"], "K_z": ["K_w"], "K_a": ["K_q"], "K_q": ["K_a"]}
@classmethod
def clean(cls):
"""Clean the pygame's event list"""
pygame.event.clear()
@classmethod
def listen_event(cls):
"""Create a list name 'events' and fill it with all the keys the user type during the game"""
events = []
for event in pygame.event.get():
if hasattr(event, "key"):
for azerty_key in cls.azerty_conversion.keys():
if event.key == getattr(pygame, azerty_key):
event.key = getattr(pygame, cls.azerty_conversion[azerty_key][0], None)
break
events.append(event)
return events

85
logic/mover.py Normal file
View File

@ -0,0 +1,85 @@
import typing
import map.box as boxfile
import map.mapper
import map.moveable
import map.player as playerfile
import map.superposeable as superposeablefile
class Mover:
@classmethod
def move_player(cls, player, direction: map.moveable.DIRECTION):
"""allows to add the new place of the player"""
coords_player = player.position
grid = player.entity_mapper.grid_of_sprites
mapper = player.entity_mapper.mapper
entity_coords = grid[coords_player[0]][coords_player[1]]
player_instance = cls.get_instance_from_coords(entity_coords, playerfile.Player)
new_coords_player = player_instance.get_new_coords(direction, coords_player)
if mapper.coords_in_map(new_coords_player):
entity_new_coords = grid[new_coords_player[0]][new_coords_player[1]]
if issubclass(entity_new_coords.__class__, superposeablefile.Superposeable):
try:
if not (cls.move_box(mapper, grid, new_coords_player, direction)):
return False
except AttributeError:
pass
entity_new_coords.get_last_superposeable(True).superposer = player_instance
entity_coords.get_last_superposeable(True).superposer = None
player_instance.dx = (new_coords_player[1] - coords_player[1]) * mapper.measure["horizontal"]
player_instance.dy = (new_coords_player[0] - coords_player[0]) * mapper.measure["vertical"]
player_instance.update()
player_instance.dx = 0
player_instance.dy = 0
player.position = new_coords_player
return True
return False
@classmethod
def move_box(cls, mapper: map.mapper.Mapper, grid, coords_box: typing.Tuple[int, int],
direction: map.moveable.DIRECTION):
"""Change the coords of a box"""
entity_coords = grid[coords_box[0]][coords_box[1]]
box_instance = cls.get_instance_from_coords(entity_coords, boxfile.Box)
box_new_coords = box_instance.get_new_coords(direction, coords_box)
if mapper.coords_in_map(box_new_coords):
"""Check if a box is in front of a wall dans and print a move is impossible"""
entity_new_coords = grid[box_new_coords[0]][box_new_coords[1]]
if issubclass(entity_new_coords.__class__, superposeablefile.Superposeable) \
and not isinstance(entity_new_coords.get_last_superposer(), boxfile.Box):
""" Player can't move more than 1 box """
entity_new_coords.get_last_superposeable(True).superposer = box_instance
box_instance.dx = (box_new_coords[1] - coords_box[1]) * mapper.measure["horizontal"]
box_instance.dy = (box_new_coords[0] - coords_box[0]) * mapper.measure["vertical"]
box_instance.update()
box_instance.dx = 0
box_instance.dy = 0
return True
return False
@classmethod
def get_instance_from_coords(cls, superposer, object_check):
"""Check if the last superposer is in object instance"""
last_superposer = superposer.get_last_superposer()
if isinstance(last_superposer, object_check):
return last_superposer
else:
raise AttributeError("the last superposer of superposer is not instance of object_ckeck.")

107
logic/player.py Normal file
View File

@ -0,0 +1,107 @@
import typing
import pygame
import logic.mover
import logic.searcher
import map.box
import map.moveable
import map.moveable as movfile
class Player:
def __init__(self, entity_mapper):
self.entity_mapper = entity_mapper
self.full_path = None
self.full_path_temoin = []
self.position = logic.searcher.Searcher.get_coords_player_from_map(self.entity_mapper.grid_of_sprites)
self.position_start = self.position[:]
self.direction: typing.Optional[map.moveable.DIRECTION] = None
self.blow_counter = 0
self.restarted = False
self.wanted_move = False
self.had_moved = False
self.interrupt = False
if self.entity_mapper.position == 1:
self.inputs = ["left", "right", "up", "down", 'p']
self.direction_association = None
else:
self.inputs = ["q", "d", "z", "s", 'o']
self.direction_association = dict(zip(self.inputs, ["left", "right", "up", "down"]))
def player_analizer(self, event):
"""Check if the player want to restart the game. Then move the player using the 'move' function"""
if self.interrupt:
return None
if event.key == pygame.K_o and self.entity_mapper.position == 0:
self.restart_player_game()
elif event.key == pygame.K_p and self.entity_mapper.position == 1:
self.restart_player_game()
elif self.direction is None:
key_direction = ""
if self.entity_mapper.position == 0:
str_key = event.unicode.lower()
try:
key_direction = self.direction_association[str_key].upper()
except KeyError:
return None
else:
for direction in ("left", "right", "up", "down"):
if event.key == getattr(pygame, "K_" + direction.upper()):
key_direction = direction.upper()
break
direction_enum = getattr(movfile.DIRECTION, key_direction, None)
if direction_enum is not None:
self.direction = direction_enum
self.move()
def move(self):
"""If the player want to move, the he moves (if the movement is possible)
and the blow_counter is increamentedby 1. If the player has win the his part of the screen is
interrupt"""
if self.direction is not None:
self.wanted_move = True
if logic.mover.Mover.move_player(self, self.direction):
self.had_moved = True
self.blow_counter += 1
if self.has_win():
self.interrupt = True
self.direction = None
def move_from_full_path(self):
"""Move the computer considering a full path accross the grid"""
if self.full_path:
self.direction = self.full_path.pop(0)
self.move()
def reset_status(self):
"""Reset the player statuts 'wanted_move' and 'had_moved' """
self.wanted_move = False
self.had_moved = False
def restart_player_game(self):
"""Restart the player game (the first or the second one, independently of the other one)"""
self.restarted = True
self.entity_mapper.re_map()
self.entity_mapper.update_sprites()
self.position = self.position_start
self.interrupt = False
self.blow_counter = 0
def has_win(self):
"""Return True if a player win the game(boxs are superposed
with all validation dots), else return False"""
dots_coords = logic.searcher.Searcher.get_coords_dots_from_map(self.entity_mapper.grid_of_sprites)
for dot_coords in dots_coords:
if not isinstance(self.entity_mapper.grid_of_sprites[dot_coords[0]][dot_coords[1]].get_last_superposer(),
map.box.Box):
return False
return True

14
logic/rewriter_sok.py Normal file
View File

@ -0,0 +1,14 @@
import os
import config.constants
class Overwrite:
def writer(self, grid, a):
"""Overwrite a level consedering a giving grid and level number"""
self.cpt = 0
self.level = os.path.join(config.constants.BASE_DIR, "levels", f'level{a}.sok')
with open(self.level, "w") as file:
for i in range(len(grid)):
for y in range(len(grid)):
file.write(grid[i][y])
if i != len(grid) - 1:
file.write('\n')

58
logic/searcher.py Normal file
View File

@ -0,0 +1,58 @@
import typing
import map.air as airfile
import map.dot
import map.box
import map.moveable
import map.player as playerfile
import map.superposeable
class Searcher:
@classmethod
def get_coords_player_from_map(cls, grid: typing.List[typing.List]):
"""Browse the game grid to find the player and to get his coords"""
i = 0
while i < len(grid):
j = 0
while j < len(grid[0]):
entity = grid[i][j]
if isinstance(entity, airfile.Air):
if isinstance(entity.get_last_superposer(), playerfile.Player):
return i, j
j += 1
i += 1
raise IndexError("No player in the map.")
@classmethod
def get_coords_dots_from_map(cls, grid: typing.List[typing.List]):
"""Browse the game grid to find the dots and to get their coords"""
dotsList = []
i = 0
while i < len(grid):
j = 0
while j < len(grid[0]):
entity = grid[i][j]
if isinstance(entity, airfile.Air) and isinstance(entity.superposer, map.dot.Dot):
dotsList.append((i, j))
j += 1
i += 1
return dotsList
@classmethod
def get_coords_box_to_place_from_map(cls, grid: typing.List[typing.List]):
"""Browse the game grid to find all the boxes (execpted the ones in a dot) and to get their coords"""
boxList = []
i = 0
while i < len(grid):
j = 0
while j < len(grid[0]):
entity = grid[i][j]
if isinstance(entity, airfile.Air):
if isinstance(entity.get_last_superposer(), map.box.Box) \
and not isinstance(entity.superposer, map.dot.Dot):
boxList.append((i, j))
j += 1
i += 1
return boxList

324
logic/state.py Normal file
View File

@ -0,0 +1,324 @@
import config.constants
import typing
import pygame
import logic.player
import logic.keyboardTools
import logic.generate
import logic.rewriter_sok
import solver.solver
import solver.pathNotFoundException
import map.entity_mapper as entity_mapper
import map.mapper as mapper
import interface.goback
class State:
quit = False
next_state = False
state_go_back = False
versus = None
do_generate_levels = False
dicGen = {"1": [8, 8, 2, 3, 1], "2": [9, 9, 3, 4, 2], "3": [10, 10, 4, 5, 2], "4": [15, 15, 6, 7, 2], "5": [
17, 17, 7, 8, 2], "6": [18, 18, 8, 9, 1], "7": [20, 20, 6, 7, 2], "8": [25, 25, 7, 8, 3], "9": [30, 30, 7, 10, 4]}
difficulty: typing.Union[str, None] = None
level: typing.Union[int, str, None] = None
players: typing.List[logic.player.Player] = []
situations = ('select_difficult', 'select_level', 'select_versus_mode', 'game')
situation = 0
inputs = {
"option_menu_difficulty": logic.keyboardTools.KeyboardTools.get_keys_event_from_choices(['g']),
"difficulty": logic.keyboardTools.KeyboardTools.get_keys_event_from_choices(['a', 'b', 'c', 'e']),
"levels": {
'a': logic.keyboardTools.KeyboardTools.get_keys_event_from_range(1, 9),
'b': logic.keyboardTools.KeyboardTools.get_keys_event_from_range(1, 3),
'c': logic.keyboardTools.KeyboardTools.get_keys_event_from_range(4, 6),
'e': logic.keyboardTools.KeyboardTools.get_keys_event_from_range(7, 9)
},
"versus": logic.keyboardTools.KeyboardTools.get_keys_event_from_choices(['a', 'b']),
}
@classmethod
def inputs_checker(cls, event, valid_inputs):
"""Check if the key the player press is valid or not"""
for valid_input in valid_inputs:
if event.key == getattr(pygame, valid_input) \
or (getattr(event, "unicode", None) and event.unicode.lower() == logic.keyboardTools.KeyboardTools.get_str_from_keyboard_event(valid_input).lower()):
return True
return False
@classmethod
def analizer(cls, events: list, textes=None):
"""Analize the events and modify the state class's variables in consequence"""
for event in events:
if event.type == pygame.QUIT:
cls.quit = True
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
cls.state_go_back = True
else:
if not cls.analizer_menu(event):
cls.analizer_game(event)
elif event.type == pygame.MOUSEBUTTONUP:
cls.analizer_menu_mouse(event, textes)
elif event.type == config.constants.COMPUTER_EVENT:
cls.analizer_game_from_computer()
@classmethod
def get_letter_pointed_from_mouse(cls, event, textes, attr):
"""Change the variables "state_go_back" and "next_state" according to what the mouse is pointing at"""
for texte in textes:
if texte.rect.collidepoint(event.pos):
if isinstance(texte, interface.goback.GoBack):
cls.state_go_back = True
break
elif ':' in texte.texte:
command = texte.texte.split(":")[0].lower().strip()
command_pygame_key = logic.keyboardTools.KeyboardTools.get_keys_event_from_choices([command])[0]
if command_pygame_key in cls.inputs["option_menu_difficulty"]:
cls.do_generate_levels = True
else:
setattr(cls, attr, command)
cls.next_state = True
break
@classmethod
def analizer_menu_mouse(cls, event, textes):
"""Enable the player to choose a level and a game difficulty in the menu with the mouse pointer"""
if cls.situations[cls.situation][:6] != "select":
return False
if cls.difficulty is None:
cls.get_letter_pointed_from_mouse(event, textes, "difficulty")
elif cls.level is None:
for texte in textes:
if texte.rect.collidepoint(event.pos):
if isinstance(texte, interface.goback.GoBack):
cls.state_go_back = True
break
elif ':' in texte.texte:
try:
integer = int(texte.texte.split(":")[0].strip().lower()[-1])
cls.level = integer
cls.next_state = True
except ValueError:
pass
except IndexError:
pass
elif cls.versus is None:
cls.get_letter_pointed_from_mouse(event, textes, "versus")
return True
@classmethod
def get_unicode_letter_for_situation(cls, event, input_sector, attr):
"""Return the unicode of a given letter and make the 'next_state' variable True """
if cls.inputs_checker(event, cls.inputs[input_sector]):
integer = logic.keyboardTools.KeyboardTools.check_integer_keyboard(
event.unicode)
if integer is None:
if event.unicode.isalpha():
setattr(cls, attr, event.unicode.lower())
cls.next_state = True
@classmethod
def analizer_menu(cls, event):
"""Movidifed the State class considering what the player choose in the menu interface"""
if cls.situations[cls.situation][:6] != "select":
return False
if cls.difficulty is None:
if cls.inputs_checker(event, cls.inputs["option_menu_difficulty"]):
cls.do_generate_levels = True
else:
cls.get_unicode_letter_for_situation(event, "difficulty", "difficulty")
elif cls.level is None:
if cls.inputs_checker(event, cls.inputs['levels'][cls.difficulty]):
integer = logic.keyboardTools.KeyboardTools.check_integer_keyboard(
event.unicode)
if integer is not None:
cls.level = integer
cls.next_state = True
elif cls.versus is None:
cls.get_unicode_letter_for_situation(event, "versus", "versus")
return True
@classmethod
def generate_grids_for_levels(cls):
"""Call the Generate class to create a random level according to the parameters we
decided for each level difficulty (in the dicGen dictionnary)"""
i = 1
while i <= 9:
grid = logic.generate.Generate.gridGen(
cls.dicGen[str(i)][0],
cls.dicGen[str(i)][1],
cls.dicGen[str(i)][2],
cls.dicGen[str(i)][3],
cls.dicGen[str(i)][4])
_mapper = mapper.Mapper("", grid)
_player = logic.player.Player(entity_mapper.EntityMapper(_mapper, i, initsprites=False))
_solver = solver.solver.Solver(_player)
try:
_solver.get_full_path()
yield grid
except solver.pathNotFoundException.Path_not_found_exception:
i -= 1
i += 1
@classmethod
def generate_levels(cls):
"""Call the Overwrite class to erase all the levels existing and replace them by random generated levels"""
overwriter = logic.rewriter_sok.Overwrite()
grids = [grid for grid in cls.generate_grids_for_levels()]
for i, grid in enumerate(grids, start=1):
overwriter.writer(grid, str(i))
cls.do_generate_levels = False
@classmethod
def analizer_game(cls, event):
"""This fonction analize all the player's actions during a game and act consequently"""
if cls.situations[cls.situation] != "game":
return False
for player in cls.players:
cls.game_traitment(player)
if player.inputs:
valid_inputs = logic.keyboardTools.KeyboardTools.get_keys_event_from_choices(
player.inputs)
if cls.inputs_checker(event, valid_inputs):
player.player_analizer(event)
if cls.versus == "a" and event.key == pygame.K_o and player.entity_mapper.position == 1:
player.restart_player_game()
if cls.versus == "a" and any((player.had_moved for player in cls.players)):
cls.analizer_game_from_computer()
if cls.players[0].interrupt and not cls.players[1].interrupt:
pygame.time.set_timer(config.constants.COMPUTER_EVENT, 500)
if all((player.interrupt for player in cls.players)):
cls.next_state = True
return True
@classmethod
def analizer_game_from_computer(cls):
"""All the computer to finish the game alone if the player beats him"""
if cls.situations[cls.situation] != "game":
return False
cls.game_traitment(cls.players[1])
cls.players[1].move_from_full_path()
if cls.players[0].interrupt:
if all((player.interrupt for player in cls.players)):
cls.next_state = True
return True
@classmethod
def go_back(cls):
"""Make the game one step back """
cls.leave_situation(-1)
if cls.situation < 0:
cls.situation = 0
cls.quit = True
if not cls.situations[cls.situation] == "select_versus_mode":
cls.state_go_back = False
@classmethod
def leave_situation(cls, direction):
"""Change the situation variable 'situation' to change the 'cls.situations[cls.situations]' value.
Also impact the 'level', 'difficulty', and 'versus' vraibles"""
if cls.situations[cls.situation] == "select_level":
if direction == -1:
cls.level = None
cls.difficulty = None
elif cls.situations[cls.situation] == "select_versus_mode":
if direction == -1:
cls.versus = None
cls.level = None
elif cls.situations[cls.situation] == "game":
for player in cls.players:
player.entity_mapper.sprites.empty()
cls.players.clear()
if cls.versus == "a":
pygame.time.set_timer(config.constants.COMPUTER_EVENT, 0)
if direction == 1:
cls.level = None
cls.difficulty = None
cls.versus = None
if direction == -1:
cls.situation -= 1
else:
cls.situation = (cls.situation + 1) % len(cls.situations)
@classmethod
def generate_players(cls):
""""""
for player in cls.players:
player.entity_mapper.sprites.empty()
cls.players.clear()
if cls.difficulty == "a" and cls.level is not None:
grid = logic.generate.Generate.gridGen(
cls.dicGen[str(cls.level)][0],
cls.dicGen[str(cls.level)][1],
cls.dicGen[str(cls.level)][2],
cls.dicGen[str(cls.level)][3],
cls.dicGen[str(cls.level)][4]
)
_mapper = mapper.Mapper("", grid)
elif cls.level is not None:
_mapper = mapper.Mapper("level" + str(cls.level) + ".sok")
if cls.level is not None:
for i in range(2):
_player = logic.player.Player(
entity_mapper.EntityMapper(_mapper, i))
cls.players.append(_player)
@classmethod
def select_game_traitment(cls):
""""""
cls.generate_players()
if cls.versus == "a" and cls.players:
while True:
cls.players[1].inputs = []
_solver = solver.solver.Solver(cls.players[1])
if cls.difficulty == "a":
try:
_solver.get_full_path()
break
except solver.pathNotFoundException.Path_not_found_exception:
cls.generate_players()
continue
else:
try:
_solver.get_full_path()
break
except solver.pathNotFoundException.Path_not_found_exception:
cls.next_state = True
cls.next_state = True
@classmethod
def game_traitment(cls, player):
"""Reset the player statuts"""
if player.restarted:
player.restarted = False
if player.full_path_temoin:
player.full_path = player.full_path_temoin[:]
player.reset_status()

8
main.py Normal file
View File

@ -0,0 +1,8 @@
import logic.game
import map.mapper
import solver.astar
if __name__ == "__main__":
"""Start the game"""
game = logic.game.Game()
game.start()

0
map/__init__.py Normal file
View File

48
map/air.py Normal file
View File

@ -0,0 +1,48 @@
import os
import typing
import config.constants
import map.dot
import map.moveable
import map.superposeable
class Air(map.superposeable.Superposeable):
_character = " "
path_image = os.path.join(config.constants.BASE_DIR, "images", "floor.png")
def draw(self, surface):
for image in self.images:
surface.blit(image, self.rect)
@property
def images(self) -> typing.List[str]:
images = [self.image]
superposers = self.get_superposers()
i = 0
while i < len(superposers):
use_default_image = True
if i == len(superposers) - 1 and issubclass(superposers[i].__class__, map.moveable.Moveable):
try:
if isinstance(superposers[i - 1], map.dot.Dot) and superposers[i].imageAlternative:
use_default_image = False
except IndexError:
pass
if use_default_image:
images.append(superposers[i].image)
else:
images.append(superposers[i].imageAlternative)
i += 1
return images
@property
def character(self) -> str:
if self.superposer is not None:
return self.superposer.character
return super(Air, self).character

14
map/box.py Normal file
View File

@ -0,0 +1,14 @@
import map.moveable
import os
import config.constants
import pygame
class Box(map.moveable.Moveable):
_character = "$"
_alternativeCharacter = "*"
path_image = os.path.join(config.constants.BASE_DIR, "images", "box.png")
path_imageAlternative = os.path.join(config.constants.BASE_DIR, "images", "dark_box.png")
def __init__(self, x, y, image, image_alternative=None):
super(Box, self).__init__(x, y, image, image_alternative)

17
map/dot.py Normal file
View File

@ -0,0 +1,17 @@
import os
import pygame
import config.constants
import map.moveable
import map.superposeable
class Dot(map.superposeable.Superposeable):
_character = "."
path_image = os.path.join(config.constants.BASE_DIR, "images", "dot.png")
@property
def character(self):
if isinstance(self.superposer, map.moveable.Moveable):
return self.superposer.alternativeCharacter
return super(Dot, self).character

36
map/entity.py Normal file
View File

@ -0,0 +1,36 @@
import pygame
class Entity(pygame.sprite.Sprite):
_character: str = ""
_alternativeCharacter: str = ""
path_image: str = ""
path_imageAlternative: str = ""
_image: str = ""
_imageAlternative: str = ""
def __init__(self, x, y, image, image_alternative=""):
super(Entity, self).__init__()
self.dx = 0
self.dy = 0
self.image = image
self._imageAlternative = image_alternative
self.rect = self.image.get_rect()
self.rect = self.rect.move(x, y)
def update(self):
self.rect = self.rect.move(self.dx, self.dy)
@property
def imageAlternative(self) -> str:
return self._imageAlternative
@property
def alternativeCharacter(self) -> str:
return self._alternativeCharacter
@property
def character(self) -> str:
return self._character

92
map/entity_mapper.py Normal file
View File

@ -0,0 +1,92 @@
import typing
import config.constants
import pygame
import map.air as airfile
import map.dot as dotfile
import map.moveable as moveablefile
import map.wall as wallfile
class EntityMapper:
def __init__(self, mapper, position, initsprites=True):
self.mapper = mapper
self.position = position
self.grid_of_sprites = self.create_map()
if initsprites:
self.sprites = None
self.update_sprites()
def create_map(self) -> typing.List[typing.List]:
"""Creat the map"""
grille = []
i = 0
while i <= self.mapper.length_of_line - 1:
liste = []
j = 0
while j <= self.mapper.length_of_column - 1:
letter = self.mapper.get_file_letter(i, j)
entity = self.get_instance_entity_from_letter(letter, (i, j))
liste.append(entity)
j += 1
grille.append(liste)
i += 1
return grille
def re_map(self):
self.grid_of_sprites = self.create_map()
def update_sprites(self):
if self.sprites is not None:
self.sprites.empty()
self.sprites = pygame.sprite.Group()
for line in self.grid_of_sprites:
for sprite in line:
sprite.dx = config.constants.SIDE_WINDOW * self.position
sprite.update()
sprite.dx = 0
self.sprites.add(sprite)
def get_instance_entity_from_letter(self, letter: str, coords: typing.Tuple) -> \
typing.Union[wallfile.Wall, airfile.Air]:
for entity in self.mapper.entities:
param_entity = self.get_init_param_for_sprites(entity, *coords)
instance_entity = entity(*param_entity)
if instance_entity.character == letter:
if issubclass(entity, moveablefile.Moveable) or isinstance(instance_entity, dotfile.Dot):
param_air = self.get_init_param_for_sprites(airfile.Air, *coords)
air = airfile.Air(*param_air)
air.superposer = instance_entity
return air
else:
return instance_entity
elif instance_entity.alternativeCharacter == letter:
if not issubclass(entity, moveablefile.Moveable):
raise ValueError("The entity is not recognized")
param_air = self.get_init_param_for_sprites(airfile.Air, *coords)
param_dot = self.get_init_param_for_sprites(dotfile.Dot, *coords)
air = airfile.Air(*param_air)
dot = dotfile.Dot(*param_dot)
dot.superposer = instance_entity
air.superposer = dot
return air
raise ValueError("The letter: " + str(letter) + " is not associated to a entity.")
def get_init_param_for_sprites(self, entity, line, column) -> typing.List:
params = [column * self.mapper.measure["horizontal"], line * self.mapper.measure["vertical"],
self.mapper.images[entity.path_image]]
if entity.path_imageAlternative:
params.append(self.mapper.images[entity.path_imageAlternative])
return params

103
map/mapper.py Normal file
View File

@ -0,0 +1,103 @@
import os
import typing
import pygame
import config.constants
import map.air as airfile
import map.box as boxfile
import map.dot as dotfile
import map.player as playerfile
import map.wall as wallfile
class Mapper:
entities = (airfile.Air, boxfile.Box, playerfile.Player,
wallfile.Wall, dotfile.Dot)
def __init__(self, level_path: str, from_grid=None, with_load_image=True):
self.level_path_file = os.path.join(
config.constants.BASE_DIR, "levels", level_path)
self._lines: int = 0
self._columns: int = 0
self._measure: dict = {}
self.images = {}
if from_grid is not None:
self.grid_file = from_grid
else:
self.grid_file = self.create_grid_from_file()
if with_load_image:
self.load_images()
def load_images(self):
images_path = []
for entity in self.entities:
if entity.path_image not in images_path and entity.path_image:
images_path.append(entity.path_image)
if entity.path_imageAlternative not in images_path and entity.path_imageAlternative:
images_path.append(entity.path_imageAlternative)
for path in images_path:
image = pygame.image.load(path).convert_alpha()
self.images[path] = pygame.transform.scale(image,
(self.measure["horizontal"], self.measure["vertical"]))
@property
def measure(self):
if not self._measure:
self._measure = {
"horizontal": config.constants.SIDE_WINDOW // self.length_of_column,
"vertical": config.constants.SIDE_WINDOW // self.length_of_line
}
return self._measure
def create_grid_from_file(self):
"""Creat the map"""
grille = []
with open(self.level_path_file, "r") as file_of_level:
for line in file_of_level:
liste = []
for letter in line:
if letter != "\n":
liste.append(letter)
grille.append(liste)
return grille
def coords_in_map(self, coords: typing.Tuple):
return (0 <= coords[0] <= self.length_of_line - 1) and (0 <= coords[1] <= self.length_of_column - 1)
@property
def length_of_column(self):
if self._columns != 0:
return self._columns
self._columns = 0
for line in self.grid_file:
if len(line) > self._columns:
self._columns = len(line)
return self._columns
@property
def length_of_line(self):
if self._lines != 0:
return self._lines
self._lines = len(self.grid_file)
return self._lines
def get_file_letter(self, _line: int, _column: int):
try:
letter = self.grid_file[_line][_column]
return letter
except IndexError:
return "#"

30
map/moveable.py Normal file
View File

@ -0,0 +1,30 @@
import enum
import typing
import map.entity
DIRECTION = enum.Enum('direction', 'UP DOWN LEFT RIGHT')
class Moveable(map.entity.Entity):
directions_dict = {DIRECTION.LEFT: lambda x, y: (x, y - 1),
DIRECTION.RIGHT: lambda x, y: (x, y + 1),
DIRECTION.UP: lambda x, y: (x - 1, y),
DIRECTION.DOWN: lambda x, y: (x + 1, y)}
def __init__(self, x, y, image, image_alternative=None):
super(Moveable, self).__init__(x, y, image, image_alternative)
@classmethod
def get_new_coords(cls, direction: DIRECTION, coords: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
"""
Get the new coordinates from a move by a moveable entity.
this function takes a direction from the ENUM "DIRECTION"
and the coordinates that you want to change with the chosen direction.
direction: enum.ENUM DIRECTION
coords: typing.Tuple(int, int) - line/column
This function return a typing.Tuple(int, int)
"""
return cls.directions_dict[direction](coords[0], coords[1])

10
map/player.py Normal file
View File

@ -0,0 +1,10 @@
import os
import config.constants
import map.moveable
class Player(map.moveable.Moveable):
"""The definition of the Player class"""
_character = "@"
_alternativeCharacter = "+" # used when it is superposed to an dot
path_image = os.path.join(config.constants.BASE_DIR, "images", "player.png")

80
map/superposeable.py Normal file
View File

@ -0,0 +1,80 @@
import typing
import map.entity
import map.moveable as moveablefile
class Superposeable(map.entity.Entity):
def __init__(self, x, y, image, image_alternative=None):
super(Superposeable, self).__init__(x, y, image, image_alternative)
self._superposer: typing.Union[None, moveablefile.Moveable, Superposeable] = None
@property
def superposer(self) -> typing.Union[None, moveablefile.Moveable, "Superposeable"]:
return self._superposer
@superposer.setter
def superposer(self, superposer: typing.Union[None, moveablefile.Moveable, "Superposeable"]) -> None:
self._superposer = superposer
@property
def images(self) -> typing.List[str]:
images = [self.image]
superposers = self.get_superposers()
for superposer in superposers:
images.append(superposer.image)
return images
def get_last_superposeable(self, include_self=False) -> typing.Optional["Superposeable"]:
superposeable: typing.Optional["Superposeable"]
if issubclass(self.superposer.__class__, Superposeable):
superposeable = self.superposer
while issubclass(superposeable.__class__, Superposeable) and issubclass(superposeable.superposer.__class__,
Superposeable):
superposeable = superposeable.superposer
else:
superposeable = None
if superposeable is None and include_self:
return self
return superposeable
def get_last_superposer(self, include_self=False) -> typing.Union[None, moveablefile.Moveable, "Superposeable"]:
superposer = self.superposer
while issubclass(superposer.__class__, Superposeable) and superposer.superposer is not None:
superposer = superposer.superposer
if superposer is None and include_self:
return self
return superposer
def get_superposers(self, include_self=False) -> typing.List[typing.Union["Superposeable", moveablefile.Moveable]]:
if self.superposer is None:
if include_self:
return [self]
else:
return []
superposers: typing.List[
typing.Union[
"Superposeable",
moveablefile.Moveable]
] = [self.superposer]
superposer = superposers[0]
while issubclass(superposer.__class__, Superposeable) and superposer.superposer is not None:
superposer = superposer.superposer
superposers.append(superposer)
if include_self:
return [self] + superposers
else:
return superposers

15
map/wall.py Normal file
View File

@ -0,0 +1,15 @@
import os
import config.constants
import map.entity
class Wall(map.entity.Entity):
_character = "#"
path_image = os.path.join(config.constants.BASE_DIR, "images", "wall.png")
def __init__(self, x, y, image, image_alternative=None):
super(Wall, self).__init__(x, y, image, image_alternative)
def draw(self, surface):
surface.blit(self.image, self.rect)

0
solver/__init__.py Normal file
View File

106
solver/astar.py Normal file
View File

@ -0,0 +1,106 @@
import heapq
import map.wall
import map.box
import solver.node
import solver.pathNotFoundException
class Astar:
""" From https://fr.wikipedia.org/wiki/Algorithme_A* and http://code.activestate.com/recipes/578919-python-a-pathfinding-with-binary-heap/ """
offsets = {(0, 1): "RIGHT", (1, 0): "DOWN", (0, -1): "LEFT", (-1, 0): "UP"}
def __init__(self, grid):
self.grid = [[solver.node.Node(i, j, grid[i][j]) for j in range(len(grid[i]))] for i in range(len(grid))]
self.obstacle = {map.wall.Wall._character, map.box.Box._character}
def get_node(self, line, column):
return self.grid[line][column]
def reset_grid_data(self):
for line in self.grid:
for node in line:
node.reset_data()
def reset_grid_calcul_data(self):
for line in self.grid:
for node in line:
node.reset_calc_data()
def is_obstacle(self, node):
"""Return True if node is an obstacle (wall or box) else return False"""
return (node.character in self.obstacle and node.exception is False) or node.transform_to_obstacle
def get_neighbors(self, node, goal):
"""Return a list of neighbors nodes from the left, right, up and down direction of `node` param if nodes aren't obstacle"""
neighbors = []
for x, y in self.offsets.keys():
try:
_node = self.grid[node["x"] + x][node["y"] + y]
except IndexError:
continue
if not self.is_obstacle(_node):
neighbors.append(_node)
elif _node.x == goal.x and _node.y == goal.y:
raise solver.pathNotFoundException.Path_not_found_exception("No path found")
return neighbors
def get_neighbor_from_openlist(self, neighbor, openlist):
for data in openlist:
_dict = data[3]
if neighbor.x == _dict["x"] and neighbor.y == _dict["y"]:
return _dict
return False
def neighbor_in_closedlist(self, neighbor, closedList):
for _dict in closedList:
if neighbor.x == _dict["x"] and neighbor.y == _dict["y"]:
return True
return False
def solve(self, node_start, node_goal, include_coords_start=True):
"""Return a path to go from node_start to node_goal"""
start = node_start.__dict__
goal = node_goal
closedList = []
openList = []
came_from = {}
heapq.heappush(openList, (0, 0, id(start), start))
while openList:
current_node = heapq.heappop(openList)[3]
if current_node["x"] == goal.x and current_node["y"] == goal.y:
# path was found
data = []
while id(current_node) in came_from:
data.append(self.get_node(current_node["x"], current_node["y"]))
current_node = came_from[id(current_node)]
if include_coords_start:
data.append(node_start)
return list(reversed(data))
neighbors = self.get_neighbors(current_node, goal)
i = 0
while i < len(neighbors):
neighbor = neighbors[i]
the_neighbor_from_openlist = self.get_neighbor_from_openlist(neighbor, openList)
if not (self.neighbor_in_closedlist(neighbor, closedList)
or (the_neighbor_from_openlist is not False and neighbor.cost <= the_neighbor_from_openlist.get("cost", None))):
neighbor.cost = current_node["cost"] + 1
neighbor.heuristic = neighbor.cost * neighbor.heuristic_calc(goal)
neighbor_copy = neighbor.__dict__
came_from[id(neighbor_copy)] = current_node
class_current_node = self.get_node(current_node["x"], current_node["y"])
heapq.heappush(openList, (class_current_node.compare(neighbor), neighbor.heuristic, id(neighbor_copy), neighbor_copy))
i += 1
closedList.append(current_node)
raise solver.pathNotFoundException.Path_not_found_exception("no path found", start["x"], start["y"], "&", goal.x, goal.y)

30
solver/node.py Normal file
View File

@ -0,0 +1,30 @@
class Node:
""" From https://fr.wikipedia.org/wiki/Algorithme_A* """
def __init__(self, x, y, character, cost=0, heuristic=0):
self.x = x
self.y = y
self.cost = cost
self.heuristic = heuristic
self.character = character
self.exception = False
self.transform_to_obstacle = False
def compare(self, other):
if self.heuristic < other.heuristic:
return 1
elif self.heuristic == other.heuristic:
return 0
else:
return -1
def heuristic_calc(self, other):
return abs(self.x - other.x) + abs(self.y - other.y)
def reset_data(self):
self.transform_to_obstacle = False
self.exception = False
self.reset_calc_data()
def reset_calc_data(self):
self.heuristic = 0
self.cost = 0

View File

@ -0,0 +1,8 @@
class Path_not_found_exception(Exception):
"""
Attributes:
message -- explanation of why the specific transition is not allowed
"""
def __init__(self, *message):
self.message = message

221
solver/solver.py Normal file
View File

@ -0,0 +1,221 @@
import solver.astar
import logic.searcher
import map.moveable
import map.box
class Solver:
def __init__(self, player):
self.player = player
self.astar = solver.astar.Astar(player.entity_mapper.mapper.grid_file)
self.boxs_to_place = set(logic.searcher.Searcher.get_coords_box_to_place_from_map(player.entity_mapper.grid_of_sprites))
self.dots = set(logic.searcher.Searcher.get_coords_dots_from_map(player.entity_mapper.grid_of_sprites))
def get_box_near_player(self):
player_node = self.astar.get_node(self.player.position[0], self.player.position[1])
near_box_data = [None, None]
for box in self.boxs_to_place:
node_box = self.astar.get_node(box[0], box[1])
heuristic = player_node.heuristic_calc(node_box)
if near_box_data[0] is None or heuristic <= near_box_data[1]:
near_box_data[0] = node_box
near_box_data[1] = heuristic
return near_box_data[0], player_node
def get_dot_near_box(self, box_node):
near_dot_data = [None, None]
for dot in self.dots:
node_dot = self.astar.get_node(dot[0], dot[1])
heuristic = box_node.heuristic_calc(node_dot)
if near_dot_data[0] is None or heuristic <= near_dot_data[1]:
near_dot_data[0] = node_dot
near_dot_data[1] = heuristic
return near_dot_data[0], box_node
def get_symetric_node(self, from_node, to_node):
relative_position = (to_node.x - from_node.x, to_node.y - from_node.y)
return self.astar.get_node(from_node.x - relative_position[0], from_node.y - relative_position[1])
def get_diagonal_node(self, from_node, relative_position):
next_target_pos = [from_node.x + relative_position[0], from_node.y + relative_position[1]]
next_target_pos_temoin = next_target_pos[:]
additive = [1, -1]
if relative_position[1]:
arg = 0
else:
arg = 1
next_target_pos[arg] += additive[0]
next_target = self.astar.get_node(*next_target_pos)
if self.astar.is_obstacle(next_target):
next_target_pos = next_target_pos_temoin
next_target_pos[arg] += additive[1]
next_target = self.astar.get_node(*next_target_pos)
if self.astar.is_obstacle(next_target):
raise Exception("No diagonal available found.", next_target.x, next_target.y)
return next_target
def analyse_path_from_box_to_dot(self, path):
box_node = path[0]
target_dot = path[-1]
path_predecalage = path[:-1]
path_postdecalage = path[1:]
steps = []
history = None
intermediate_target = None
for i in range(len(path_postdecalage)):
try:
previous = path_predecalage[i - 1]
except IndexError:
previous = None
current = path_predecalage[i]
post = path_postdecalage[i]
symetric_node = self.get_symetric_node(current, post)
if self.astar.is_obstacle(symetric_node):
steps = False
if current == box_node:
post.transform_to_obstacle = True
if post == target_dot:
relative_position = (post.x - current.x, post.y - current.y)
intermediate_target = self.get_diagonal_node(current, relative_position)
break
else:
current.transform_to_obstacle = True
elif steps is not False:
bifurcation = self.get_bifurcation_from_path(current, post)
if bifurcation != history:
if history is not None:
steps.extend([{previous: current}, {symetric_node: current}])
history = bifurcation
if i == len(path_postdecalage) - 1 and steps is not False:
steps.append({current: post})
return steps, self.get_symetric_node(path[0], path[1]), intermediate_target
def get_bifurcation_from_path(self, current, post):
relative_position = (post.x - current.x, post.y - current.y)
return self.astar.offsets[relative_position]
def generate_steps(self, box_node, dot_node):
box_node.exception = True
change_target = False
intermediate_target = None
default = True
path_from_box_to_dot_base = []
path_from_box_to_dot = self.astar.solve(box_node, dot_node)
analyse = self.analyse_path_from_box_to_dot(path_from_box_to_dot)
while analyse[0] is False:
if analyse[2]:
param = (box_node, analyse[2])
intermediate_target = analyse[2]
change_target = True
default = False
else:
if change_target:
dot_node.transform_to_obstacle = False
param = (intermediate_target, dot_node, False)
default = False
else:
param = (box_node, dot_node)
default = True
change_target = False
path_from_box_to_dot = self.astar.solve(*param)
self.astar.reset_grid_calcul_data()
if not default:
path_from_box_to_dot_base.extend(path_from_box_to_dot)
if change_target is False:
analyse = self.analyse_path_from_box_to_dot(path_from_box_to_dot_base)
else:
analyse = False, None, None
else:
path_from_box_to_dot_base = []
analyse = self.analyse_path_from_box_to_dot(path_from_box_to_dot)
self.astar.reset_grid_data()
return analyse
def path_for_player_to_join_adjacent_box(self, player_node, adjacent_box_node):
path = self.astar.solve(player_node, adjacent_box_node, include_coords_start=False)
self.astar.reset_grid_calcul_data()
return path
def get_path_from_steps(self, steps, box_node):
box_node.exception = True
path = []
for i, step_dict in enumerate(steps, start=1):
goal = tuple(step_dict.keys())[0]
box = step_dict[goal]
if i % 2 == 0:
box.transform_to_obstacle = True
player_node = self.astar.get_node(self.player.position[0], self.player.position[1])
path.extend(self.astar.solve(player_node, goal, include_coords_start=False))
if path:
self.player.position = path[-1].x, path[-1].y
self.astar.reset_grid_data()
box_node.exception = True
return path
def transform_path_to_direction(self, path):
path_predecalage = path[:-1]
path_postdecalage = path[1:]
path = []
for i in range(len(path_postdecalage)):
current = path_predecalage[i]
post = path_postdecalage[i]
relative_position = (post.x - current.x, post.y - current.y)
path.append(getattr(map.moveable.DIRECTION, self.astar.offsets[relative_position]))
return path
def get_full_path(self):
full_path = []
while self.dots and self.boxs_to_place:
near_box_from_player = self.get_box_near_player()
near_dot_from_box = self.get_dot_near_box(near_box_from_player[0])[0]
steps, adjacent_box_node, _ = self.generate_steps(near_box_from_player[0], near_dot_from_box)
path_to_join_adjacent_box = self.path_for_player_to_join_adjacent_box(near_box_from_player[1], adjacent_box_node)
full_path.extend(path_to_join_adjacent_box)
if full_path:
self.player.position = full_path[-1].x, full_path[-1].y
path_from_steps = self.get_path_from_steps(steps, near_box_from_player[0])
full_path.extend(path_from_steps)
self.boxs_to_place.remove((near_box_from_player[0].x, near_box_from_player[0].y))
self.dots.remove((near_dot_from_box.x, near_dot_from_box.y))
self.astar.reset_grid_data()
near_box_from_player[0].character = " "
near_dot_from_box.character = "#"
near_box_from_player[1].character = " "
self.astar.get_node(self.player.position[0], self.player.position[1]).character = "@"
self.player.position = self.player.position_start
full_path.insert(0, self.astar.get_node(self.player.position[0], self.player.position[1]))
self.player.full_path = self.transform_path_to_direction(full_path)
self.player.full_path_temoin = self.player.full_path[:]
return True

175
solver/solver_old.py Normal file
View File

@ -0,0 +1,175 @@
import map.entity_mapper
import solver.astar
import solver.node
import logic.searcher
import map.wall
import map.moveable
import typing
import math
class Solver:
@classmethod
def solve(cls, mapEntityClass: map.entity_mapper.EntityMapper) -> typing.List[typing.Tuple[int, int]]:
"""
- Go from nearest box(from player position) to nearest validation dot - Done
- Calculate in which path it must travel to place the box over the validation dot (use A* algorithm to check if
player can go to position where it will move the move) - Done
- If impossible, find if the same box can go to an other Dot - TODO
- If impossible too, we search for an other box and we go back on this box later - TODO
- Return finally a List of Tuple where player need to place, List can be empty or uncompleted if
solver can't find a solution - Done
"""
successfulDots: typing.List[typing.Tuple[int, int]] = []
successfulBox: typing.List[typing.Tuple[int, int]] = []
maze = mapEntityClass.grid_of_sprites
coordsPlayer = logic.searcher.Searcher.get_coords_player_from_map(maze)
path = [coordsPlayer]
boxFinalPath = []
while len(cls.get_coords_free_dots_from_map(maze, successfulDots)) > 0:
coordsDots = cls.get_coords_free_dots_from_map(maze, successfulDots)
coordsBox = cls.get_coords_free_box_from_map(maze, successfulBox)
if len(coordsDots) == 0 or len(coordsBox) == 0:
return cls.to_directions(cls.complete_path(path, mapEntityClass, boxFinalPath))
# Search the nearest box
nearestBox = cls.nearest_instance(coordsPlayer, coordsBox, successfulBox)
# then we looking for the nearest validation dot
nearestDot = cls.nearest_instance(nearestBox, coordsDots, successfulDots)
if nearestBox == (-1, -1) or nearestDot == (-1, -1):
raise Exception("Can found nearest Box or Dot")
cls.boxPath(path, mapEntityClass, nearestBox, nearestDot, boxFinalPath, successfulBox, successfulDots)
return cls.to_directions(cls.complete_path(path, mapEntityClass, boxFinalPath))
@classmethod
def distance(cls, coordA: typing.Tuple[int, int], coordB: typing.Tuple[int, int]) -> float:
return math.sqrt(pow(coordB[0] - coordA[0], 2) + pow(coordB[1] - coordA[1], 2))
@classmethod
def nearest_instance(cls, coordsFrom: typing.Tuple[int, int], listTo: typing.List[typing.Tuple[int, int]],
exclude: typing.List[typing.Tuple[int, int]]) -> typing.Tuple[int, int]:
i = 20000
nearest: typing.Tuple[int, int] = (-1, -1)
for c in listTo:
if c not in exclude:
tmp = cls.distance(coordsFrom, c)
if tmp < i:
i = tmp
nearest = c
return nearest
@classmethod
def get_coords_free_dots_from_map(cls, grid: typing.List[typing.List],
successfulDots: typing.List[typing.Tuple[int, int]]) -> typing.List[
typing.Tuple[int, int]]:
"""Return all dots which have no box on it"""
dots = logic.searcher.Searcher.get_coords_dots_from_map(grid)
freeDots = []
for i in dots:
if grid[i[0]][i[1]].superposer.superposer is None and i not in successfulDots:
freeDots.append(i)
return freeDots
@classmethod
def get_coords_free_box_from_map(cls, grid: typing.List[typing.List],
successfulBox: typing.List[typing.Tuple[int, int]]) -> typing.List[
typing.Tuple[int, int]]:
"""Return all box which haven't been moved on a dot"""
box = logic.searcher.Searcher.get_coords_box_to_place_from_map(grid)
freeBox = []
for i in box:
if i not in successfulBox:
freeBox.append(i)
return freeBox
@classmethod
def clear_path(cls, path: typing.List[typing.Tuple[int, int]]) -> typing.List[typing.Tuple[int, int]]:
"""Delete duplicate Tuple"""
toDel = []
for i in range(len(path)):
if i < len(path) - 1:
if path[i] == path[i + 1]:
toDel.append(i)
for i in toDel[::-1]:
del path[i]
return path
@classmethod
def complete_path(cls, path: typing.List[typing.Tuple[int, int]], mapEntity,
boxPath: typing.List[typing.List[typing.Tuple[int, int]]]) -> typing.List[typing.Tuple[int, int]]:
"""
give an uncomplete path in param and return path between 2 locations Tuple
"""
newPath: typing.List[typing.Tuple[int, int]] = []
cantWalk = []
ignore = []
for i in boxPath:
for e in i:
cantWalk.append(e)
boxOnMap = logic.searcher.Searcher.get_coords_box_to_place_from_map(mapEntity.grid_of_sprites)
for i in boxOnMap:
ignore.append(i)
for i in range(len(path)):
if i < len(path) - 1:
if len(cantWalk) != 0:
ignore.append(cantWalk[0])
# aStarPath = solver.astar.Astar.solve(mapEntity, path[i], path[i + 1], ignore, cantWalk)
aStarPath = solver.astar.Astar.solve(mapEntity, path[i], path[i + 1])
if len(cantWalk) != 0:
ignore.remove(cantWalk[0])
if aStarPath is not None:
for y in aStarPath:
newPath.append(y)
pathToDel = []
for e in range(len(cantWalk)):
if y == cantWalk[e]:
pathToDel.append(e)
for e in pathToDel[::-1]:
del cantWalk[e]
# newPath.append(path[-1])
return cls.clear_path(newPath)
@classmethod
def to_directions(cls, path):
"""Convert path Tuple to the direction Enum"""
movementDirection = None
directionsEnum = map.moveable.DIRECTION
directions = [(0, -1), (0, 1), (1, 0), (-1, 0)]
newPath = []
for i, value in enumerate(path):
if i < len(path) - 1:
movement = (path[i + 1][0] - value[0], path[i + 1][1] - value[1])
if movement not in directions:
raise Exception("Movement isn't in direction", movement) # Error
if movement == (1, 0):
movementDirection = directionsEnum.DOWN
if movement == (-1, 0):
movementDirection = directionsEnum.UP
if movement == (0, 1):
movementDirection = directionsEnum.RIGHT
if movement == (0, -1):
movementDirection = directionsEnum.LEFT
newPath.append(movementDirection)
return newPath
@classmethod
def boxPath(cls, path, mapEntityClass, nearestBox, nearestDot, boxFinalPath, successfulDots, successfulBox) -> None:
"""using A* algo from Box to Dot and return locations Tuple where the player need to be to move the box"""
astar = solver.astar.Astar(mapEntityClass.mapper.grid_file)
boxNode = astar.get_node(*nearestBox)
dotNode = astar.get_node(*nearestDot)
boxPath = astar.solve(boxNode, boxNode)
boxPath2 = []
for i in range(len(boxPath)):
if i < len(boxPath) - 1:
movement = (boxPath[i + 1][0] - boxPath[i][0], boxPath[i + 1][1] - boxPath[i][1])
playerPosition = (boxPath[i][0] - movement[0], boxPath[i][1] - movement[1])
path.append(playerPosition)
path.append(boxPath[i])
boxPath2.append(boxPath[i])
# path.append(boxPath[-1])
boxFinalPath.append(boxPath2)
successfulDots.append(nearestDot)
successfulBox.append(nearestBox)

36
start.bat Normal file
View File

@ -0,0 +1,36 @@
::[Bat To Exe Converter]
::
::YAwzoRdxOk+EWAnk
::fBw5plQjdG8=
::YAwzuBVtJxjWCl3EqQJgSA==
::ZR4luwNxJguZRRnk
::Yhs/ulQjdF+5
::cxAkpRVqdFKZSDk=
::cBs/ulQjdF+5
::ZR41oxFsdFKZSDk=
::eBoioBt6dFKZSDk=
::cRo6pxp7LAbNWATEpCI=
::egkzugNsPRvcWATEpCI=
::dAsiuh18IRvcCxnZtBJQ
::cRYluBh/LU+EWAnk
::YxY4rhs+aU+JeA==
::cxY6rQJ7JhzQF1fEqQJQ
::ZQ05rAF9IBncCkqN+0xwdVs0
::ZQ05rAF9IAHYFVzEqQJQ
::eg0/rx1wNQPfEVWB+kM9LVsJDGQ=
::fBEirQZwNQPfEVWB+kM9LVsJDGQ=
::cRolqwZ3JBvQF1fEqQJQ
::dhA7uBVwLU+EWDk=
::YQ03rBFzNR3SWATElA==
::dhAmsQZ3MwfNWATElA==
::ZQ0/vhVqMQ3MEVWAtB9wSA==
::Zg8zqx1/OA3MEVWAtB9wSA==
::dhA7pRFwIByZRRnk
::Zh4grVQjdCyDJGyX8VAjFCpBSQqWNWWGIrof/eX+4f6Unl4SX+09eYGWiOXXcLBHvh3YZpkm2XhbloUJFB44
::YB416Ek+ZG8=
::
::
::978f952a14a936cc963da21a135fa983
%ECHO OFF
python3 ./main.py
PAUSE