Skip to content

Commit bfefe85

Browse files
committed
New Statistics Validator
1 parent 6793187 commit bfefe85

14 files changed

+519
-246
lines changed

.vscode/launch.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,19 @@
9191
"justMyCode": true,
9292
"console": "integratedTerminal",
9393
},
94+
{
95+
"name": "Statistics3",
96+
"type": "debugpy",
97+
"request": "launch",
98+
"module": "app.statistics3.statistics3",
99+
"args": [
100+
],
101+
"env": {
102+
"REVERSIM_INSTANCE": "${input:instancePath}"
103+
},
104+
"justMyCode": true,
105+
"console": "integratedTerminal",
106+
},
94107
],
95108
"inputs": [
96109
{

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
"gameplay",
131131
"gamerule",
132132
"gamerules",
133+
"gamestate",
133134
"getpid",
134135
"gnds",
135136
"graphviz",

app/gameConfig.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def loadGameConfig(self, configName: str = "conf/gameConfig.json", instanceFolde
146146

147147
# Get Git Hash from Config
148148
configStorage['gitHash'] = get_git_revision_hash(shortHash=True)
149-
logging.info("Game Version: " + LOGFILE_VERSION + "-" + self.getGitHash())
149+
logging.info("Game Version: " + LOGFILE_VERSION + "-" + configStorage['gitHash'])
150150

151151
# Validate and initialize all groups / add default gamerule
152152
for g in configStorage['groups']:
@@ -177,14 +177,14 @@ def loadGameConfig(self, configName: str = "conf/gameConfig.json", instanceFolde
177177
gamerules = configStorage['groups'][g]['config']
178178
# Validate pause timer
179179
if TIMER_NAME_PAUSE in configStorage['groups'][g]['config']:
180-
self.validatePauseTimer(g, gamerules)
180+
self.validatePauseTimer(configStorage, g, gamerules)
181181

182182
if TIMER_NAME_GLOBAL_LIMIT in configStorage['groups'][g]['config']:
183-
self.validateGlobalTimer(g, gamerules, TIMER_NAME_GLOBAL_LIMIT)
183+
self.validateGlobalTimer(configStorage, g, gamerules, TIMER_NAME_GLOBAL_LIMIT)
184184

185185
# Validate skill sub-groups gamerules are the same as origin gamerules
186186
if PhaseType.Skill in configStorage['groups'][g]:
187-
self.validateSkillGroup(g)
187+
self.validateSkillGroup(configStorage, g)
188188

189189
# Make sure the error report level is set
190190
if 'crashReportLevel' not in configStorage:
@@ -216,28 +216,31 @@ def loadGameConfig(self, configName: str = "conf/gameConfig.json", instanceFolde
216216
raise e
217217

218218

219-
def validatePauseTimer(self, group: str, gameruleName: str):
220-
P_CONF = self.__configStorage['groups'][group]['config'][TIMER_NAME_PAUSE]
219+
@staticmethod
220+
def validatePauseTimer(configStorage: dict[str, Any], group: str, gameruleName: str):
221+
P_CONF = configStorage['groups'][group]['config'][TIMER_NAME_PAUSE]
221222
assert 'duration' in P_CONF and P_CONF['duration'] >= 0, 'Invalid pause duration in "' + gameruleName + '"'
222-
return self.validateGlobalTimer(group, gameruleName, TIMER_NAME_PAUSE)
223+
return GameConfig.validateGlobalTimer(configStorage, group, gameruleName, TIMER_NAME_PAUSE)
223224

224225

225-
def validateGlobalTimer(self, group: str, gameruleName: str, timerName: str):
226-
P_CONF = self.__configStorage['groups'][group]['config'][timerName]
226+
@staticmethod
227+
def validateGlobalTimer(configStorage: dict[str, Any], group: str, gameruleName: str, timerName: str):
228+
P_CONF = configStorage['groups'][group]['config'][timerName]
227229
assert 'after' in P_CONF and P_CONF['after'] >= 0, 'Invalid pause timer start value in "' + gameruleName + '"'
228230

229231
assert P_CONF['startEvent'] in [*PHASES, None], 'Invalid start event specified "' + gameruleName + '"'
230232

231233

232-
def validateSkillGroup(self, group: str):
234+
@staticmethod
235+
def validateSkillGroup(configStorage: dict[str, Any], group: str):
233236
"""Make sure that gamerules of the SkillAssessment sub-groups matches the origin gamerules"""
234-
originGamerules: Dict[str, Any] = self.__configStorage['groups'][group]['config']
237+
originGamerules: Dict[str, Any] = configStorage['groups'][group]['config']
235238

236239
# Loop over all groups the player can be assigned to after the Skill assessment
237-
for subGroup in self.__configStorage['groups'][group][PhaseType.Skill]['groups'].keys():
240+
for subGroup in configStorage['groups'][group][PhaseType.Skill]['groups'].keys():
238241
# Make sure the sub-group gamerules key&values match the parents gamerules
239242
# Debug: [(str(k), originGamerules.get(k) == v) for k, v in subGamerules.items()]
240-
subGamerules: Dict[str, Any] = self.__configStorage['groups'][subGroup]['config']
243+
subGamerules: Dict[str, Any] = configStorage['groups'][subGroup]['config']
241244
if not all((originGamerules.get(k) == v for k, v in subGamerules.items())):
242245
logging.warning("The gamerules of the sub-groups specified for SkillAssessment should match the origin gamerules" \
243246
+ " (" + group + " -> " + subGroup + ")!"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class GameConfigValidator:
2+
"""
3+
Ensure that the player logfile/statistic is plausible when compared to the
4+
[gameConfig.json](instance/conf/gameConfig.json) file.
5+
"""
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class GameStateValidator:
2+
"""
3+
Ensure that the player logfile/statistic is plausible when compared to the last saved
4+
game state in the [reversim.db](instance/statistics/reversim.db) player database.
5+
"""
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
from sqlalchemy.orm import Session
2+
3+
from app.gameConfig import ALL_LEVEL_TYPES, PHASES_WITH_LEVELS
4+
from app.model.Level import Level
5+
from app.model.LogEvents import (
6+
AltTaskEvent,
7+
ChronoEvent,
8+
ClickEvent,
9+
ConfirmClickEvent,
10+
DrawEvent,
11+
GameOverEvent,
12+
GroupAssignmentEvent,
13+
IntroNavigationEvent,
14+
LanguageSelectionEvent,
15+
LogCreatedEvent,
16+
LogEvent,
17+
PopUpEvent,
18+
QualiEvent,
19+
ReconnectEvent,
20+
RedirectEvent,
21+
SelectDrawToolEvent,
22+
SimulateEvent,
23+
SkillAssessmentEvent,
24+
StartSessionEvent,
25+
SwitchClickEvent,
26+
)
27+
28+
from app.model.Participant import Participant
29+
from app.statistics3.statisticsUtils import LogValidationError
30+
from app.statistics3.StatsParticipant import StatsParticipant
31+
from app.statistics3.StatsPhaseLevels import StatsPhaseLevels
32+
33+
34+
class LogEventValidator():
35+
36+
def handle_event(self, event: LogEvent, session: Session, statsParticipant: StatsParticipant, player: Participant):
37+
match event.eventType:
38+
case LogCreatedEvent.__name__:
39+
assert isinstance(event, LogCreatedEvent)
40+
self.event_log_created(statsParticipant, event)
41+
case LanguageSelectionEvent.__name__:
42+
pass
43+
case GroupAssignmentEvent.__name__:
44+
assert isinstance(event, GroupAssignmentEvent)
45+
self.event_group_assignment(statsParticipant, event)
46+
case RedirectEvent.__name__:
47+
pass
48+
case ReconnectEvent.__name__:
49+
pass
50+
case GameOverEvent.__name__:
51+
pass
52+
case ChronoEvent.__name__:
53+
assert isinstance(event, ChronoEvent)
54+
self.event_chrono(session, statsParticipant, player, event)
55+
case StartSessionEvent.__name__:
56+
pass
57+
case SkillAssessmentEvent.__name__:
58+
pass
59+
case QualiEvent.__name__:
60+
pass
61+
case ClickEvent.__name__:
62+
pass
63+
case SwitchClickEvent.__name__:
64+
assert isinstance(event, SwitchClickEvent)
65+
self.event_switch_click(session, statsParticipant, event)
66+
case ConfirmClickEvent.__name__:
67+
assert isinstance(event, ConfirmClickEvent)
68+
self.event_confirm_click(session, statsParticipant, event)
69+
case SimulateEvent.__name__:
70+
pass
71+
case IntroNavigationEvent.__name__:
72+
pass
73+
case SelectDrawToolEvent.__name__:
74+
pass
75+
case DrawEvent.__name__:
76+
pass
77+
case PopUpEvent.__name__:
78+
pass
79+
case AltTaskEvent.__name__:
80+
pass
81+
case _:
82+
raise LogValidationError('Unexpected Log Type')
83+
84+
85+
def event_log_created(self,
86+
participant: StatsParticipant,
87+
event: LogCreatedEvent
88+
):
89+
participant.pseudonym = event.plain_pseudonym
90+
91+
if event.plain_pseudonym != event.pseudonym:
92+
raise LogValidationError(f'Pseudonym mismatch in LogCreatedEvent: "{event.plain_pseudonym} != {event.pseudonym}"')
93+
94+
95+
def event_group_assignment(self,
96+
statsParticipant: StatsParticipant,
97+
event: GroupAssignmentEvent
98+
):
99+
100+
if event.group in statsParticipant.groups:
101+
raise LogValidationError(f'Group {event.group} was assigned twice', event)
102+
103+
statsParticipant.groups.append(event.group)
104+
105+
106+
def event_chrono(self, session: Session, participant: StatsParticipant, player: Participant, event: ChronoEvent):
107+
if event.timeClient is None:
108+
raise LogValidationError('The chrono event did not contain the client time')
109+
110+
# Phase Operations
111+
if 'phase' == event.timerType:
112+
# Phase Load
113+
if 'load' == event.operation:
114+
self.load_phase(event, participant, player)
115+
116+
# Phase Start
117+
elif 'start' == event.operation:
118+
self.start_phase(event, participant)
119+
120+
else:
121+
raise LogValidationError(f'Unknown operation "{event.operation}"')
122+
123+
# Level Operations
124+
elif event.timerType in ALL_LEVEL_TYPES:
125+
# Check that the Level/Info Slide was created in a Phase which supports them
126+
if not isinstance(participant.activePhase, StatsPhaseLevels):
127+
raise LogValidationError(f'Slide type {event.timerType} should not exist in phase {participant.activePhase}', event)
128+
129+
# Load a Slide
130+
if 'load' == event.operation:
131+
self.load_slide(event, participant)
132+
133+
# Start a slide
134+
elif 'start' == event.operation:
135+
self.start_slide(event, participant)
136+
137+
else:
138+
raise LogValidationError(f'Unknown operation "{event.operation}"')
139+
140+
141+
def event_switch_click(self, session: Session, statsParticipant: StatsParticipant, event: SwitchClickEvent):
142+
statsParticipant.activePhase
143+
144+
145+
def event_confirm_click(self, session: Session, statsParticipant: StatsParticipant, event: ConfirmClickEvent):
146+
statsParticipant.activePhase
147+
148+
149+
def load_phase(self, event: ChronoEvent, statsParticipant: StatsParticipant, player: Participant):
150+
assert event.timeClient is not None
151+
152+
phaseType = event.timerName
153+
statsParticipant.load_phase(phaseType, event.timeClient)
154+
155+
# The phase from the game state
156+
assert statsParticipant.phaseIdx is not None
157+
gamestate_phase = player.phases[statsParticipant.phaseIdx]
158+
159+
# Check that the phase type matches what was shown during the game
160+
if gamestate_phase.name != phaseType:
161+
raise LogValidationError(f'{phaseType} does not match the gamestate {gamestate_phase.name}')
162+
163+
# Assert that a phase with levels really has levels
164+
assert phaseType in PHASES_WITH_LEVELS and len(gamestate_phase.levels) > 0, (
165+
f'Phase {phaseType} is expected to have no levels, but gameState has {len(gamestate_phase.levels)}'
166+
)
167+
168+
# Assert that a phase without levels really has no levels
169+
assert phaseType not in PHASES_WITH_LEVELS and len(gamestate_phase.levels) < 1, (
170+
f'Phase {phaseType} is expected to have levels, but gameState has 0'
171+
)
172+
173+
174+
def start_phase(self, event: ChronoEvent, statsParticipant: StatsParticipant):
175+
assert event.timeClient is not None
176+
177+
statsParticipant.activePhase.start(event.timeClient)
178+
179+
180+
def load_slide(self, event: ChronoEvent, statsParticipant: StatsParticipant):
181+
assert event.timeClient is not None
182+
183+
activePhase = statsParticipant.activePhase
184+
if not isinstance(activePhase, StatsPhaseLevels):
185+
raise LogValidationError('')
186+
187+
activePhase.load_level(
188+
type_level=event.timerType,
189+
log_name=Level.uniformName(event.timerName),
190+
time_load=event.timeClient
191+
)
192+
193+
194+
def start_slide(self, event: ChronoEvent, statsParticipant: StatsParticipant):
195+
assert event.timeClient is not None
196+

app/statistics3/StatsAltTask.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
from app.statistics3.StatsSlide import StatsSlide
3+
4+
5+
class StatsAltTask(StatsSlide):
6+
pass

app/statistics3/StatsCircuit.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from app.statistics3.StatsSlide import StatsSlide
2+
from app.statistics3.statisticsUtils import TIMESTAMP_MS, CurrentState, LogValidationError
3+
4+
5+
class StatsCircuit(StatsSlide):
6+
switchClicks: int = 0
7+
minSwitchClicks: int|None = None
8+
confirmClicks: int = 0
9+
10+
def click_switch(self):
11+
if self.status != CurrentState.STARTED:
12+
raise LogValidationError(f'Cannot click switch in {self.slide_type} with status {self.status}')
13+
14+
self.switchClicks += 1
15+
16+
17+
def skip(self, time_skip: TIMESTAMP_MS):
18+
if self.status != CurrentState.STARTED:
19+
raise LogValidationError(f'Cannot skip {self.slide_type} with status {self.status}')
20+
21+
self.time_finish = time_skip
22+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from app.config import PHASES_WITH_LEVELS
2+
from app.statistics3.StatsPhase import StatsPhase
3+
from app.statistics3.StatsPhaseLevels import StatsPhaseLevels
4+
from app.statistics3.statisticsUtils import TIMESTAMP_MS, LogValidationError
5+
from app.utilsGame import PhaseType
6+
7+
8+
class StatsParticipant:
9+
pseudonym: str
10+
is_debug: bool
11+
groups: list[str] = []
12+
13+
phases: list[StatsPhase] = []
14+
phaseIdx: int = -1
15+
16+
@property
17+
def activePhase(self) -> StatsPhase:
18+
assert self.phaseIdx >= 0 and self.phaseIdx < len(self.phases)
19+
return self.phases[self.phaseIdx]
20+
21+
22+
def __init__(self, pseudonym: str, is_debug: bool) -> None:
23+
self.pseudonym = pseudonym
24+
self.is_debug = is_debug
25+
26+
27+
def load_phase(self, type_phase: str, time_loaded: TIMESTAMP_MS):
28+
try:
29+
phaseType = PhaseType(type_phase)
30+
except Exception:
31+
raise LogValidationError('Unexpected phase type in database')
32+
33+
if phaseType in PHASES_WITH_LEVELS:
34+
phase = StatsPhaseLevels(phaseType, time_loaded)
35+
else:
36+
phase = StatsPhase(phaseType, time_loaded)
37+
38+
self.phases.append(phase)
39+
self.phaseIdx = len(self.phases) - 1
40+

0 commit comments

Comments
 (0)