#!/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(''' : 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) Color(0,0,0, mode='rgb') self.outline = Line(rectangle=(*self.pos, *self.size)) def resize_bg(self, obj): self.bg.size = self.size self.bg.pos = self.pos self.outline.rectangle=(*self.pos, *self.size) 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()