-
Notifications
You must be signed in to change notification settings - Fork 6
/
game_gia.py
400 lines (341 loc) · 15.3 KB
/
game_gia.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
import random
import configparser
import json
import os
from kivy.app import App, Builder
from kivy.uix.screenmanager import Screen
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.image import Image as ImgK
from kivy.core.image import Image as CoreImage
from kivy.core.window import Window
from PIL import Image, ImageDraw, ImageFont
from typing import List
import abc
import time
from io import BytesIO
from dataclasses import dataclass
import threading
import gia_algo
@dataclass
class AnswerDetails:
nb: int
question: list
answer: str
answering_time: float
correct: bool = False
functions_for_test = {'numbers':gia_algo.number_speed_and_accuracy,
'letters':gia_algo.perceptual_speed,
'rotated_r':gia_algo.spatial_visualisation,
'pairs':gia_algo.word_meaning,
'reasoning':gia_algo.reasoning}
class Menu(BoxLayout, Screen):
"""
Assigns the proper screen for a given test
set the needed extra layout
and generates the first question.
"""
def go_to_a_screen_test(self, screen_name: str):
""""""
app.root.current = screen_name
screen = app.root.current_screen
screen.screen_name = screen_name
screen.design()
func = functions_for_test[screen_name]
screen.update_layout_with_new_question(func)
screen.start_timer()
class MetaSingleTest(abc.ABCMeta,type(Screen)):
"""Combined meta class with abc and kivy"""
pass
class AbstractSingleTest(abc.ABC, Screen, metaclass=MetaSingleTest):
"""Abstract SingleTest class to implement the new metaclass"""
def __init__(self, **kwargs):
super(AbstractSingleTest, self).__init__(**kwargs)
class SingleTestInterface(BoxLayout, AbstractSingleTest):
"""Interface for all the different test screens classes"""
def __init__(self, **kwargs):
super(SingleTestInterface, self).__init__(**kwargs)
self.screen_name = ''
self.question: str = ''
self.answer: str = ''
self.score: int = 0
self.number_of_questions_answered: int = 0
self.widgets: dict = {'buttons': {}, 'labels': {}, 'images': {},'box':{}}
self.timer: threading.Timer = None
self.load_config()
self.details_results: List[AnswerDetails] = [] # To store all the details of each question answered
self.question_start_time: float = 0
# Read the timer duration from the configuration file and convert it to an integer
self.timer_duration = int(self.config['TIMER']['duration'])
def load_config(self):
self.config = configparser.ConfigParser()
if os.path.exists('config.ini'):
self.config.read('config.ini')
else:
self.config['TIMER'] = {'duration': '180'}
@classmethod
def __subclasshook__(cls, subclass):
return (hasattr(subclass, 'design') and
callable(subclass.design) and
hasattr(subclass, 'update_layout_with_new_question') and
callable(subclass.update_layout_with_new_question) or
NotImplemented)
@abc.abstractmethod
def design(self):
"""Designs specific layout for a given test"""
pass
@abc.abstractmethod
def update_layout_with_new_question(self, func):
"""Generates a new question and assigns it to the layout of a given test"""
self.question_start_time = time.time()
self.question,self.answer = func()
def create_new_detail_result(self):
detail = AnswerDetails(question= self.question,
answer= self.answer,
answering_time=time.time()-self.question_start_time,
nb=self.number_of_questions_answered
)
return detail
def check_answer_update_score_and_add_new_question(self, button: Button):
"""Compares the user's answer with the expected answer"""
# Create a new question detail
detail = self.create_new_detail_result()
self.details_results.append(detail)
# Checks if answer is correct and updtes score
if button.text == str(self.answer):
self.score += 1
# Set the question detail correct parameter to True
detail.correct = True
else:
self.score -= 1
# Get the proper update function to call
func = functions_for_test[self.screen_name]
# Generates a new question and updates the layout
self.update_layout_with_new_question(func)
self.number_of_questions_answered += 1
def stop_game(self):
""" When the timer is done, disable all buttons and show the user score"""
for value in self.widgets['buttons'].values():
value.disabled = True
self.ids.score_lbl.text = f'Your score is {self.score}\n' \
f'You answered {self.number_of_questions_answered} questions'
for detail in self.details_results:
print(f'Question: {detail.nb} was {detail.correct} in {round(detail.answering_time,1)} s')
def start_timer(self):
"""Timer to start when a test is starting"""
duration = self.config.getint('TIMER', 'duration', fallback=180)
self.timer = threading.Timer(duration, self.stop_game)
self.timer.start()
def remove_test_layout(self):
"""
Reset the screen and variables to their original layout/values
Set the 'menu' screen as the current one
"""
self.timer.cancel()
# Loop through the range of the length of children because
# when looping through the children, some were missed and
# I couldn't figure out why
for n in range(len(self.ids.boxtest.children)):
self.ids.boxtest.remove_widget(self.ids.boxtest.children[0])
self.ids.score_lbl.text = ''
self.score = 0
self.number_of_questions_answered = 0
app.root.current = 'menu'
class NumbersTest(SingleTestInterface):
def design(self):
layout_for_3_choices(self)
def update_layout_with_new_question(self, func):
super().update_layout_with_new_question(func)
update_layout_for_3_choices(self)
class LettersTest(SingleTestInterface):
def design(self):
h_layout = BoxLayout(orientation='vertical',
pos_hint={'center_x': 0.5, 'center_y': 0.5},
size_hint=(0.8, 0.7))
grid = GridLayout(cols=4,
spacing=30,
size_hint=(1, 0.5))
for n in range(8):
lbl = Label(text='',
font_size=60)
grid.add_widget(lbl)
self.widgets['labels'][f'letter_{str(n)}'] = lbl
answers_line = BoxLayout(orientation='horizontal',
size_hint=(1, 0.4))
for n in range(5):
a_btn = Button(text=str(n),
on_release=self.check_answer_update_score_and_add_new_question,
font_size=50,
disabled=False)
answers_line.add_widget(a_btn)
self.widgets['buttons'][f'answer_{str(n)}']=a_btn
h_layout.add_widget(grid)
h_layout.add_widget(Label(size_hint=(1, 0.2)))
h_layout.add_widget(answers_line)
self.ids.boxtest.add_widget(h_layout)
def update_layout_with_new_question(self, func):
super().update_layout_with_new_question(func)
for label, letter in zip(self.widgets['labels'],self.question):
self.widgets['labels'][label].text = letter
class RTest(SingleTestInterface):
def design(self):
RTest.LETTER = random.choice("R")
h_layout = BoxLayout(orientation='vertical',
pos_hint={'center_x': 0.5, 'center_y': 0.5},
size_hint=(0.8, 0.8))
grid = GridLayout(cols=2,
size_hint=(0.5, 0.8),
pos_hint={'center_x': 0.5, 'center_y': 0.5})
for n in range(4):
source = make_letter_image(RTest.LETTER, 0, 0)
img = ImgK()
img.texture = source.texture
grid.add_widget(img)
self.widgets['images'][f'image_{str(n)}'] = img
h_layout.add_widget(grid)
answers_line = BoxLayout(orientation='horizontal',
size_hint=(0.8, 0.3),
pos_hint={'center_x': 0.5, 'center_y': 0.5})
for n in range(3):
a_btn = Button(text=str(n),
on_release=self.check_answer_update_score_and_add_new_question,
font_size=50,
disabled=False)
answers_line.add_widget(a_btn)
self.widgets['buttons'][f'answer_{str(n)}']=a_btn
h_layout.add_widget(Label(size_hint=(1,0.2)))
h_layout.add_widget(answers_line)
self.ids.boxtest.add_widget(h_layout)
def update_layout_with_new_question(self, func):
# Real test seems to only use 'R', but you can make it harder by uncommenting below
# RTest.LETTER = random.choice("FGJLNPQRSZ")
super().update_layout_with_new_question(func)
R_data =[self.question[0][0],self.question[1][0],self.question[0][1],self.question[1][1]]
for data, image in zip(R_data,self.widgets['images'].values()):
source = make_letter_image(RTest.LETTER, data[0], data[1])
image.texture = source.texture
class PairsTest(SingleTestInterface):
def design(self):
layout_for_3_choices(self)
def update_layout_with_new_question(self, func):
super().update_layout_with_new_question(func)
update_layout_for_3_choices(self)
class ReasoningTest(SingleTestInterface):
def design(self):
h_layout = BoxLayout(orientation='vertical',
pos_hint={'center_x': 0.5, 'center_y': 0.7},
size_hint=(0.8, 0.8)
)
fact_question = Label(text="test",
font_size=40,
size_hint=(0.8, 0.8),
pos_hint={'center_x': 0.5, 'center_y': 0.5}
)
self.widgets['labels']['fact'] = fact_question
# Layout which holds either the See question button
# or the 2 answer buttons with names
button_layout = BoxLayout(size_hint=(0.4, 0.5),
pos_hint={'center_x': 0.5, 'center_y': 0.5}
)
self.widgets['box']['buttons'] = button_layout
# Create buttons but don't add them to the layout yet.
# Only when update_layout_with_new_question is called
see_question = Button(text='Get question',on_release=self.get_question, font_size = 30
)
name1 = Button(text='', on_release=self.check_answer_update_score_and_add_new_question, font_size = 40, )
name2 = Button(text='', on_release=self.check_answer_update_score_and_add_new_question, font_size = 40, )
self.widgets['buttons']['see_question']=see_question
self.widgets['buttons']['name1']=name1
self.widgets['buttons']['name2']=name2
# Add the label and buttons layout to the main layout
h_layout.add_widget(fact_question)
h_layout.add_widget(button_layout)
self.ids.boxtest.add_widget(h_layout)
def update_layout_with_new_question(self, func):
super().update_layout_with_new_question(func)
self.widgets['labels']['fact'].text = self.question[0]
# Remove the answers buttons
self.widgets['box']['buttons'].remove_widget(self.widgets['buttons']['name1'])
self.widgets['box']['buttons'].remove_widget(self.widgets['buttons']['name2'])
# Add the See question button
self.widgets['box']['buttons'].add_widget(self.widgets['buttons']['see_question'])
def get_question(self, d):
# Shuffle the 2 answer names so that the first one named in the fact
# is not always the first choice
shuffled_names = self.question[2]
random.shuffle(shuffled_names)
# Replace the fact text by the question text
self.widgets['labels']['fact'].text = self.question[1]
# Remove the See question button
self.widgets['box']['buttons'].size_hint = (0.8, 0.5)
self.widgets['box']['buttons'].remove_widget(self.widgets['buttons']['see_question'])
# Set the text on the answer buttons
self.widgets['buttons']['name1'].text = shuffled_names[0]
self.widgets['buttons']['name2'].text = shuffled_names[1]
# Add the 2 answer buttons to the layout
self.widgets['box']['buttons'].add_widget(self.widgets['buttons']['name1'])
self.widgets['box']['buttons'].add_widget(self.widgets['buttons']['name2'])
def layout_for_3_choices(screen):
"""Generates main layout for 3 choices questions"""
h_layout = BoxLayout(pos_hint={'center_x': 0.5, 'center_y': 0.5},
size_hint=(0.6, 0.6))
for n in range(3):
b_name = f'button_{n+1}'
button = Button(text='',
on_release=screen.check_answer_update_score_and_add_new_question,
font_size=30,
disabled=False)
screen.widgets['buttons'][b_name] = button
h_layout.add_widget(button)
screen.ids.boxtest.add_widget(h_layout)
def update_layout_for_3_choices(screen):
"""Assign the 3 numbers from the question to the text of the 3 buttons"""
for i, value in enumerate(screen.widgets['buttons'].values()):
value.text = str(screen.question[i])
def make_letter_image(letter, side, angle):
"""
Generates a PIL Image of a drawn letter with a given side and angle
Args:
letter (str): The letter to draw on the image
side (int): Either 0 or 1; 0 for normal orientation, 1 for flipped horizontally
angle (float): The angle to rotate the image, in degrees
Returns:
kivy.core.image.CoreImage: The resulting CoreImage
"""
# Define text font
font_size = 85
try:
fnt = ImageFont.truetype('arial.ttf', font_size)
except OSError:
fnt = ImageFont.load_default(font_size)
# Create a new PIL image
image = Image.new(mode="RGB", size=(150, 150), color="white")
# Draw the letter on the image
draw = ImageDraw.Draw(image)
draw.text((40, 40), letter, font=fnt, fill='black', align='center', stroke_width=1,
stroke_fill="black")
# Rotate the image
image = image.rotate(angle)
# Flip the image horizontally if needed
if side == 1:
image = image.transpose(method=Image.FLIP_LEFT_RIGHT)
# Convert the image to bytes
data = BytesIO()
image.save(data, format='png')
data.seek(0)
# Generates a Kivy CoreImage
image = CoreImage(BytesIO(data.read()), ext='png')
return image
class MainApp(App):
def build(self):
print(Window.size)
file = Builder.load_file('gia_screens.kv')
return file
def on_stop(self):
if self.root.current_screen.timer is not None:
self.root.current_screen.timer.cancel()
if __name__ == '__main__':
app = MainApp()
app.run()