Merge pull request 'First version' (#1) from dev into master

Reviewed-on: saxodwarf/Devinddessin#1
This commit is contained in:
saxodwarf 2024-04-14 22:40:50 +02:00
commit bd06c21a38
4 changed files with 539 additions and 1 deletions

6
.gitignore vendored
View File

@ -138,3 +138,9 @@ dmypy.json
# Cython debug symbols
cython_debug/
*_creations/
*.exe
*.zip
*.csv
*.txt

View File

@ -1,2 +1,61 @@
# Devinddessin
# Devine-dessin
Jeu permettant de dessiner un mot choisi au hasard dans une liste.
Tant que le jeu n'est pas quitté, les mots de la liste ne sont pas proposé à nouveau.
## Liste de mot
La liste de mot peut être au format texte brut, avec un mot par ligne, ou au format CSV.
Si la liste est au format CSV, la première colonne doit contenir le mot à dessiner, et la seconde colonne peut contenir un indice.
Les colonnes du fichier CSV ne doivent pas avoir de titre.
Par défaut, le jeu cherche une liste dans un fichier du dossier courant nommé `mots.csv`.
## Installation
Le jeu n'est pour l'instant pas packagé.
Pour le lancer:
- Construire une liste de mot comme présenté plus haut
- Créer un venv : `python -m venv <path_to_venv>`
- Activer le venv : `. <path_to_venv>/bin/activate`
- Installer les dépendances : `pip install -r requirements.txt`
- Récupérer le fichier `guess_what_I_draw.py`
- Lancer le jeu en exécutant le fichier.
## Contrôles:
### Tout le temps:
- **`Échap` :** Quitter le jeu immédiatement
### Quand le jeu n'est pas lancé:
- **`c`:** Ouvrir le menu de configuration
- **`Espace` :** Lancer la manche
### Quand le jeu est en cours:
- **Clic gauche** dans la zone de dessin : dessiner
- **`Ctrl + z` :** Annuler le dernier trait
- **`Entrer` :** Passer le mot en cours (une pénalité de temps est appliquée)
- **`Espace` :** Valider le mot en cours comme "trouvé"
- **`p` :** Activer/Désactiver la pause.
### Quand la manche est écoulée:
Le menu de récapitulation est affiché.
- **`Backspace` :** Passer au tour suivant
## Configuration
Dans le menu de configuration, il est possible de configurer :
- Le chemin vers la liste de mot ;
- La durée d'une manche ;
- Le temps de pénalité appliqué pour chaque mot passé.
Une fois la configuration effectuée, vous pouvez l'appliquer en cliquant en dehors de la pop-up du menu
## Dessins
Les dessins sont sauvegardés pour la postérité dans un dossier créé dans le répertoire courant, nommé `<date au format iso>T<heure-minutes>_creations`.
Le nom des fichiers de dessin sont au format `round-<i>_<mot à deviner>.png`, avec `i` le numéro de la manche.

473
guess_what_I_draw.py Executable file
View File

@ -0,0 +1,473 @@
#!/usr/bin/env python3
import re
import os
import csv
from random import random, randint, choice
from datetime import timedelta, datetime
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button, ButtonBehavior
from kivy.uix.label import Label
from kivy.graphics import Color, Ellipse, Line, Rectangle
from kivy.core.window import Window
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.clock import Clock
from kivy.uix.popup import Popup
from kivy.uix.colorpicker import ColorPicker
from kivy.uix.scrollview import ScrollView
from kivy.properties import StringProperty, BooleanProperty
from kivy.lang import Builder
from kivy.uix.textinput import TextInput
PASS_PENALTY = 4
DRAWING_TIME = 60
WORDS_FILE = "mots.csv"
Builder.load_string('''
<ScrollableLabel>:
Label:
size_hint_y: None
height: self.texture_size[1]
text_size: self.width, None
text: root.text
markup: root.markup
''')
class ScrollableLabel(ScrollView):
text = StringProperty('')
markup = BooleanProperty(False)
class NumberInput(TextInput):
def __init__(self, init_value, **kwargs):
self.init_value = init_value
kwargs["text"] = str(init_value)
super().__init__(**kwargs)
pat = r"^[0-9]+$"
def insert_text(self, substring, from_undo=False):
if not re.match(self.pat, substring):
s=''
else:
s = substring
super().insert_text(s, from_undo=from_undo)
class MyPaintWidget(Label):
_game_state = {"game_started": False,
"game_paused": False,
"game_finished": False,
"word passed": timedelta(0),
"word found": timedelta(0)
}
_game_wordlists = {"possible_wordlist": dict(),
"found_words": set(),
"unfound_words": set()
}
pen_characteristics = {"current_color": (0,0,0,1),
"current_width": 4
}
bg = None
current_round = 0
current_word = ""
message_label = None
time = timedelta(seconds=DRAWING_TIME)
_popup = None
def __init__(self, text="", **kwargs):
super().__init__(**kwargs)
self._keyboard = None
self._load_words()
with self.canvas.before:
Color(1,1,1, mode='rgb')
self.bg = Rectangle(pos=self.pos, size=self.size)
def resize_bg(self, obj):
self.bg.size = self.size
self.bg.pos = self.pos
self.bind(size=resize_bg, pos=resize_bg)
self._get_keyboard()
self._output_dir = datetime.now().isoformat(timespec='minutes').replace(":","-") + "_creations"
self._fullscreen = False
self.reset_game()
def _load_words(self):
try:
if WORDS_FILE.endswith(".csv"):
# read a CSV file
with open(WORDS_FILE, "r", newline='', encoding="utf-8") as wordfile:
my_reader = csv.reader(wordfile)
self._game_wordlists["possible_wordlist"] = {line[0]:
line[1] for line in my_reader}
else:
# read a plain text file
with open(WORDS_FILE, "r", encoding="utf-8") as wordfile:
self._wordlist = (word.strip() for word in wordfile.readlines())
self._game_wordlists["possible_wordlist"] = dict.fromkeys(self._wordlist)
except OSError:
errmsg = f"Erreur de lecture du fichier '{WORDS_FILE}'"
if self.bg:
Popup(title="Error", content=Label(text=errmsg)).open()
else:
print(errmsg)
self._game_wordlists["possible_wordlist"] = dict()
def _get_keyboard(self):
if self._keyboard is None:
self._keyboard = Window.request_keyboard(
self._keyboard_closed, self, 'text')
if self._keyboard.widget:
# If it exists, this widget is a VKeyboard object which you can use
# to change the keyboard layout.
pass
self._keyboard.bind(on_key_down=self._on_keyboard_down)
def _keyboard_closed(self):
print('My keyboard has been closed!')
self._keyboard.unbind(on_key_down=self._on_keyboard_down)
self._keyboard = None
def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
print('The key', keycode, 'have been pressed')
print(' - text is %r' % text)
print(' - modifiers are %r' % modifiers)
if not self._game_state["game_started"]:
if keycode[0] == 32:
self.start_game()
elif keycode[1] == 'c':
self.open_config()
elif keycode[1] == 'p':
self.toggle_pause()
elif self.is_game_playing:
if keycode[0] == 32:
self.next_word()
elif keycode[0] == 13:
self.pass_word()
elif keycode[1] == 'z' and "ctrl" in modifiers:
if self.canvas.after.children:
self.canvas.after.remove(self.canvas.after.children[-1])
self.canvas.after.remove(self.canvas.after.children[-1])
self.canvas.after.remove(self.canvas.after.children[-1])
elif self._game_state["game_finished"]:
if keycode[0] == 8:
self.reset_game()
if keycode[0] == 292:
print(Window.fullscreen)
if self._fullscreen:
Window.fullscreen = False
else:
Window.fullscreen = "auto"
self._fullscreen = not self._fullscreen
# Keycode is composed of an integer + a string
# If we hit escape, let the input through to leave the app
if keycode[1] == 'escape':
return False
# Return True to accept the key. Otherwise, it will be used by
# the system.
return True
def toggle_pause(self):
self._game_state["game_paused"] = not self._game_state["game_paused"]
def start_game(self):
self.text=""
self._game_state["game_started"] = True
self._game_state["game_paused"] = False
self.current_round += 1
self.next_word(True)
def next_word(self, passed=False):
if not passed:
self._game_wordlists["found_words"].add(self.current_word)
if self.message_label and self.current_word:
if passed:
self.message_label.color = '#FF2A40'
self.message_label.text = f'"{self.current_word}" a été passé !'
else:
self.message_label.color = '#5BB834'
self.message_label.text = f'"{self.current_word}" a été trouvé !'
if not self._game_wordlists["possible_wordlist"]:
self.time = timedelta(0)
return
self.save_frame()
self.current_word = choice(list(self._game_wordlists["possible_wordlist"]))
value = self._game_wordlists["possible_wordlist"].pop(self.current_word)
if value:
self.message_label.text += '\n'+f'Le prochain mot est de type: {value}'
self.clear_drawing()
def pass_word(self):
self._game_wordlists["unfound_words"].add(self.current_word)
self.next_word(True)
if self.time - timedelta(seconds=PASS_PENALTY) > timedelta(0):
self.time -= timedelta(seconds=PASS_PENALTY)
else:
self.time = timedelta(0)
def save_frame(self):
if self.current_word:
if not os.path.isdir(self._output_dir):
os.mkdir(self._output_dir)
filename = os.path.join(self._output_dir,
f"round-{self.current_round}_" + os.path.normcase(self.current_word) + ".png")
self.export_to_png(filename)
@property
def is_game_playing(self):
return (self._game_state["game_started"]
and not self._game_state["game_paused"]
and not self._game_state["game_finished"])
@property
def current_color(self):
return self.pen_characteristics["current_color"]
def on_touch_down(self, touch):
if self.is_game_playing:
with self.canvas.after:
if self.collide_point(touch.x, touch.y):
Color(*self.pen_characteristics["current_color"], mode='rgba')
diameter = self.pen_characteristics["current_width"]
Ellipse(pos=(touch.x - diameter / 2, touch.y - diameter / 2),
size=(2*diameter, 2*diameter))
touch.ud['line'] = Line(points=(touch.x, touch.y), width=diameter)
def on_touch_move(self, touch):
if self.is_game_playing:
if 'line' in touch.ud:
if self.collide_point(touch.x, touch.y):
touch.ud['line'].points += [touch.x, touch.y]
elif self.collide_point(touch.ud['line'].points[-2], touch.y):
touch.ud['line'].points += [touch.ud['line'].points[-2], touch.y]
elif self.collide_point(touch.x, touch.ud['line'].points[-1]):
touch.ud['line'].points += [touch.x, touch.ud['line'].points[-1]]
def open_config(self):
layout = GridLayout()
layout.cols=2
layout.add_widget(Label(text="Chemin vers la liste de mots"))
layout.add_widget(TextInput(text=WORDS_FILE, multiline=False))
layout.add_widget(Label(text="Temps de dessin\n(secondes"))
layout.add_widget(NumberInput(DRAWING_TIME, multiline=False))
layout.add_widget(Label(text="Temps de pénalité\nquand on passe\n(secondes)"))
layout.add_widget(NumberInput(PASS_PENALTY, multiline=False))
conf_popup = Popup(title='Configuration', content=layout, size_hint=(0.5, 0.5))
conf_popup.bind(on_dismiss=self.apply_conf)
conf_popup.open()
def apply_conf(self, obj):
global DRAWING_TIME
global PASS_PENALTY
global WORDS_FILE
try:
drawing_time = int(obj.content.children[2].text)
if drawing_time > 0:
DRAWING_TIME = drawing_time
except ValueError:
Popup(title="Error", content=Label(text="Mauvaise valeur pour temps de dessin")).open()
try:
pass_penalty = int(obj.content.children[0].text)
if pass_penalty > 0 and pass_penalty < drawing_time:
PASS_PENALTY = pass_penalty
except ValueError:
Popup(title="Error", content=Label(text="Mauvaise valeur pour pénalité")).open()
self.reset_game()
if obj.content.children[4].text != WORDS_FILE:
old_file = WORDS_FILE
WORDS_FILE = obj.content.children[4].text
try:
self._load_words()
except OSError:
Popup(title="Error", content=Label(text=f"Erreur de lecture du fichier '{WORDS_FILE}'")).open()
WORDS_FILE = old_file
self._get_keyboard()
def recap(self):
found_words = self._game_wordlists['found_words']
unfound_words =self._game_wordlists['unfound_words']
lines = ["[b]Tour terminé ![/b]\n"+
f"Trouvés: [color=#5BB834]{len(found_words)}[/color]",
"-"+ "\n-".join(found_words) if found_words else "",
f"Passés: [color=#FF2A40]{len(unfound_words)}[/color]",
"- "+ "\n- ".join(unfound_words) if unfound_words else ""]
self.save_frame()
self.clear_drawing()
self._popup = Popup(title='Fini !',
content=ScrollableLabel(text="[size=24]"+"\n".join(lines)+"[/size]",
markup=True),
size_hint=(0.6,0.6),
auto_dismiss=False)
self._popup.open()
def game_finished(self):
self._game_state["game_finished"] = True
self.recap()
def reset_game(self):
if self._popup:
self._popup.dismiss()
if self.message_label:
self.message_label.text = ""
self.color = (0,0,0,1)
self.text = "Appuyer sur 'espace' pour commencer à jouer"
self.current_word = ""
self._game_wordlists["found_words"] = set()
self._game_wordlists["unfound_words"] = set()
self.time = timedelta(seconds=DRAWING_TIME)
self._game_state["game_finished"] = False
self._game_state["game_started"] = False
def clear_drawing(self):
self.canvas.after.clear()
class SizeButton(ButtonBehavior, Widget):
def __init__(self, callback, size=1, **kwargs):
super().__init__(**kwargs)
self.dot_size = size
self.callback = callback
with self.canvas:
Color(0,0,0, mode="rgb")
self.bg = Rectangle(pos=self.pos, size=self.size)
Color(1,1,1, mode="rgb")
self.dot = Ellipse(pos=(self.center_x-size/2, self.center_y-size/2),
size = (size*2, size*2))
def redraw(self, args):
self.dot.pos = (self.center_x-size/2, self.center_y-size/2)
self.bg.pos = self.pos
self.bg.size = self.size
self.bind(size=redraw, pos=redraw)
def on_release(self):
self.callback(self.dot_size)
class GuessWhatIDrawApp(App):
state = False
init = True
def build(self):
Window.clearcolor = (1, 1, 1, 1)
#self.init_keyboard()
parent = BoxLayout(orientation='vertical')
self.painter = MyPaintWidget(size_hint=(0.9, 1))
Clock.schedule_interval(self.update_time, 1.0/10.0)
clearbtn = Button(text='Clear')
clearbtn.bind(on_release=self.clear_canvas)
togglebtn = Button(text='Inverser la couleur du fond')
togglebtn.bind(on_release=self.toggle_background)
self.right_row = BoxLayout(orientation="vertical", size_hint=(0.1,1))
color_picker_button = Button(text="Couleur")
color_picker_button.background_color = self.painter.pen_characteristics["current_color"]
color_picker_button.background_normal = ''
color_picker_button.bind(on_release=self.open_color_picker)
self.right_row.add_widget(color_picker_button)
for i in range(1,6):
size_button = SizeButton(self.set_cursor_size, 2*i)
self.right_row.add_widget(size_button)
middle_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.75))
middle_row.add_widget(self.painter)
middle_row.add_widget(self.right_row)
bottom_row = BoxLayout(orientation='horizontal', spacing=5, size_hint=(1, .05))
bottom_row.add_widget(togglebtn)
bottom_row.add_widget(clearbtn)
top_row = BoxLayout(orientation='horizontal', spacing=5, size_hint=(1, .2))
self.word_label = Label(text="Mot:", color=[0,0,0,1], halign="left", valign="top", size_hint=(0.2,1))
def redraw(self, obj):
self.text_size = self.size
self.word_label.bind(size=redraw)
self.time_label = Label(text=f"{self.painter.time}", color=[0,0,0,1],
font_size='50pt', size_hint=(0.2,1))
message_label = Label(color=[0,0,0,1], halign="center", valign="top",
font_size='40pt', size_hint=(0.6,1))
message_label.bind(size=redraw)
top_row.add_widget(self.word_label)
top_row.add_widget(message_label)
top_row.add_widget(self.time_label)
self.painter.message_label = message_label
parent.add_widget(top_row)
parent.add_widget(middle_row)
parent.add_widget(bottom_row)
return parent
def set_cursor_size(self, size):
print(size)
self.painter.pen_characteristics["current_width"] = size
def open_color_picker(self, _):
clr_picker = ColorPicker()
clr_picker.color = self.painter.pen_characteristics["current_color"]
def on_color_change(instance, value):
self.painter.pen_characteristics["current_color"] = value
self.right_row.children[-1].background_color = self.painter.current_color
clr_picker.bind(color=on_color_change)
popup = Popup(title="Couleur du pinceau",
content=clr_picker,
size_hint=(0.5,0.5))
popup.open()
def update_time(self, _):
if self.painter.is_game_playing:
if self.painter.time > timedelta(0):
self.painter.time -= timedelta(milliseconds=100)
self.word_label.text = f"Mot: {self.painter.current_word}"
else:
self.painter.game_finished()
self.time_label.text = f"{formatTimedelta(self.painter.time)}"
def toggle_background(self, obj):
if self.state:
Window.clearcolor = (1, 1, 1, 1)
with self.painter.canvas.before:
Color(1,1,1, mode='rgb')
self.painter.bg = Rectangle(pos=self.painter.pos, size=self.painter.size)
self.painter.pen_characteristics["current_color"] = (0,0,0,1)
self.right_row.children[-1].background_color = self.painter.current_color
self.word_label.color = (0,0,0,1)
self.time_label.color = (0,0,0,1)
self.state = False
else:
Window.clearcolor = (0,0,0,1)
with self.painter.canvas.before:
Color(0,0,0, mode='rgb')
self.painter.bg = Rectangle(pos=self.painter.pos, size=self.painter.size)
self.painter.pen_characteristics["current_color"] = (1,1,1,1)
self.right_row.children[-1].background_color = self.painter.current_color
self.word_label.color = (1,1,1,1)
self.time_label.color = (1,1,1,1)
self.state = True
self.clear_canvas(obj)
def clear_canvas(self, _):
self.painter.clear_drawing()
def formatTimedelta(time:timedelta):
seconds = int(time.total_seconds())
minutes, reminder = divmod(seconds, 60)
return f"{minutes:02}:{reminder:02}.{int(time.microseconds/10000):02}"
if __name__ == '__main__':
GuessWhatIDrawApp().run()

BIN
pictionnary.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B