Gestion explicite de KeyboardInterrupt pour être sûr de quitter le jeu sur un ^C.
[minwii.git] / src / minwii / loganalyse.py
1 # -*- coding: utf-8 -*-
2 """
3 Module d'analyse des fichiers de log minwii.
4
5 $Id$
6 $URL$
7 """
8
9 from minwii.logfilereader import LogFileReader
10 from pprint import pprint
11 from minwii.musicxml import musicXml2Song
12 from minwii.globals import PLAYING_MODES
13 from statlib import stats
14 from datetime import timedelta
15 from xml.etree import ElementTree
16 import os.path
17
18 PLAYING_MODES = dict(PLAYING_MODES)
19
20 DEFAULT_STATS = (#'geometricmean',
21 ('harmonicmean', 'Moyenne harmonique'),
22 ('mean', 'Moyenne '),
23 ('median', 'Médiane'),
24 #'medianscore',
25 #'mode',
26 #'moment',
27 ('variation', 'Variation'),
28 #'skew',
29 ('kurtosis', 'Kurtosis'),
30 #'itemfreq',
31 #'histogram',
32 #'cumfreq',
33 #'relfreq',
34 )
35
36 def statsresults(m) :
37 def computeList(self):
38 l = m(self)
39 results = []
40 for name, label in DEFAULT_STATS :
41 results.append('%s : %s' % (label, getattr(stats, name)(l)))
42 return '\n'.join(results)
43 computeList.__name__ = m.__name__
44 computeList.__doc__ = m.__doc__
45 return computeList
46
47 def timebased(m) :
48 m.timebased = True
49 return m
50
51 class LogFileAnalyser(LogFileReader) :
52
53 POSSIBLE_ANALYSES = {'BEGINNER' : ('songDuration',
54 'playingDuration',
55 'noteEndNoteOnLatency',
56 'realisationRate')
57
58 ,'EASY' : ('songDuration',
59 'playingDuration',
60 'noteEndNoteOnLatency',
61 'realisationRate',
62 'missCount',
63 'getMissPerTimeFrame')
64
65 ,'NORMAL' : ('songDuration',
66 'playingDuration',
67 'realisationRate',
68 'missCount',
69 'getMissPerTimeFrame')
70
71 ,'ADVANCED' : ('songDuration',
72 'playingDuration',
73 'realisationRate',
74 'missCount',
75 'getMissPerTimeFrame')
76
77 ,'EXPERT' : ('songDuration',
78 'playingDuration',
79 'realisationRate',
80 'missCount',
81 'getMissPerTimeFrame')
82 }
83
84 def analyse(self) :
85 results = []
86
87 try :
88 self.mode = mode = self.getMode()
89 results.append(('Mode de jeu', PLAYING_MODES.get(mode, mode), False))
90
91 self.songTitle = LogFileAnalyser.getSongTitle(self.getSongFile())
92 results.append(('Chanson', self.songTitle, False))
93
94 for name in self.POSSIBLE_ANALYSES[mode] :
95 meth = getattr(self, name)
96 results.append( (meth.__doc__, meth(), getattr(meth, 'timebased', False)) )
97 except :
98 raise
99
100 return results
101
102 @staticmethod
103 def getSongTitle(file) :
104 if os.path.exists(file) :
105 it = ElementTree.iterparse(file, ['start', 'end'])
106 creditFound = False
107
108 for evt, el in it :
109 if el.tag == 'credit' :
110 creditFound = True
111 if el.tag == 'credit-words' and creditFound:
112 return el.text
113 if el.tag == 'part-list' :
114 # plus de chance de trouver un titre
115 return os.path.basename(file)
116 else :
117 return os.path.basename(file)
118
119 def _toTimeDelta(self, milliseconds) :
120 duration = milliseconds / 1000.
121 duration = int(round(duration, 0))
122 return str(timedelta(seconds=duration))
123
124 def playingDuration(self) :
125 'Temps de jeu'
126 #retourne la durée écoulée entre le premier et de dernier message
127 #de type événement : correspond à la durée d'interprétation.
128
129 last = self.getLastEventTicks()
130 first = self.getFirstEventTicks()
131 return self._toTimeDelta(last - first)
132
133
134 def songDuration(self) :
135 'Durée de référence de la chanson'
136 #retourne la durée de référence de la chanson
137 #en prenant en compte le tempo présent dans la transcription
138 #et en effectuant toutes les répétitions des couplets / refrains.
139
140 songFile = self.getSongFile()
141 song = musicXml2Song(songFile)
142 duration = song.duration
143 return self._toTimeDelta(duration)
144
145 @statsresults
146 def noteEndNoteOnLatency(self) :
147 'Réactivité'
148 eIter = self.getEventsIterator()
149 latencies = []
150 lastnoteEndT = 0
151
152 for ticks, eventName, message in eIter :
153 if eventName == 'NOTEEND':
154 lastnoteEndT = ticks
155 if eventName == 'NOTEON' and lastnoteEndT :
156 latencies.append(ticks - lastnoteEndT)
157
158 return latencies
159
160 def noteOnCount(self) :
161 "retourne le nombre d'événements NOTEON"
162
163 eIter = self.getEventsIterator()
164 cpt = 0
165
166 for ticks, eventName, message in eIter :
167 if eventName == 'NOTEON' :
168 cpt = cpt + 1
169
170 return cpt
171
172 def realisationRate(self) :
173 'Taux de réalisation'
174 #taux de réalisation en nombre de note
175 #peut être supérieur à 100 % car la chanson
176 #boucle à l'infini.
177
178 songFile = self.getSongFile()
179 song = musicXml2Song(songFile)
180 songNoteCpt = 0
181 for note, verseIndex in song.iterNotes() :
182 songNoteCpt = songNoteCpt + 1
183
184 return round(self.noteOnCount() / float(songNoteCpt) * 100, 1)
185
186 def missCount(self) :
187 "Nombre d'erreurs"
188 eIter = self.getEventsIterator()
189 miss = 0
190 if self.mode in ('EASY', 'NORMAL') :
191 catchColUp = False
192 for ticks, eventName, message in eIter :
193 if eventName == 'COLDOWN' :
194 colState = message.split(None, 2)[1]
195 colState = colState == 'True'
196 if colState :
197 catchColUp = False
198 continue
199 else :
200 catchColUp = True
201 elif eventName == 'NOTEON' :
202 catchColUp = False
203 elif eventName == 'COLUP' and catchColUp :
204 miss = miss + 1
205 else :
206 for ticks, eventName, message in eIter :
207 if eventName == 'COLDOWN' :
208 colState = message.split(None, 2)[1]
209 colState = colState == 'True'
210 if not colState :
211 miss = miss + 1
212
213 return miss
214
215 @timebased
216 def getMissPerTimeFrame(self, timeFrame=10000) :
217 "Nombre d'erreurs en fonction du temps"
218 eIter = self.getEventsIterator()
219 firstTicks = self.getFirstEventTicks()
220 frames = [0]
221
222 if self.mode in ('EASY', 'NORMAL') :
223 catchColUp = False
224 for ticks, eventName, message in eIter :
225 if ticks - firstTicks > timeFrame :
226 firstTicks = ticks
227 frames.append(0)
228
229 if eventName == 'COLDOWN' :
230 colState = message.split(None, 2)[1]
231 colState = colState == 'True'
232 if colState :
233 catchColUp = False
234 continue
235 else :
236 catchColUp = True
237 elif eventName == 'NOTEON' :
238 catchColUp = False
239 elif eventName == 'COLUP' and catchColUp :
240 frames[-1] = frames[-1] + 1
241 else :
242 for ticks, eventName, message in eIter :
243 if ticks - firstTicks > timeFrame :
244 firstTicks = ticks
245 frames.append(0)
246
247 if eventName == 'COLDOWN' :
248 colState = message.split(None, 2)[1]
249 colState = colState == 'True'
250 if not colState :
251 frames[-1] = frames[-1] + 1
252
253 return frames
254
255
256
257
258
259 def main() :
260 from optparse import OptionParser
261 usage = "%prog logfile"
262 op = OptionParser(usage)
263 options, args = op.parse_args()
264 if len(args) != 1 :
265 op.error("incorrect number of arguments")
266
267
268 lfa = LogFileAnalyser(args[0])
269 pprint(lfa.analyse())
270
271 if __name__ == "__main__" :
272 from os.path import realpath, sep
273 import sys
274 minwiipath = realpath(__file__).split(sep)
275 minwiipath = minwiipath[:-2]
276 minwiipath = sep.join(minwiipath)
277 sys.path.insert(1, minwiipath)
278 main()