Coverage for src/ensae_teaching_cs/helpers/pygame_helper.py: 79%

147 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-01-27 05:44 +0100

1""" 

2@file 

3@brief pygame helpers 

4 

5The module pygame is not imported in this module but sent 

6to every function as a parameter to avoid importing 

7the module if not needed. 

8""" 

9import math 

10 

11 

12MOUSE = "mouse" 

13KEY = "key" 

14 

15 

16def wait_event(pygame): 

17 """ 

18 The function waits for an event, a 

19 

20 @param pygame module pygame 

21 """ 

22 while True: 

23 for event in pygame.event.get(): 

24 if event.type == pygame.MOUSEBUTTONUP: 

25 return (MOUSE, event.button, event.pos[0], event.pos[1]) 

26 elif event.type == pygame.KEYUP: 

27 if event.key == 27: 

28 return None 

29 else: 

30 return (KEY, event.key) 

31 elif event.type == pygame.QUIT: 

32 return None 

33 

34 

35def empty_main_loop(pygame, msg=None): 

36 """ 

37 Removes all events in the main loop, 

38 a mouse click make the program halt, 

39 another click makes it start again. 

40 

41 @param pygame module pygame 

42 @return event ``pygame.QUIT``? 

43 """ 

44 for event in pygame.event.get(): 

45 if event.type == pygame.QUIT: 

46 return False 

47 if event.type == pygame.MOUSEBUTTONUP: 

48 if msg is not None: 

49 print(msg()) 

50 wait_event(pygame) 

51 return True 

52 

53 

54def get_pygame_screen_font(h, size, flags=0): 

55 """ 

56 Creates a surface with :epkg:`pygame`, initialize the module, 

57 creates font. 

58 

59 @param h size of the main font 

60 @param size screen size 

61 @param flags see `pygame.display.set_mode <https://www.pygame.org/docs/ref/display.html#pygame.display.set_mode>`_ 

62 @return pygame, screen, dictionary of fonts 

63 

64 The dictionary of fonts contains three fonts of size *h*, 

65 *3h/4*, *5h/6*. 

66 

67 This function leaves file still opened and generates warnings. 

68 Parameter *flag* can be useful if you run the function just 

69 to test that it is working and the result does not need to be seen. 

70 """ 

71 import pygame 

72 pygame.init() 

73 font = pygame.font.Font("freesansbold.ttf", h) 

