imported from svn
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.idea
|
||||||
|
.svn
|
||||||
|
__pychache/
|
BIN
Rapport/Illustrations/Original.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
Rapport/Illustrations/execution.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
Rapport/Illustrations/generate.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
Rapport/Illustrations/gestionnaire_tache_forte_conso.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
Rapport/Illustrations/graphJetBrains.png
Normal file
After Width: | Height: | Size: 165 KiB |
BIN
Rapport/Illustrations/map_1.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
Rapport/Illustrations/map_2.png
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
Rapport/Illustrations/map_3.png
Normal file
After Width: | Height: | Size: 384 KiB |
BIN
Rapport/Illustrations/map_4.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
Rapport/Illustrations/menu.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
Rapport/Illustrations/mouv_1.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
Rapport/Illustrations/mouv_2.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
Rapport/Illustrations/solver1.jpg
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
Rapport/Illustrations/solver2.jpg
Normal file
After Width: | Height: | Size: 173 KiB |
BIN
Rapport/Illustrations/splitscreen.PNG
Normal file
After Width: | Height: | Size: 495 KiB |
BIN
Rapport/Illustrations/state_1.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
Rapport/Illustrations/state_2.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
Rapport/Illustrations/state_3.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
Rapport/Illustrations/state_4.png
Normal file
After Width: | Height: | Size: 190 KiB |
BIN
Rapport/Illustrations/unicaen.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
Rapport/Rapport.odt
Normal file
BIN
Rapport/rapport.pdf
Normal file
512
Rapport/rapport.tex
Normal 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é d’ajouter 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 d’avoir 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, qu’est-ce que c’est ?}
|
||||||
|
|
||||||
|
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 l’entrepô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 l’enchaî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 à l’algorithme 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, l’algorithme 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 à l’algorithme 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 à l’emplacement 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 d’exé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
14
config/constants.py
Normal 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
After Width: | Height: | Size: 62 KiB |
BIN
images/dark_box.png
Normal file
After Width: | Height: | Size: 563 KiB |
BIN
images/dot.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
images/floor.png
Normal file
After Width: | Height: | Size: 871 B |
BIN
images/player.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
images/wall.png
Normal file
After Width: | Height: | Size: 226 KiB |
0
interface/__init__.py
Normal file
13
interface/goback.py
Normal 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
@ -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
@ -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
@ -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
@ -0,0 +1,8 @@
|
|||||||
|
########
|
||||||
|
# ###
|
||||||
|
# # . #
|
||||||
|
# @ . #
|
||||||
|
# $$ #
|
||||||
|
## #
|
||||||
|
# #
|
||||||
|
########
|
9
levels/level2.sok
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#########
|
||||||
|
# ###
|
||||||
|
# # ##
|
||||||
|
# @$. # #
|
||||||
|
# .$ #
|
||||||
|
# $. #
|
||||||
|
# ##
|
||||||
|
## ###
|
||||||
|
#########
|
10
levels/level3.sok
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
##########
|
||||||
|
## # ##
|
||||||
|
# # . #
|
||||||
|
# $ #
|
||||||
|
# .$$ #
|
||||||
|
# @ . ##
|
||||||
|
# $. #
|
||||||
|
# # ###
|
||||||
|
# #
|
||||||
|
##########
|
15
levels/level4.sok
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
###############
|
||||||
|
# # #
|
||||||
|
# # @ # ##
|
||||||
|
# #
|
||||||
|
# # $. #
|
||||||
|
# . #
|
||||||
|
# # $ # #
|
||||||
|
# . . #
|
||||||
|
# $$ #
|
||||||
|
# # . #
|
||||||
|
# . # #
|
||||||
|
# # $$ #
|
||||||
|
## # ##
|
||||||
|
# # #
|
||||||
|
###############
|
17
levels/level5.sok
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#################
|
||||||
|
### #
|
||||||
|
## #
|
||||||
|
# # #
|
||||||
|
# $ . ##
|
||||||
|
# #
|
||||||
|
# .$ # #
|
||||||
|
# # ## . #
|
||||||
|
# $ #
|
||||||
|
# @ . #
|
||||||
|
# # $ $ #
|
||||||
|
## . $ #
|
||||||
|
# # #
|
||||||
|
# .. $ # #
|
||||||
|
# # # #
|
||||||
|
# # #
|
||||||
|
#################
|
18
levels/level6.sok
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
##################
|
||||||
|
# #
|
||||||
|
# . #
|
||||||
|
# . #
|
||||||
|
# . #
|
||||||
|
# $ $ ##
|
||||||
|
## . . # #
|
||||||
|
# $ @ #
|
||||||
|
# #
|
||||||
|
# . $ #
|
||||||
|
## $ $ ##
|
||||||
|
# $ . # #
|
||||||
|
# . #
|
||||||
|
# $ $. #
|
||||||
|
# #
|
||||||
|
# # #
|
||||||
|
# # # #
|
||||||
|
##################
|
20
levels/level7.sok
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
####################
|
||||||
|
# # ## #
|
||||||
|
# #
|
||||||
|
# ## #
|
||||||
|
## # # #
|
||||||
|
# @ #
|
||||||
|
# . $ . #
|
||||||
|
# # # #
|
||||||
|
# # . #
|
||||||
|
# # #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# $ $ . # #
|
||||||
|
# $ . #
|
||||||
|
# $ . # #
|
||||||
|
## $ #
|
||||||
|
# # #
|
||||||
|
# # # #
|
||||||
|
# # #
|
||||||
|
####################
|
25
levels/level8.sok
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#########################
|
||||||
|
# # #
|
||||||
|
# . #
|
||||||
|
# . # . #
|
||||||
|
# @ # $ # #
|
||||||
|
# # #
|
||||||
|
# # #
|
||||||
|
# # $ # #
|
||||||
|
# # $ $ #
|
||||||
|
# # # $ . #
|
||||||
|
# # $ #
|
||||||
|
# # . #
|
||||||
|
# # #
|
||||||
|
# # # #
|
||||||
|
# $ #
|
||||||
|
# # # #
|
||||||
|
# # #
|
||||||
|
# # # #
|
||||||
|
# . # ##
|
||||||
|
# . # # #
|
||||||
|
# # # #
|
||||||
|
# # # # #
|
||||||
|
# # # # # #
|
||||||
|
# # #
|
||||||
|
#########################
|
30
levels/level9.sok
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
##############################
|
||||||
|
# #
|
||||||
|
# . # # #
|
||||||
|
# # # #
|
||||||
|
# # # # #
|
||||||
|
# $ #
|
||||||
|
# # #
|
||||||
|
# # # #
|
||||||
|
# # $ #
|
||||||
|
# #
|
||||||
|
# # # $ #
|
||||||
|
## # $@ # #
|
||||||
|
# . # ##
|
||||||
|
# # #
|
||||||
|
# . # #
|
||||||
|
# # # # $ #
|
||||||
|
# # # #
|
||||||
|
# # . . # #
|
||||||
|
## # # # #
|
||||||
|
# # ## ##
|
||||||
|
# # # #
|
||||||
|
## # # ##
|
||||||
|
# # $ #
|
||||||
|
# ### #
|
||||||
|
# . . # # #
|
||||||
|
# # # ##
|
||||||
|
## $ # #
|
||||||
|
# # # # #
|
||||||
|
# # # #
|
||||||
|
##############################
|
0
logic/__init__.py
Normal file
35
logic/context.py
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
48
map/air.py
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
106
solver/astar.py
Normal 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
@ -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
|
8
solver/pathNotFoundException.py
Normal 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
@ -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
@ -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
@ -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
|