Skip to content

Commit a52f864

Browse files
authored
Merge pull request #7 from emsec/dev
## Game - Fix error when switching tools in IntroDrawing ## Statistics3 - JSON export - Global Start Time and Time Limit ## Level Viewer - showClue: Overlay the actual gate type for covert/camouflaged gates - showSwitchID: Overlay the ID to correlate each switch to the log event - showWireLen: Overlay the length of each wire in grid units - addMargin: Shrink the level slightly as done for all levels in the main game - darkMode: Dark background instead of the print-friendly white background - hidePower: Don't show the internal logic state of wires and output elements (light bulb/danger sign)
2 parents 310f8b0 + c8c4940 commit a52f864

File tree

16 files changed

+228
-121
lines changed

16 files changed

+228
-121
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ conf/
2121
log_merging_config.json
2222
dockerComposeMPI.yml
2323
*.csv
24+
statistics_*.json
2425
reversim-conf
2526

2627
# Debugpy logs, WinMerge Backups etc.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ instance/statistics/**
1212

1313
# Don't track generated statistics
1414
*.csv
15+
statistics_*.json
1516
log_merging_config.json
1617
investigateLogs.py
1718

.vscode/launch.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,13 @@
9696
"type": "debugpy",
9797
"request": "launch",
9898
"module": "app.statistics3.statistics3",
99-
"args": [
100-
],
10199
"env": {
102100
"REVERSIM_INSTANCE": "${input:instancePath}"
103101
},
102+
"args": [
103+
"--beginning",
104+
"${input:date}"
105+
],
104106
"justMyCode": true,
105107
"console": "integratedTerminal",
106108
},
@@ -145,5 +147,11 @@
145147
"type": "promptString",
146148
"default": "instance"
147149
},
150+
{
151+
"id": "date",
152+
"description": "A date in ISO 8601 format at which the logs shall start",
153+
"type": "promptString",
154+
"default": ""
155+
},
148156
]
149157
}

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ ARG PROMETHEUS_MULTIPROC_DIR="/tmp/prometheus_multiproc"
1818
MAINTAINER Max Planck Institute for Security and Privacy
1919
LABEL org.opencontainers.image.authors="Max Planck Institute for Security and Privacy"
2020
# NOTE Also change the version in config.py
21-
LABEL org.opencontainers.image.version="2.1.3"
21+
LABEL org.opencontainers.image.version="2.1.4"
2222
LABEL org.opencontainers.image.licenses="AGPL-3.0-only"
2323
LABEL org.opencontainers.image.description="Ready to deploy Docker container to use ReverSim for research. ReverSim is an open-source environment for the browser, originally developed at the Max Planck Institute for Security and Privacy (MPI-SP) to study human aspects in hardware reverse engineering."
2424
LABEL org.opencontainers.image.source="https://github.com/emsec/ReverSim"

app/gameConfig.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
# CONFIG Current Log File Version.
1818
# NOTE Also change this in the Dockerfile
19-
LOGFILE_VERSION = "2.1.3" # Major.Milestone.Subversion
19+
LOGFILE_VERSION = "2.1.4" # Major.Milestone.Subversion
2020

2121
PSEUDONYM_LENGTH = 32
2222
LEVEL_ENCODING = 'UTF-8' # was Windows-1252

app/model/Participant.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,11 +730,13 @@ def selectDrawingTool(self, timeStamp: int, tool: Any):
730730

731731
self.logger.writeToLog(EventType.Click, e, timeStamp)
732732

733+
LEVEL_INTRO_DRAWING = (LevelType.LEVEL, 'elementIntroduction/simple_circuit.txt')
734+
733735
event = SelectDrawToolEvent(
734736
clientTime=timeStamp, serverTime=now(),
735737
pseudonym=self.pseudonym,
736738
phase=self.getPhaseName(),
737-
level=self.getLevelContext(),
739+
level=self.getLevelContext() if self.getPhase().hasLevels() else LEVEL_INTRO_DRAWING,
738740
object=tool
739741
)
740742
event.commit()

