From 85da9981d1531775dcea84d15a443bc1e8e49dbf Mon Sep 17 00:00:00 2001 From: Adrien Bourmault Date: Fri, 14 Apr 2023 09:51:55 +0200 Subject: [PATCH] Commit initial --- fltk.py | 844 +++++++++++++++++++++++++++++++++++++++++++++ maps/map1.txt | 10 + maps/map2.txt | 10 + maps/map3.txt | 10 + maps/map4.txt | 10 + maps/map_test.txt | 10 + media/Dragon_s.png | Bin 0 -> 7284 bytes media/Knight_s.png | Bin 0 -> 9446 bytes media/treasure.png | Bin 0 -> 3345 bytes tiles.txt | 11 + 10 files changed, 905 insertions(+) create mode 100644 fltk.py create mode 100644 maps/map1.txt create mode 100644 maps/map2.txt create mode 100644 maps/map3.txt create mode 100644 maps/map4.txt create mode 100644 maps/map_test.txt create mode 100644 media/Dragon_s.png create mode 100644 media/Knight_s.png create mode 100644 media/treasure.png create mode 100644 tiles.txt diff --git a/fltk.py b/fltk.py new file mode 100644 index 0000000..9957508 --- /dev/null +++ b/fltk.py @@ -0,0 +1,844 @@ +import subprocess +import sys +import tkinter as tk +from collections import deque +from os import system +from pathlib import Path +from time import sleep, time +from tkinter import PhotoImage +from tkinter.font import Font +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Deque, + Dict, + List, + Optional, + Set, + Tuple, + TypeVar, + Union, +) + + +try: + # noinspection PyUnresolvedReferences + from PIL import Image, ImageTk + + print("Bibliothèque PIL chargée.", file=sys.stderr) + PIL_AVAILABLE = True +except ImportError as e: + PIL_AVAILABLE = False + +if TYPE_CHECKING: + from typing_extensions import Literal + + Anchor = Literal["nw", "n", "ne", "w", "center", "e", "sw", "s", "se"] + TkEvent = tk.Event[tk.BaseWidget] +else: + Anchor = str + TkEvent = tk.Event +FltkEvent = Tuple[str, Optional[TkEvent]] + +__all__ = [ + # gestion de fenêtre + "cree_fenetre", + "ferme_fenetre", + "redimensionne_fenetre", + "mise_a_jour", + # dessin + "ligne", + "fleche", + "polygone", + "rectangle", + "cercle", + "point", + "image", + "texte", + "taille_texte", + # effacer + "efface_tout", + "efface", + # utilitaires + "attente", + "capture_ecran", + "touche_pressee", + "abscisse_souris", + "ordonnee_souris", + "hauteur_fenetre", + "largeur_fenetre", + # événements + "donne_ev", + "attend_ev", + "attend_clic_gauche", + "attend_fermeture", + "type_ev", + "abscisse", + "ordonnee", + "touche", +] + + +class CustomCanvas: + """ + Classe qui encapsule tous les objets tkinter nécessaires à la création + d'un canevas. + """ + + _on_osx = sys.platform.startswith("darwin") + + _ev_mapping = { + "ClicGauche": "", + "ClicMilieu": "", + "ClicDroit": "" if _on_osx else "", + "Deplacement": "", + "Touche": "", + "Redimension": "", + } + + _default_ev = ["ClicGauche", "ClicDroit", "Touche"] + + def __init__( + self, + width: int, + height: int, + refresh_rate: int = 100, + events: Optional[List[str]] = None, + resizing: bool = False, + ) -> None: + # width and height of the canvas + self.width = width + self.height = height + self.interval = 1 / refresh_rate + + # root Tk object + self.root = tk.Tk() + + # canvas attached to the root object + self.canvas = tk.Canvas( + self.root, width=width, height=height, highlightthickness=0 + ) + + # adding the canvas to the root window and giving it focus + self.canvas.pack(fill=tk.BOTH, expand=tk.YES) + self.root.resizable(width=resizing, height=resizing) + self.canvas.focus_set() + self.first_resize = True + + # binding events + self.ev_queue: Deque[FltkEvent] = deque() + self.pressed_keys: Set[str] = set() + self.events = CustomCanvas._default_ev if events is None else events + self.bind_events() + + # update for the first time + self.last_update = time() + self.root.update() + + if CustomCanvas._on_osx: + system( + """/usr/bin/osascript -e 'tell app "Finder" \ + to set frontmost of process "Python" to true' """ + ) + + def update(self) -> None: + t = time() + self.root.update() + sleep(max(0.0, self.interval - (t - self.last_update))) + self.last_update = time() + + def resize(self, width: int, height: int) -> None: + self.root.geometry(f"{int(width)}x{int(height)}") + + def bind_events(self) -> None: + self.root.protocol("WM_DELETE_WINDOW", self.event_quit) + self.canvas.bind("", self.event_resize) + self.canvas.bind("", self.register_key) + self.canvas.bind("", self.release_key) + for name in self.events: + self.bind_event(name) + + # noinspection PyUnresolvedReferences + def register_key(self, ev: TkEvent) -> None: + self.pressed_keys.add(ev.keysym) + + # noinspection PyUnresolvedReferences + def release_key(self, ev: TkEvent) -> None: + if ev.keysym in self.pressed_keys: + self.pressed_keys.remove(ev.keysym) + + def event_quit(self) -> None: + self.ev_queue.append(("Quitte", None)) + + # noinspection PyUnresolvedReferences + def event_resize(self, event: TkEvent) -> None: + if event.widget.widgetName == "canvas": + if self.width != event.width or self.height != event.height: + self.width, self.height = event.width, event.height + if not self.ev_queue or self.ev_queue[-1][0] != "Redimension": + self.ev_queue.append(("Redimension", event)) + + def bind_event(self, name: str) -> None: + e_type = CustomCanvas._ev_mapping.get(name, name) + + def handler(event: TkEvent, _name: str = name) -> None: + self.ev_queue.append((_name, event)) + + self.canvas.bind(e_type, handler, "+") + + def unbind_event(self, name: str) -> None: + e_type = CustomCanvas._ev_mapping.get(name, name) + self.canvas.unbind(e_type) + + +__canevas: Optional[CustomCanvas] = None +__img: Dict[Tuple[Path, int, int], PhotoImage] = {} + + +############################################################################# +# Exceptions +############################################################################# + + +class TypeEvenementNonValide(Exception): + pass + + +class FenetreNonCree(Exception): + pass + + +class FenetreDejaCree(Exception): + pass + + +Ret = TypeVar("Ret") + + +def _fenetre_cree(func: Callable[..., Ret]) -> Callable[..., Ret]: + def new_func(*args: Any, **kwargs: Any) -> Ret: + if __canevas is None: + raise FenetreNonCree( + 'La fenêtre n\'a pas été crée avec la fonction "cree_fenetre".' + ) + return func(*args, **kwargs) + + return new_func + + +############################################################################# +# Initialisation, mise à jour et fermeture +############################################################################# + + +def cree_fenetre( + largeur: int, hauteur: int, frequence: int = 100, + redimension: bool = False +) -> None: + """ + Crée une fenêtre de dimensions ``largeur`` x ``hauteur`` pixels. + :rtype: + """ + global __canevas + if __canevas is not None: + raise FenetreDejaCree( + 'La fenêtre a déjà été crée avec la fonction "cree_fenetre".' + ) + __canevas = CustomCanvas(largeur, hauteur, frequence, resizing=redimension) + + +@_fenetre_cree +def ferme_fenetre() -> None: + """ + Détruit la fenêtre. + """ + global __canevas + assert __canevas is not None + __canevas.root.destroy() + __canevas = None + + +@_fenetre_cree +def redimensionne_fenetre(largeur: int, hauteur: int) -> None: + """ + Fixe les dimensions de la fenêtre à (``hauteur`` x ``largeur``) pixels. + + Le contenu du canevas n'est pas automatiquement mis à l'échelle et doit + être redessiné si nécessaire. + """ + assert __canevas is not None + __canevas.resize(width=largeur, height=hauteur) + + +@_fenetre_cree +def mise_a_jour() -> None: + """ + Met à jour la fenêtre. Les dessins ne sont affichés qu'après + l'appel à cette fonction. + """ + assert __canevas is not None + __canevas.update() + + +############################################################################# +# Fonctions de dessin +############################################################################# + + +# Formes géométriques + + +@_fenetre_cree +def ligne( + ax: float, + ay: float, + bx: float, + by: float, + couleur: str = "black", + epaisseur: float = 1, + tag: str = "", +) -> int: + """ + Trace un segment reliant le point ``(ax, ay)`` au point ``(bx, by)``. + + :param float ax: abscisse du premier point + :param float ay: ordonnée du premier point + :param float bx: abscisse du second point + :param float by: ordonnée du second point + :param str couleur: couleur de trait (défaut 'black') + :param float epaisseur: épaisseur de trait en pixels (défaut 1) + :param str tag: étiquette d'objet (défaut : pas d'étiquette) + :return: identificateur d'objet + """ + assert __canevas is not None + return __canevas.canvas.create_line( + ax, ay, bx, by, fill=couleur, width=epaisseur, tags=tag + ) + + +@_fenetre_cree +def fleche( + ax: float, + ay: float, + bx: float, + by: float, + couleur: str = "black", + epaisseur: float = 1, + tag: str = "", +) -> int: + """ + Trace une flèche du point ``(ax, ay)`` au point ``(bx, by)``. + + :param float ax: abscisse du premier point + :param float ay: ordonnée du premier point + :param float bx: abscisse du second point + :param float by: ordonnée du second point + :param str couleur: couleur de trait (défaut 'black') + :param float epaisseur: épaisseur de trait en pixels (défaut 1) + :param str tag: étiquette d'objet (défaut : pas d'étiquette) + :return: identificateur d'objet + """ + x, y = (bx - ax, by - ay) + n = (x ** 2 + y ** 2) ** 0.5 + x, y = x / n, y / n + points = [ + bx, + by, + bx - x * 5 - 2 * y, + by - 5 * y + 2 * x, + bx - x * 5 + 2 * y, + by - 5 * y - 2 * x, + ] + assert __canevas is not None + return __canevas.canvas.create_polygon( + points, fill=couleur, outline=couleur, width=epaisseur, tags=tag + ) + + +@_fenetre_cree +def polygone( + points: List[float], + couleur: str = "black", + remplissage: str = "", + epaisseur: float = 1, + tag: str = "", +) -> int: + """ + Trace un polygone dont la liste de points est fournie. + + :param list points: liste de couples (abscisse, ordonnee) de points + :param str couleur: couleur de trait (défaut 'black') + :param str remplissage: couleur de fond (défaut transparent) + :param float epaisseur: épaisseur de trait en pixels (défaut 1) + :param str tag: étiquette d'objet (défaut : pas d'étiquette) + :return: identificateur d'objet + """ + assert __canevas is not None + return __canevas.canvas.create_polygon( + points, fill=remplissage, outline=couleur, width=epaisseur, tags=tag + ) + + +@_fenetre_cree +def rectangle( + ax: float, + ay: float, + bx: float, + by: float, + couleur: str = "black", + remplissage: str = "", + epaisseur: float = 1, + tag: str = "", +) -> int: + """ + Trace un rectangle noir ayant les point ``(ax, ay)`` et ``(bx, by)`` + comme coins opposés. + + :param float ax: abscisse du premier coin + :param float ay: ordonnée du premier coin + :param float bx: abscisse du second coin + :param float by: ordonnée du second coin + :param str couleur: couleur de trait (défaut 'black') + :param str remplissage: couleur de fond (défaut transparent) + :param float epaisseur: épaisseur de trait en pixels (défaut 1) + :param str tag: étiquette d'objet (défaut : pas d'étiquette) + :return: identificateur d'objet + """ + assert __canevas is not None + return __canevas.canvas.create_rectangle( + ax, ay, bx, by, + outline=couleur, fill=remplissage, width=epaisseur, tags=tag + ) + + +@_fenetre_cree +def cercle( + x: float, + y: float, + r: float, + couleur: str = "black", + remplissage: str = "", + epaisseur: float = 1, + tag: str = "", +) -> int: + """ + Trace un cercle de centre ``(x, y)`` et de rayon ``r`` en noir. + + :param float x: abscisse du centre + :param float y: ordonnée du centre + :param float r: rayon + :param str couleur: couleur de trait (défaut 'black') + :param str remplissage: couleur de fond (défaut transparent) + :param float epaisseur: épaisseur de trait en pixels (défaut 1) + :param str tag: étiquette d'objet (défaut : pas d'étiquette) + :return: identificateur d'objet + """ + assert __canevas is not None + return __canevas.canvas.create_oval( + x - r, + y - r, + x + r, + y + r, + outline=couleur, + fill=remplissage, + width=epaisseur, + tags=tag, + ) + + +@_fenetre_cree +def arc( + x: float, + y: float, + r: float, + ouverture: float = 90, + depart: float = 0, + couleur: str = "black", + remplissage: str = "", + epaisseur: float = 1, + tag: str = "", +) -> int: + """ + Trace un arc de cercle de centre ``(x, y)``, de rayon ``r`` et + d'angle d'ouverture ``ouverture`` (défaut : 90 degrés, dans le sens + contraire des aiguilles d'une montre) depuis l'angle initial ``depart`` + (défaut : direction 'est'). + + :param float x: abscisse du centre + :param float y: ordonnée du centre + :param float r: rayon + :param float ouverture: abscisse du centre + :param float depart: ordonnée du centre + :param str couleur: couleur de trait (défaut 'black') + :param str remplissage: couleur de fond (défaut transparent) + :param float epaisseur: épaisseur de trait en pixels (défaut 1) + :param str tag: étiquette d'objet (défaut : pas d'étiquette) + :return: identificateur d'objet + """ + assert __canevas is not None + return __canevas.canvas.create_arc( + x - r, + y - r, + x + r, + y + r, + extent=ouverture, + start=depart, + style=tk.ARC, + outline=couleur, + fill=remplissage, + width=epaisseur, + tags=tag, + ) + + +@_fenetre_cree +def point( + x: float, y: float, + couleur: str = "black", epaisseur: float = 1, + tag: str = "" +) -> int: + """ + Trace un point aux coordonnées ``(x, y)`` en noir. + + :param float x: abscisse + :param float y: ordonnée + :param str couleur: couleur du point (défaut 'black') + :param float epaisseur: épaisseur de trait en pixels (défaut 1) + :param str tag: étiquette d'objet (défaut : pas d'étiquette) + :return: identificateur d'objet + """ + assert __canevas is not None + return cercle(x, y, epaisseur, + couleur=couleur, remplissage=couleur, tag=tag) + + +# Image + + +@_fenetre_cree +def image( + x: float, + y: float, + fichier: str, + largeur: Optional[int] = None, + hauteur: Optional[int] = None, + ancrage: Anchor = "center", + tag: str = "", +) -> int: + """ + Affiche l'image contenue dans ``fichier`` avec ``(x, y)`` comme centre. Les + valeurs possibles du point d'ancrage sont ``'center'``, ``'nw'``, + etc. Les arguments optionnels ``largeur`` et ``hauteur`` permettent de + spécifier des dimensions maximales pour l'image (sans changement de + proportions). + + :param largeur: largeur de l'image + :param hauteur: hauteur de l'image + :param float x: abscisse du point d'ancrage + :param float y: ordonnée du point d'ancrage + :param str fichier: nom du fichier contenant l'image + :param ancrage: position du point d'ancrage par rapport à l'image + :param str tag: étiquette d'objet (défaut : pas d'étiquette) + :return: identificateur d'objet + """ + assert __canevas is not None + if PIL_AVAILABLE: + tk_image = _load_pil_image(fichier, hauteur, largeur) + else: + tk_image = _load_tk_image(fichier, hauteur, largeur) + img_object = __canevas.canvas.create_image( + x, y, anchor=ancrage, image=tk_image, tags=tag + ) + return img_object + + +def _load_tk_image(fichier: str, + hauteur: Optional[int] = None, + largeur: Optional[int] = None) -> PhotoImage: + chemin = Path(fichier) + ph_image = PhotoImage(file=fichier) + largeur_o = ph_image.width() + hauteur_o = ph_image.height() + if largeur is None: + largeur = largeur_o + if hauteur is None: + hauteur = hauteur_o + zoom_l = max(1, largeur // largeur_o) + zoom_h = max(1, hauteur // hauteur_o) + red_l = max(1, largeur_o // largeur) + red_h = max(1, hauteur_o // hauteur) + largeur = largeur_o * zoom_l // red_l + hauteur = hauteur_o * zoom_h // red_h + if (chemin, largeur, hauteur) in __img: + return __img[(chemin, largeur, hauteur)] + ph_image = ph_image.zoom(zoom_l, zoom_h) + ph_image = ph_image.subsample(red_l, red_h) + __img[(chemin, largeur, hauteur)] = ph_image + return ph_image + + +def _load_pil_image(fichier: str, + hauteur: Optional[int] = None, + largeur: Optional[int] = None) -> PhotoImage: + chemin = Path(fichier) + img = Image.open(fichier) + if largeur is None: + largeur = img.width + if hauteur is None: + hauteur = img.height + if (chemin, largeur, hauteur) in __img: + return __img[(chemin, largeur, hauteur)] + img = img.resize((largeur, hauteur)) + ph_image = ImageTk.PhotoImage(img) + __img[(chemin, largeur, hauteur)] = ph_image # type:ignore + return ph_image # type:ignore + + +# Texte + + +@_fenetre_cree +def texte( + x: float, + y: float, + chaine: str, + couleur: str = "black", + ancrage: Anchor = "nw", + police: str = "Helvetica", + taille: int = 24, + tag: str = "", +) -> int: + """ + Affiche la chaîne ``chaine`` avec ``(x, y)`` comme point d'ancrage (par + défaut le coin supérieur gauche). + + :param float x: abscisse du point d'ancrage + :param float y: ordonnée du point d'ancrage + :param str chaine: texte à afficher + :param str couleur: couleur de trait (défaut 'black') + :param ancrage: position du point d'ancrage (défaut 'nw') + :param police: police de caractères (défaut : `Helvetica`) + :param taille: taille de police (défaut 24) + :param tag: étiquette d'objet (défaut : pas d'étiquette + :return: identificateur d'objet + """ + assert __canevas is not None + return __canevas.canvas.create_text( + x, y, + text=chaine, font=(police, taille), + tags=tag, fill=couleur, anchor=ancrage + ) + + +def taille_texte( + chaine: str, police: str = "Helvetica", taille: int = 24 +) -> Tuple[int, int]: + """ + Donne la largeur et la hauteur en pixel nécessaires pour afficher + ``chaine`` dans la police et la taille données. + + :param str chaine: chaîne à mesurer + :param police: police de caractères (défaut : `Helvetica`) + :param taille: taille de police (défaut 24) + :return: couple (w, h) constitué de la largeur et la hauteur de la chaîne + en pixels (int), dans la police et la taille données. + """ + font = Font(family=police, size=taille) + return font.measure(chaine), font.metrics("linespace") + + +############################################################################# +# Effacer +############################################################################# + + +@_fenetre_cree +def efface_tout() -> None: + """ + Efface la fenêtre. + """ + assert __canevas is not None + __canevas.canvas.delete("all") + + +@_fenetre_cree +def efface(objet_ou_tag: Union[int, str]) -> None: + """ + Efface ``objet`` de la fenêtre. + + :param: objet ou étiquette d'objet à supprimer + :type: ``int`` ou ``str`` + """ + assert __canevas is not None + __canevas.canvas.delete(objet_ou_tag) + + +############################################################################# +# Utilitaires +############################################################################# + + +def attente(temps: float) -> None: + start = time() + while time() - start < temps: + mise_a_jour() + + +@_fenetre_cree +def capture_ecran(file: str) -> None: + """ + Fait une capture d'écran sauvegardée dans ``file.png``. + """ + assert __canevas is not None + __canevas.canvas.postscript( # type: ignore + file=file + ".ps", + height=__canevas.height, + width=__canevas.width, + colormode="color", + ) + + subprocess.call( + "convert -density 150 -geometry 100% -background white -flatten" + " " + file + ".ps " + file + ".png", + shell=True, + ) + subprocess.call("rm " + file + ".ps", shell=True) + + +@_fenetre_cree +def touche_pressee(keysym: str) -> bool: + """ + Renvoie `True` si ``keysym`` est actuellement pressée. + :param keysym: symbole associé à la touche à tester. + :return: `True` si ``keysym`` est actuellement pressée, `False` sinon. + """ + assert __canevas is not None + return keysym in __canevas.pressed_keys + + +############################################################################# +# Gestions des évènements +############################################################################# + + +@_fenetre_cree +def donne_ev() -> Optional[FltkEvent]: + """ + Renvoie immédiatement l'événement en attente le plus ancien, + ou ``None`` si aucun événement n'est en attente. + """ + assert __canevas is not None + if not __canevas.ev_queue: + return None + return __canevas.ev_queue.popleft() + + +def attend_ev() -> FltkEvent: + """Attend qu'un événement ait lieu et renvoie le premier événement qui + se produit.""" + while True: + ev = donne_ev() + if ev is not None: + return ev + mise_a_jour() + + +def attend_clic_gauche() -> Tuple[int, int]: + """Attend qu'un clic gauche sur la fenêtre ait lieu et renvoie ses + coordonnées. **Attention**, cette fonction empêche la détection d'autres + événements ou la fermeture de la fenêtre.""" + while True: + ev = donne_ev() + if ev is not None and type_ev(ev) == "ClicGauche": + x, y = abscisse(ev), ordonnee(ev) + assert isinstance(x, int) and isinstance(y, int) + return x, y + mise_a_jour() + + +def attend_fermeture() -> None: + """Attend la fermeture de la fenêtre. Cette fonction renvoie None. + **Attention**, cette fonction empêche la détection d'autres événements.""" + while True: + ev = donne_ev() + if ev is not None and type_ev(ev) == "Quitte": + ferme_fenetre() + return + mise_a_jour() + + +def type_ev(ev: Optional[FltkEvent]) -> Optional[str]: + """ + Renvoie une chaîne donnant le type de ``ev``. Les types + possibles sont 'ClicDroit', 'ClicGauche', 'Touche' et 'Quitte'. + Renvoie ``None`` si ``evenement`` vaut ``None``. + """ + return ev if ev is None else ev[0] + + +def abscisse(ev: Optional[FltkEvent]) -> Optional[int]: + """ + Renvoie la coordonnée x associé à ``ev`` si elle existe, None sinon. + """ + x = attribut(ev, "x") + assert isinstance(x, int) or x is None + return x + + +def ordonnee(ev: Optional[FltkEvent]) -> Optional[int]: + """ + Renvoie la coordonnée y associé à ``ev`` si elle existe, None sinon. + """ + y = attribut(ev, "y") + assert isinstance(y, int) or y is None + return y + + +def touche(ev: Optional[FltkEvent]) -> str: + """ + Renvoie une chaîne correspondant à la touche associé à ``ev``, + si elle existe. + """ + keysym = attribut(ev, "keysym") + assert isinstance(keysym, str) + return keysym + + +def attribut(ev: Optional[FltkEvent], nom: str) -> Any: + if ev is None: + raise TypeEvenementNonValide( + f"Accès à l'attribut {nom} impossible sur un événement vide" + ) + tev, evtk = ev + if not hasattr(evtk, nom): + raise TypeEvenementNonValide( + f"Accès à l'attribut {nom} impossible " + f"sur un événement de type {tev}" + ) + attr = getattr(evtk, nom) + return attr if attr != "??" else None + + +@_fenetre_cree +def abscisse_souris() -> int: + assert __canevas is not None + return __canevas.canvas.winfo_pointerx() - __canevas.canvas.winfo_rootx() + + +@_fenetre_cree +def ordonnee_souris() -> int: + assert __canevas is not None + return __canevas.canvas.winfo_pointery() - __canevas.canvas.winfo_rooty() + + +@_fenetre_cree +def largeur_fenetre() -> int: + assert __canevas is not None + return __canevas.width + + +@_fenetre_cree +def hauteur_fenetre() -> int: + assert __canevas is not None + return __canevas.height diff --git a/maps/map1.txt b/maps/map1.txt new file mode 100644 index 0000000..c7c0e8c --- /dev/null +++ b/maps/map1.txt @@ -0,0 +1,10 @@ +╗╝╩╞╦╔ +╩╦╡╩╝╠ +╦╬╡╝╔╥ +╬═╥╩╚╗ +╨╦╬╔╣╗ +╬╨╩╨╞╔ +A 2 5 +D 4 2 1 +D 2 0 2 +D 4 0 3 diff --git a/maps/map2.txt b/maps/map2.txt new file mode 100644 index 0000000..5084f9c --- /dev/null +++ b/maps/map2.txt @@ -0,0 +1,10 @@ +╣╩╝╥╩╠ +╥╞╨╞╬╬ +═╚╠╡╝╥ +╨╦╝╡╬═ +║╗╝═╝╗ +╠╡╩╚╣╦ +A 4 1 +D 2 3 1 +D 3 5 2 +D 4 0 3 \ No newline at end of file diff --git a/maps/map3.txt b/maps/map3.txt new file mode 100644 index 0000000..b13be50 --- /dev/null +++ b/maps/map3.txt @@ -0,0 +1,10 @@ +╣╬╣╝╚║ +╝╬╩╨╚╗ +╠╩╗╔╦╗ +╬╥╬╡╚╔ +╥╞╗╩╦╞ +╦╩╚╔╦═ +A 4 0 +D 4 5 1 +D 2 5 2 +D 0 1 3 \ No newline at end of file diff --git a/maps/map4.txt b/maps/map4.txt new file mode 100644 index 0000000..eb7065c --- /dev/null +++ b/maps/map4.txt @@ -0,0 +1,10 @@ +╗╥║╠╚╝ +╨╥═╥╠╩ +╡╨╞╚═╔ +╠╣╝╝╩╗ +╩╣║╚╡╨ +╔╩╚╝╔╨ +A 5 2 +D 3 4 1 +D 5 1 2 +D 1 3 3 \ No newline at end of file diff --git a/maps/map_test.txt b/maps/map_test.txt new file mode 100644 index 0000000..0d612a9 --- /dev/null +++ b/maps/map_test.txt @@ -0,0 +1,10 @@ +╔╦╦╦╦╗ +╠╩╩╩╩╣ +╠═╗╔═╣ +╠═╣╠═╣ +╠╗╠╣╔╣ +╚╩╩╩╩╝ +A 0 2 +D 1 1 1 +D 2 0 2 +D 2 5 3 diff --git a/media/Dragon_s.png b/media/Dragon_s.png new file mode 100644 index 0000000000000000000000000000000000000000..288727f4c0ee9fada45b7276694a808af7b3ca85 GIT binary patch literal 7284 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 literal 0 HcmV?d00001 diff --git a/media/treasure.png b/media/treasure.png new file mode 100644 index 0000000000000000000000000000000000000000..4591ccf311a181ada5a665b3f0aa9a4eee41b710 GIT binary patch literal 3345 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 b!2