74 font_small = pygame.font.Font("freesansbold.ttf", 3 * h // 4) 

75 try: 

76 screen = pygame.display.set_mode(size, flags) 

77 except pygame.error as e: 

78 raise Exception( 

79 f"Unable to create a screen, flags={flags}") from e 

80 font = pygame.font.Font("freesansbold.ttf", h) 

81 font_small = pygame.font.Font("freesansbold.ttf", 3 * h // 4) 

82 font_half = pygame.font.Font("freesansbold.ttf", 5 * h // 6) 

83 return pygame, screen, dict(font=font, font_half=font_half, font_small=font_small) 

84 

85 

86def build_diff_image(pygame, screen, h, maxw, seq1=None, seq2=None, diff=None, fonts=None, 

87 bars=None, colors=None, progress=None, prev_bars=None): 

88 """ 

89 Builds an image to show a difference between two lists, 

90 we assume these list contain distinct items. 

91 

92 @param pygame module pygame 

93 @param screen screen (pygame surface) 

94 @param h height of a line 

95 @param maxw width of the screen 

96 @param seq1 list 1 (can be None) 

97 @param seq2 list 2 (cannot be None) 

98 @param diff difference (object `SequenceMatcher <https://docs.python.org/3.5/library/difflib.html#sequencematcher-objects>`_) 

99 @param fonts dictionary of fonts with keys ``'font'``, ``'font_small'``, ``'font_half'`` 

100 @param bars each item of sequence 2 can be associated to a width (in [0, 1]) 

101 @param colors dictionary of colors (see below) 

102 @param progress draws the progress between two list 

103 @param prev_bars previous width 

104 

105 Colors: 

106 

107 * black: no change 

108 * blue: new 

109 * red: deleted 

110 * green: vert 

111 * yellow: bars 

112 

113 When *progress* is not None, the picture is a kind of average 

114 between the previous position and the new one. When a suggestion moves 

115 from *p1* to *p2*, it defines a circle. 

116 The result looks like this. 

117 

118 .. raw:: html 

119 

120 <video autoplay=" controls="" loop="" height="250"> 

121 <source src="http://www.xavierdupre.fr/enseignement/complements/diff.mp4" type="video/mp4" /> 

122 </video> 

123 

124 """ 

125 font = fonts.get('font', None) 

126 font_small = fonts.get('font_small', None) 

127 font_half = fonts.get('font_half', None) 

128 if font is None: 

129 raise ValueError("font cannot be None") 

130 if font_small is None: 

131 raise ValueError("font_small cannot be None") 

132 if font_half is None: 

133 raise ValueError("font_half cannot be None") 

134 if seq2 is None: 

135 raise ValueError("seq2 cannot be None") 

136 

137 if colors is None: 

138 colors = {} 

139 set_seq1 = {} if seq1 is None else set(seq1) 

140 set_seq2 = set(seq2) 

141 width = h // 3 

142 color_bar = colors.get('yellow', (240, 240, 0)) 

143 pos = 0 

144 if diff is not None: 

145 if progress is None: 

146 # just the diff 

147 opcodes = [] 

148 for opcode in diff.get_opcodes(): 

149 if opcode[0] in {'delete', 'equal', 'insert'}: 

150 opcodes.append(opcode) 

151 elif opcode[0] == "replace": 

152 opcodes.append( 

153 ('delete', opcode[1], opcode[2], None, None)) 

154 opcodes.append( 

155 ('insert', None, None, opcode[3], opcode[4])) 

156 else: 

157 raise ValueError(f"unexpected: {opcode}") 

158 

159 for opcode in opcodes: 

160 if opcode[0] == "delete": 

161 for i in range(opcode[1], opcode[2]): 

162 text = seq1[i] 

163 if text not in set_seq2: 

164 color = colors.get('red', (200, 0, 0)) 

165 text = font_small.render(text, True, color) 

166 screen.blit(text, (10, h * pos + h // 6)) 

167 pos += 1 

168 else: 

169 # we skip, it is going to be display by the other 

170 # part of the loop 

171 pass 

172 elif opcode[0] == "equal": 

173 color = colors.get('black', (0, 0, 0)) 

174 for i in range(opcode[3], opcode[4]): 

175 if bars is not None: 

176 y = h * pos + (h - width) // 2 + width 

177 pygame.draw.line( 

178 screen, color_bar, (0, y), (int(bars[i] * maxw), y), width) 

179 text = seq2[i] 

180 text = font.render(text, True, color) 

181 screen.blit(text, (10, h * pos)) 

182 pos += 1 

183 else: 

184 for i in range(opcode[3], opcode[4]): 

185 if bars is not None: 

186 y = h * pos + (h - width) // 2 + width 

187 pygame.draw.line( 

188 screen, color_bar, (0, y), (int(bars[i] * maxw), y), width) 

189 text = seq2[i] 

190 if text in set_seq1: 

191 color = colors.get('green', (0, 120, 0)) 

192 text = font.render(text, True, color) 

193 screen.blit(text, (10, h * pos)) 

194 pos += 1 

195 else: 

196 color = colors.get("blue", (0, 120, 120)) 

197 text = font.render(text, True, color) 

198 screen.blit(text, (10, h * pos)) 

199 pos += 1 

200 else: 

201 # animation 

202 positions = [] 

203 opcodes = [] 

204 for opcode in diff.get_opcodes(): 

205 if opcode[0] in {'delete', 'equal', 'insert'}: 

206 opcodes.append(opcode) 

207 elif opcode[0] == "replace": 

208 opcodes.append( 

209 ('delete', opcode[1], opcode[2], None, None)) 

210 opcodes.append( 

211 ('insert', None, None, opcode[3], opcode[4])) 

212 else: 

213 raise ValueError(f"unexpected: {opcode}") 

214 

215 for opcode in opcodes: 

216 if opcode[0] == "delete": 

217 for i in range(opcode[1], opcode[2]): 

218 row = (seq1[i], i, seq2.index(seq1[i]) if seq1[i] in seq2 else None, 

219 prev_bars[i] if prev_bars is not None else None) 

220 positions.append(row) 

221 elif opcode[0] == "equal": 

222 for i in range(opcode[3], opcode[4]): 

223 row = (seq2[i], seq1.index(seq2[i]), i, 

224 bars[i] if bars is not None else None) 

225 positions.append(row) 

226 else: 

227 for i in range(opcode[3], opcode[4]): 

228 row = (seq2[i], seq1.index(seq2[i]) if seq2[i] in seq1 else None, i, 

229 bars[i] if bars is not None else None) 

230 positions.append(row) 

231 for text, p1, p2, bar_ in positions: 

232 if p1 is None: 

233 # new 

234 x = maxw * (1 - progress) 

235 y = p2 * h 

236 color = colors.get('blue', (0, 120, 120)) 

237 elif p2 is None: 

238 # deleted 

239 x = maxw * progress 

240 y = p1 * h 

241 color = colors.get('green', (0, 120, 0)) 

242 else: 

243 # moved or equal 

244 if p1 == p2: 

245 x = 0.0 

246 y = p1 * h 

247 else: 

248 x = math.sin(progress * math.pi) * maxw / 2 * \ 

249 abs(p2 - p1) / len(seq2) 

250 y = (p1 + p2) * h / 2 - (p2 - p1) * \ 

251 h * math.cos(progress * math.pi) / 2 

252 color = colors.get('black', (0, 0, 0)) 

253 

254 x = int(x) 

255 y = int(y) 

256 if bar_ is not None: 

257 y2 = y + (h - width) // 2 + width 

258 pygame.draw.line(screen, color_bar, (x, y2), 

259 (x + int(bar_ * maxw), y2), width) 

260 text = font.render(text, True, color) 

261 screen.blit(text, (x, y)) 

262 

263 else: 

264 color = colors.get('black', (0, 0, 0)) 

265 for i in range(0, len(seq2)): 

266 text = seq2[i] 

267 text = font.render(text, True, color) 

268 screen.blit(text, (10, h * pos)) 

269 pos += 1