app/screenshotGenerator.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,16 @@ def screenshotLevel(page: Page, levelName: str):
249249
# print(f'Skipping: "{outputPath}"')
250250
# return
251251

252-
quotedLevelName = urllib.parse.quote_plus(currentLevel)
253-
page.goto(f'{base_url}/game?group=viewer&lang=en&ui={pseudonym}&level={quotedLevelName}')
252+
query_string = urllib.parse.urlencode({
253+
'group': 'viewer',
254+
'lang': 'en',
255+
'showSwitchID': '',
256+
'showClues': '',
257+
'ui': pseudonym,
258+
'level': currentLevel,
259+
})
260+
261+
page.goto(f'{base_url}/game?' + query_string)
254262
page.wait_for_timeout(1000)
255263

256264
downloadCanvasImage(page, outputName=outputPath)

app/statistics3/LogEventValidator.py

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from datetime import timedelta
12
import logging
23

34
from sqlalchemy.orm import Session
@@ -58,7 +59,8 @@ def handle_event(self, event: LogEvent, session: Session, statsParticipant: Stat
5859
assert isinstance(event, ChronoEvent)
5960
self.event_chrono(statsParticipant, player, event)
6061
case StartSessionEvent.__tablename__:
61-
pass
62+
assert isinstance(event, StartSessionEvent)
63+
self.event_start_session(statsParticipant, event)
6264
case SkillAssessmentEvent.__tablename__:
6365
pass
6466
case QualiEvent.__tablename__:
@@ -204,6 +206,33 @@ def event_chrono(self,
204206
raise LogValidationError(f'Unknown timer type "{event.timerType}"')
205207

206208

209+
def event_start_session(self,
210+
statsParticipant: StatsParticipant,
211+
event: StartSessionEvent
212+
):
213+
assert event.timeClient is not None
214+
assert event.phase is not None
215+
216+
# No need to set the page reload flag, if this is the first launch
217+
if not statsParticipant.game_started:
218+
statsParticipant.game_started = True
219+
statsParticipant.start_time = event.timeClient
220+
return
221+
222+
# Otherwise at this point this must be a page reload
223+
statsParticipant.activePhase.reloaded = True
224+
levelName = ''
225+
226+
if isinstance(statsParticipant.activePhase, StatsPhaseLevels):
227+
statsParticipant.activePhase.activeLevel.reloaded = True
228+
levelName = '@' + statsParticipant.activePhase.activeLevel.log_name
229+
230+
ui = getShortPseudo(statsParticipant.pseudonym)
231+
reload_location = statsParticipant.activePhase.phaseType + levelName
232+
logging.warning(f'Participant {ui} reloaded the page at "{reload_location}"')
233+
statsParticipant.reloads.append(reload_location)
234+
235+
207236
def event_quali(self,
208237
statsParticipant: StatsParticipant,
209238
event: QualiEvent
@@ -321,24 +350,14 @@ def start_phase(self, event: ChronoEvent, statsParticipant: StatsParticipant):
321350
assert event.timeClient is not None
322351
assert event.phase is not None
323352

324-
# Set the reload flag on page reload for phase and if applicable, also level
353+
# Preload events follow directly after event_start_session
325354
if event.phase.activePhase == PhaseType.Preload:
326-
# No need to set the page reload flag, if this is the first launch
327-
if not statsParticipant.game_started:
328-
statsParticipant.game_started = True
329-
return
330-
331-
statsParticipant.activePhase.reloaded = True
332-
levelName = ''
333-
334-
if isinstance(statsParticipant.activePhase, StatsPhaseLevels):
335-
statsParticipant.activePhase.activeLevel.reloaded = True
336-
levelName = '@' + statsParticipant.activePhase.activeLevel.log_name
337-
338-
ui = getShortPseudo(statsParticipant.pseudonym)
339-
reload_location = statsParticipant.activePhase.phaseType + levelName
340-
logging.warning(f'Participant {ui} reloaded the page at "{reload_location}"')
341-
statsParticipant.reloads.append(reload_location)
355+
if not statsParticipant.game_started or statsParticipant.start_time is None:
356+
raise LogValidationError('Preload Scene must follow directly after an event_start_phase')
357+
358+
# The Preload event contains the time limit
359+
if event.limit is not None:
360+
statsParticipant.time_limit = timedelta(seconds=event.limit)
342361

343362
# If this is not a preload phase, start the phase as usual
344363
else:
@@ -413,7 +432,7 @@ def click_continue(self,
413432
assert event.object == ClickableObjects.CONTINUE
414433

415434
if statsParticipant.activePhase.phaseType == PhaseType.AltTask:
416-
logging.info('End of Phase AltTask')
435+
logging.debug('End of Phase AltTask') # TODO
417436
return
418437

419438
# If it is a level continue
@@ -428,7 +447,7 @@ def click_continue(self,
428447

429448
# Else this must be the end of a Phase
430449
else:
431-
logging.info(f'End of Phase {statsParticipant.activePhase.phaseType}')
450+
logging.debug(f'End of Phase {statsParticipant.activePhase.phaseType}') # TODO
432451

433452

434453
def click_skip(self,

app/statistics3/StatsCircuit.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1+
from dataclasses import dataclass
12
from typing import override
23
from app.statistics3.StatsSlide import StatsSlide
34
from app.statistics3.statisticsUtils import TIME_TOLERANCE, TIMESTAMP_MS, CurrentLevelState, LogValidationError
4-
from app.utilsGame import LevelType
55

66

7+
@dataclass
78
class StatsCircuit(StatsSlide):
89

9-
def __init__(self, type_slide: LevelType, log_name: str, time_load: TIMESTAMP_MS) -> None:
10-
super().__init__(type_slide, log_name, time_load)
11-
12-
self.switchClicks: int = 0
13-
self.minSwitchClicks: int|None = None
14-
self.confirmClicks: int = 0
10+
switchClicks: int = 0
11+
minSwitchClicks: int|None = None
12+
confirmClicks: int = 0
1513

1614

1715
def click_switch(self):

app/statistics3/StatsParticipant.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
1+
from dataclasses import dataclass, field
12
from datetime import timedelta
23
from app.gameConfig import PHASES_WITH_LEVELS
34
from app.statistics3.StatsPhase import StatsPhase
45
from app.statistics3.StatsPhaseLevels import StatsPhaseLevels
56
from app.statistics3.statisticsUtils import TIMESTAMP_MS, LogValidationError
67
from app.utilsGame import PhaseType
78

8-
9+
@dataclass
910
class StatsParticipant:
1011

11-
@property
12-
def activePhase(self) -> StatsPhase:
13-
assert self.phaseIdx >= 0 and self.phaseIdx < len(self.phases)
14-
return self.phases[self.phaseIdx]
15-
12+
pseudonym: str
13+
is_debug: bool
1614

17-
def __init__(self, pseudonym: str, is_debug: bool) -> None:
18-
self.pseudonym = pseudonym
19-
self.is_debug = is_debug
15+
groups: list[str] = field(default_factory=list[str])
2016

21-
self.is_debug: bool
22-
self.groups: list[str] = []
17+
phases: list[StatsPhase] = field(default_factory=list[StatsPhase])
18+
phaseIdx: int = -1
2319

24-
self.phases: list[StatsPhase] = []
25-
self.phaseIdx: int = -1
20+
quali_fails: int = 0
2621

27-
self.quali_fails: int = 0
22+
game_started: bool = False
23+
reloads: list[str] = field(default_factory=list[str])
2824

29-
self.game_started = False
30-
self.reloads: list[str] = []
25+
start_time: TIMESTAMP_MS|None = None
26+
finish_time: TIMESTAMP_MS|None = None
27+
time_limit: timedelta|None = None
3128

32-
self.time_limit: timedelta|None = None
29+
@property
30+
def activePhase(self) -> StatsPhase:
31+
assert self.phaseIdx >= 0 and self.phaseIdx < len(self.phases)
32+
return self.phases[self.phaseIdx]
3333

3434

3535
def load_phase(self, type_phase: str, time_loaded: TIMESTAMP_MS):

0 commit comments

Comments
 (0)