From 01b34ece0b62158f76188dd672ed6a690d3c304e Mon Sep 17 00:00:00 2001 From: Jannled <7737131+Jannled@users.noreply.github.com> Date: Fri, 16 Jan 2026 01:40:38 +0100 Subject: [PATCH 1/3] Fix error when switching tools in IntroDrawing, Level Viewer more options, Statistics3 JSON Export --- .dockerignore | 1 + .gitignore | 1 + .vscode/launch.json | 12 ++- app/model/Participant.py | 4 +- app/screenshotGenerator.py | 12 ++- app/statistics3/StatsCircuit.py | 12 ++- app/statistics3/StatsParticipant.py | 32 ++++--- app/statistics3/StatsPhase.py | 21 +++-- app/statistics3/StatsPhaseLevels.py | 9 +- app/statistics3/StatsSlide.py | 23 +++-- app/statistics3/statistics3.py | 109 +++++++++++++++-------- app/statistics3/statisticsUtils.py | 24 ++++- static/src/LevelEditor/LevelViewScene.js | 14 +-- 13 files changed, 177 insertions(+), 97 deletions(-) diff --git a/.dockerignore b/.dockerignore index 0a040ca..848ce56 100644 --- a/.dockerignore +++ b/.dockerignore @@ -21,6 +21,7 @@ conf/ log_merging_config.json dockerComposeMPI.yml *.csv +statistics_*.json reversim-conf # Debugpy logs, WinMerge Backups etc. diff --git a/.gitignore b/.gitignore index 9a3cf08..6e969ab 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ instance/statistics/** # Don't track generated statistics *.csv +statistics_*.json log_merging_config.json investigateLogs.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 388bdbf..77e240a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -96,11 +96,13 @@ "type": "debugpy", "request": "launch", "module": "app.statistics3.statistics3", - "args": [ - ], "env": { "REVERSIM_INSTANCE": "${input:instancePath}" }, + "args": [ + "--beginning", + "${input:date}" + ], "justMyCode": true, "console": "integratedTerminal", }, @@ -145,5 +147,11 @@ "type": "promptString", "default": "instance" }, + { + "id": "date", + "description": "A date in ISO 8601 format at which the logs shall start", + "type": "promptString", + "default": "" + }, ] } \ No newline at end of file diff --git a/app/model/Participant.py b/app/model/Participant.py index 5810432..282155a 100644 --- a/app/model/Participant.py +++ b/app/model/Participant.py @@ -730,11 +730,13 @@ def selectDrawingTool(self, timeStamp: int, tool: Any): self.logger.writeToLog(EventType.Click, e, timeStamp) + LEVEL_INTRO_DRAWING = (LevelType.LEVEL, 'elementIntroduction/simple_circuit.txt') + event = SelectDrawToolEvent( clientTime=timeStamp, serverTime=now(), pseudonym=self.pseudonym, phase=self.getPhaseName(), - level=self.getLevelContext(), + level=self.getLevelContext() if self.getPhase().hasLevels() else LEVEL_INTRO_DRAWING, object=tool ) event.commit() diff --git a/app/screenshotGenerator.py b/app/screenshotGenerator.py index f9acb05..8a73fd1 100644 --- a/app/screenshotGenerator.py +++ b/app/screenshotGenerator.py @@ -249,8 +249,16 @@ def screenshotLevel(page: Page, levelName: str): # print(f'Skipping: "{outputPath}"') # return - quotedLevelName = urllib.parse.quote_plus(currentLevel) - page.goto(f'{base_url}/game?group=viewer&lang=en&ui={pseudonym}&level={quotedLevelName}') + query_string = urllib.parse.urlencode({ + 'group': 'viewer', + 'lang': 'en', + 'showSwitchID': '', + 'showClues': '', + 'ui': pseudonym, + 'level': currentLevel, + }) + + page.goto(f'{base_url}/game?' + query_string) page.wait_for_timeout(1000) downloadCanvasImage(page, outputName=outputPath) diff --git a/app/statistics3/StatsCircuit.py b/app/statistics3/StatsCircuit.py index f6984a6..ab98c12 100644 --- a/app/statistics3/StatsCircuit.py +++ b/app/statistics3/StatsCircuit.py @@ -1,17 +1,15 @@ +from dataclasses import dataclass from typing import override from app.statistics3.StatsSlide import StatsSlide from app.statistics3.statisticsUtils import TIME_TOLERANCE, TIMESTAMP_MS, CurrentLevelState, LogValidationError -from app.utilsGame import LevelType +@dataclass class StatsCircuit(StatsSlide): - def __init__(self, type_slide: LevelType, log_name: str, time_load: TIMESTAMP_MS) -> None: - super().__init__(type_slide, log_name, time_load) - - self.switchClicks: int = 0 - self.minSwitchClicks: int|None = None - self.confirmClicks: int = 0 + switchClicks: int = 0 + minSwitchClicks: int|None = None + confirmClicks: int = 0 def click_switch(self): diff --git a/app/statistics3/StatsParticipant.py b/app/statistics3/StatsParticipant.py index 99cc092..1323885 100644 --- a/app/statistics3/StatsParticipant.py +++ b/app/statistics3/StatsParticipant.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field from datetime import timedelta from app.gameConfig import PHASES_WITH_LEVELS from app.statistics3.StatsPhase import StatsPhase @@ -5,31 +6,28 @@ from app.statistics3.statisticsUtils import TIMESTAMP_MS, LogValidationError from app.utilsGame import PhaseType - +@dataclass class StatsParticipant: - @property - def activePhase(self) -> StatsPhase: - assert self.phaseIdx >= 0 and self.phaseIdx < len(self.phases) - return self.phases[self.phaseIdx] - + pseudonym: str + is_debug: bool - def __init__(self, pseudonym: str, is_debug: bool) -> None: - self.pseudonym = pseudonym - self.is_debug = is_debug + groups: list[str] = field(default_factory=list[str]) - self.is_debug: bool - self.groups: list[str] = [] + phases: list[StatsPhase] = field(default_factory=list[StatsPhase]) + phaseIdx: int = -1 - self.phases: list[StatsPhase] = [] - self.phaseIdx: int = -1 + quali_fails: int = 0 - self.quali_fails: int = 0 + game_started: bool = False + reloads: list[str] = field(default_factory=list[str]) - self.game_started = False - self.reloads: list[str] = [] + time_limit: timedelta|None = None - self.time_limit: timedelta|None = None + @property + def activePhase(self) -> StatsPhase: + assert self.phaseIdx >= 0 and self.phaseIdx < len(self.phases) + return self.phases[self.phaseIdx] def load_phase(self, type_phase: str, time_loaded: TIMESTAMP_MS): diff --git a/app/statistics3/StatsPhase.py b/app/statistics3/StatsPhase.py index 18a3d00..86d0bcc 100644 --- a/app/statistics3/StatsPhase.py +++ b/app/statistics3/StatsPhase.py @@ -1,4 +1,5 @@ +from dataclasses import dataclass import logging from datetime import timedelta @@ -10,21 +11,19 @@ ) from app.utilsGame import PhaseType - +@dataclass class StatsPhase: + phaseType: PhaseType + time_load: TIMESTAMP_MS - def __init__(self, type_phase: PhaseType, time_load: TIMESTAMP_MS) -> None: - self.phaseType = type_phase - - self.time_load: TIMESTAMP_MS = time_load - self.time_start: TIMESTAMP_MS|None = None - self.time_start_levels: TIMESTAMP_MS|None = None - self.time_finish: TIMESTAMP_MS|None = None + time_start: TIMESTAMP_MS|None = None + time_start_levels: TIMESTAMP_MS|None = None + time_finish: TIMESTAMP_MS|None = None - self.time_limit: timedelta|None = None + time_limit: timedelta|None = None - self.status: CurrentState = CurrentState.LOADED - self.reloaded = False + status: CurrentState = CurrentState.LOADED + reloaded = False def start(self, time_start: TIMESTAMP_MS, time_limit: float|None): diff --git a/app/statistics3/StatsPhaseLevels.py b/app/statistics3/StatsPhaseLevels.py index 17a7b79..778f6c1 100644 --- a/app/statistics3/StatsPhaseLevels.py +++ b/app/statistics3/StatsPhaseLevels.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field from typing import override from app.gameConfig import LEVEL_FILETYPES_WITH_TASK, PHASES_WITH_LEVELS @@ -8,8 +9,12 @@ from app.utilsGame import LevelType, PhaseType +@dataclass class StatsPhaseLevels(StatsPhase): + levels: list[StatsSlide] = field(default_factory=list[StatsSlide]) + levelIdx: int = -1 + @property def activeLevel(self) -> StatsSlide: assert self.levelIdx >= 0 and self.levelIdx < len(self.levels) @@ -20,8 +25,8 @@ def __init__(self, type_phase: PhaseType, time_load: TIMESTAMP_MS) -> None: super().__init__(type_phase, time_load) assert type_phase in PHASES_WITH_LEVELS - self.levels: list[StatsSlide] = [] - self.levelIdx: int = -1 + self.levels = [] + self.levelIdx = -1 def load_level(self, type_level: str, log_name: str, time_load: TIMESTAMP_MS): diff --git a/app/statistics3/StatsSlide.py b/app/statistics3/StatsSlide.py index 304fc0b..abb88c2 100644 --- a/app/statistics3/StatsSlide.py +++ b/app/statistics3/StatsSlide.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass import logging from datetime import timedelta @@ -10,24 +11,20 @@ from app.utilsGame import LevelType +@dataclass class StatsSlide: - def __init__(self, - type_slide: LevelType, - log_name: str, - time_load: TIMESTAMP_MS - ) -> None: - self.slide_type = type_slide - self.log_name = log_name + slide_type: LevelType + log_name: str - self.time_load: TIMESTAMP_MS = time_load - self.time_start: TIMESTAMP_MS|None = None - self.time_finish: TIMESTAMP_MS|None = None + time_load: TIMESTAMP_MS + time_start: TIMESTAMP_MS|None = None + time_finish: TIMESTAMP_MS|None = None - self.time_limit: timedelta|None = None + time_limit: timedelta|None = None - self.status: CurrentLevelState = CurrentLevelState.LOADED - self.reloaded = False + status: CurrentLevelState = CurrentLevelState.LOADED + reloaded = False def start(self, time_start: TIMESTAMP_MS, time_limit: float|None): diff --git a/app/statistics3/statistics3.py b/app/statistics3/statistics3.py index b1ffec2..0e23413 100644 --- a/app/statistics3/statistics3.py +++ b/app/statistics3/statistics3.py @@ -1,9 +1,12 @@ import argparse +from datetime import datetime +import json import logging import os +from pathlib import Path from typing import Iterable -from sqlalchemy import Engine, create_engine, func, select +from sqlalchemy import Engine, create_engine, select from sqlalchemy.orm import Session import app.config as gameConfigLegacy @@ -14,9 +17,13 @@ from app.model.Participant import Participant from app.statistics3.GameStateValidator import GameStateValidator from app.statistics3.LogEventValidator import LogEventValidator -from app.statistics3.statisticsUtils import LogValidationError +from app.statistics3.statisticsUtils import LogValidationError, StatisticJSONEncoder from app.statistics3.StatsParticipant import StatsParticipant -from app.utilsGame import getShortPseudo +from app.utilsGame import get_git_revision_hash, getShortPseudo + + + + # Flask uses an instance folder to store and load assets INSTANCE_FOLDER = os.path.abspath(os.environ.get('REVERSIM_INSTANCE', './instance')) @@ -40,31 +47,41 @@ def __init__(self, self.engine = database - def read_group(self, group: str, skip_debug: bool = True) -> Iterable[StatsParticipant]: + def read_group(self, group: str, skip_debug: bool = True, start_time: datetime|None = None) -> Iterable[StatsParticipant]: logging.info(f'Querying all participants for group {group}') with Session(self.engine) as session: + stmt = select(GroupAssignmentEvent.pseudonym).where( + GroupAssignmentEvent.group == group + ) + if skip_debug: - stmt = select(GroupAssignmentEvent.pseudonym).where( - GroupAssignmentEvent.group == group - ) - else: - stmt = select(GroupAssignmentEvent.pseudonym).where( - GroupAssignmentEvent.group == group and - GroupAssignmentEvent.isDebug == False # noqa: E712 - ) + stmt = stmt.where(GroupAssignmentEvent.isDebug == False) # noqa: E712 + + if start_time is not None: + stmt = stmt.where(GroupAssignmentEvent.timeServer >= start_time) + expected_pseudonyms: list[str] = list(session.scalars(stmt)) valid_pseudonyms: list[str] = [] for pseudonym in expected_pseudonyms: try: - participant = self.read_participant(session, pseudonym) + player = session.get_one(Participant, pseudonym) + + # Drop all players that have not started the game + if not player.startedGame: + expected_pseudonyms.remove(player.pseudonym) + continue + + participant = self.read_participant(session, player) valid_pseudonyms.append(pseudonym) yield participant + except LogValidationError as e: lineInfo = (f'#{e.event.id}' if e.event is not None else '') logging.error(f'{getShortPseudo(pseudonym)}{lineInfo} is invalid: "{e}"') + except AssertionError as e: logging.error(f'Something went wrong while parsing {pseudonym}: "{e}"') @@ -72,9 +89,11 @@ def read_group(self, group: str, skip_debug: bool = True) -> Iterable[StatsParti logging.info(f'{len(valid_pseudonyms)} of {len(expected_pseudonyms)} player logs passed validation') - def read_participant(self, session: Session, pseudonym: str) -> StatsParticipant: - statsParticipant = StatsParticipant(pseudonym, self.is_debug(session, pseudonym)) - player = session.get_one(Participant, pseudonym) + def read_participant(self, session: Session, player: Participant) -> StatsParticipant: + statsParticipant = StatsParticipant( + pseudonym=player.pseudonym, + is_debug=player.isDebug + ) events = session.execute( statement=select(LogEvent).where(LogEvent.pseudonym == statsParticipant.pseudonym) @@ -83,7 +102,7 @@ def read_participant(self, session: Session, pseudonym: str) -> StatsParticipant log_validator = LogEventValidator() state_validator = GameStateValidator() - logging.info(f'Validating {getShortPseudo(pseudonym)}') + logging.info(f'Validating {getShortPseudo(player.pseudonym)}') for event in events: try: log_validator.handle_event(event, session, statsParticipant, player) @@ -99,26 +118,15 @@ def read_participant(self, session: Session, pseudonym: str) -> StatsParticipant return statsParticipant - @staticmethod - def is_debug(session: Session, pseudonym: str): - result = session.execute( - select(func.count()).where( - GroupAssignmentEvent.pseudonym == pseudonym and - GroupAssignmentEvent.isDebug - ) - ).scalar_one() - - return result > 0 - - def main(): parser = argparse.ArgumentParser(description="A script to aggregate the logfiles from the ReverSim game into a csv file.") parser.add_argument('-i', '--instance-path', help='', default=INSTANCE_FOLDER) parser.add_argument("-o", "--output", help="The filename of the output statistic csv file", default='statistics.csv') - parser.add_argument("-s", "--skipScreenshots", help="Skip the screenshot validation", action="store_true") + #parser.add_argument("-s", "--skipScreenshots", help="Skip the screenshot validation", action="store_true") parser.add_argument("-d", "--allowDebug", help="Allow debug groups to end up in the output", action="store_true") parser.add_argument("-l", "--log", metavar='LEVEL', help="Specify the log level, must be one of DEBUG, INFO, WARNING, ERROR or CRITICAL", default="INFO") + parser.add_argument('-b', '--beginning', help='Only include logs that start after this date in ISO 8601 format, e.g. 2026-01-15', default=None) args = parser.parse_args() @@ -138,22 +146,51 @@ def main(): # Load the GameConfig gameConfig = GameConfig( configName=CONFIG_NAME, - instanceFolder=INSTANCE_FOLDER + instanceFolder=args.instance_path ) gameConfigLegacy.setGameConfig(gameConfig) # Load the Level Loader - JsonLevelList.singleton = JsonLevelList.fromFile(instanceFolder=INSTANCE_FOLDER) + JsonLevelList.singleton = JsonLevelList.fromFile(instanceFolder=args.instance_path) # Open the Database - database_path = os.path.join(INSTANCE_FOLDER, DATABASE_PATH) + database_path = os.path.join(args.instance_path, DATABASE_PATH) engine = (create_engine("sqlite:///" + database_path, echo=False) .execution_options(sqlite_readonly = True)) - - statsGenerator = StatisticsGenerator(INSTANCE_FOLDER, gameConfig, JsonLevelList, engine) - data = list(statsGenerator.read_group('cognitive_obfuscation')) + try: + if args.beginning is not None and len(args.beginning) > 0: + start_time = datetime.fromisoformat(args.beginning) + else: + start_time = None + except Exception as e: + logging.error(e) + return + + statsGenerator = StatisticsGenerator(args.instance_path, gameConfig, JsonLevelList, engine) + data = list(statsGenerator.read_group( + group='cognitive_obfuscation', + skip_debug=not args.allowDebug, + start_time=start_time + )) + + gitHash = None + try: + gitHash = get_git_revision_hash(shortHash=True) + except Exception as e: + logging.warning('Could not determine git hash: ' + str(e)) + + now = datetime.now() + data_json = json.dumps({ + 'time': now, + 'gitHash': gitHash, + 'participants': data, + 'args': vars(args), + 'instance': INSTANCE_FOLDER, + }, cls=StatisticJSONEncoder, indent=4) + + Path(f'statistics_{now.strftime('%Y-%m-%d_%H%M')}.json').write_text(data_json, encoding='UTF-8') if __name__ == '__main__': main() diff --git a/app/statistics3/statisticsUtils.py b/app/statistics3/statisticsUtils.py index 0fda941..e9cf807 100644 --- a/app/statistics3/statisticsUtils.py +++ b/app/statistics3/statisticsUtils.py @@ -1,5 +1,8 @@ -from datetime import datetime +import dataclasses +from datetime import datetime, timedelta from enum import StrEnum +import json +from typing import Any from app.model.LogEvents import LogEvent @@ -36,3 +39,22 @@ class CurrentLevelState(StrEnum): FINISHED = 'Finished' SKIPPED = 'Skipped' TIMEOUT = 'Timeout' + + +class StatisticJSONEncoder(json.JSONEncoder): + def default(self, o: Any): + # Serialize StatsParticipant, StatsPhase etc. + if dataclasses.is_dataclass(o): + # type[dataclass] could theoretically slip through + return dataclasses.asdict(o) # type: ignore + + # Serialize datetime objects in ISO 8601 + if isinstance(o, datetime): + return o.isoformat() + + # Serialize timedelta as a float in seconds + if isinstance(o, timedelta): + return o.total_seconds() + + # Try the default handler + return super().default(o) diff --git a/static/src/LevelEditor/LevelViewScene.js b/static/src/LevelEditor/LevelViewScene.js index 7ac5acb..bad74a9 100644 --- a/static/src/LevelEditor/LevelViewScene.js +++ b/static/src/LevelEditor/LevelViewScene.js @@ -10,12 +10,13 @@ class LevelViewScene extends BaseScene const url = new URL(window.location.href); this.showTextAnchors = true; // Show text anchor positions - this.showClues = true; // Highlight covert gates and switches with random initial state - this.showIDs = true; // Show the IDs of switches + this.showClues = url.searchParams.has('showClue'); // Highlight covert gates and switches with random initial state + this.showIDs = url.searchParams.has('showSwitchID'); // Show the IDs of switches this.showWireLen = url.searchParams.has('showWireLen'); // Show the manhattan distance of wires this.splittersAlwaysVisible = true; this.enableLevelStats = false; this.levelPath = null; + this.marginFac = url.searchParams.has('addMargin') ? 1 : 0; // Add the margin that is around every level in the game this.state = { wireStateVisible: true, @@ -46,9 +47,6 @@ class LevelViewScene extends BaseScene this.customSettings(); this.loadLevelFromQueryString(); - // Make the screen print friendly - this.setBrightMode(); - this.registerClickListener('Switch', this.onSwitchClicked); } @@ -57,6 +55,12 @@ class LevelViewScene extends BaseScene */ customSettings() { + const url = new URL(window.location.href); + + // Make the screen print friendly + if(!url.searchParams.has('darkMode')) + this.setBrightMode(); + this.showTextAnchors = false; this.splittersAlwaysVisible = false; this.enableLevelStats = true; From 9e34862848ef85717f31591c4658ec8029c14377 Mon Sep 17 00:00:00 2001 From: Jannled <7737131+Jannled@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:11:54 +0100 Subject: [PATCH 2/3] Add global time metric to stats3, hide power in level viewer --- app/statistics3/LogEventValidator.py | 55 ++++++++++++++++-------- app/statistics3/StatsParticipant.py | 2 + app/statistics3/statistics3.py | 4 +- static/src/LevelEditor/LevelViewScene.js | 14 ++++-- 4 files changed, 51 insertions(+), 24 deletions(-) diff --git a/app/statistics3/LogEventValidator.py b/app/statistics3/LogEventValidator.py index 879b3d7..16bac39 100644 --- a/app/statistics3/LogEventValidator.py +++ b/app/statistics3/LogEventValidator.py @@ -1,3 +1,4 @@ +from datetime import timedelta import logging from sqlalchemy.orm import Session @@ -58,7 +59,8 @@ def handle_event(self, event: LogEvent, session: Session, statsParticipant: Stat assert isinstance(event, ChronoEvent) self.event_chrono(statsParticipant, player, event) case StartSessionEvent.__tablename__: - pass + assert isinstance(event, StartSessionEvent) + self.event_start_session(statsParticipant, event) case SkillAssessmentEvent.__tablename__: pass case QualiEvent.__tablename__: @@ -204,6 +206,33 @@ def event_chrono(self, raise LogValidationError(f'Unknown timer type "{event.timerType}"') + def event_start_session(self, + statsParticipant: StatsParticipant, + event: StartSessionEvent + ): + assert event.timeClient is not None + assert event.phase is not None + + # No need to set the page reload flag, if this is the first launch + if not statsParticipant.game_started: + statsParticipant.game_started = True + statsParticipant.start_time = event.timeClient + return + + # Otherwise at this point this must be a page reload + statsParticipant.activePhase.reloaded = True + levelName = '' + + if isinstance(statsParticipant.activePhase, StatsPhaseLevels): + statsParticipant.activePhase.activeLevel.reloaded = True + levelName = '@' + statsParticipant.activePhase.activeLevel.log_name + + ui = getShortPseudo(statsParticipant.pseudonym) + reload_location = statsParticipant.activePhase.phaseType + levelName + logging.warning(f'Participant {ui} reloaded the page at "{reload_location}"') + statsParticipant.reloads.append(reload_location) + + def event_quali(self, statsParticipant: StatsParticipant, event: QualiEvent @@ -321,24 +350,14 @@ def start_phase(self, event: ChronoEvent, statsParticipant: StatsParticipant): assert event.timeClient is not None assert event.phase is not None - # Set the reload flag on page reload for phase and if applicable, also level + # Preload events follow directly after event_start_session if event.phase.activePhase == PhaseType.Preload: - # No need to set the page reload flag, if this is the first launch - if not statsParticipant.game_started: - statsParticipant.game_started = True - return - - statsParticipant.activePhase.reloaded = True - levelName = '' - - if isinstance(statsParticipant.activePhase, StatsPhaseLevels): - statsParticipant.activePhase.activeLevel.reloaded = True - levelName = '@' + statsParticipant.activePhase.activeLevel.log_name - - ui = getShortPseudo(statsParticipant.pseudonym) - reload_location = statsParticipant.activePhase.phaseType + levelName - logging.warning(f'Participant {ui} reloaded the page at "{reload_location}"') - statsParticipant.reloads.append(reload_location) + if not statsParticipant.game_started or statsParticipant.start_time is None: + raise LogValidationError('Preload Scene must follow directly after an event_start_phase') + + # The Preload event contains the time limit + if event.limit is not None: + statsParticipant.time_limit = timedelta(seconds=event.limit) # If this is not a preload phase, start the phase as usual else: diff --git a/app/statistics3/StatsParticipant.py b/app/statistics3/StatsParticipant.py index 1323885..dfdf73d 100644 --- a/app/statistics3/StatsParticipant.py +++ b/app/statistics3/StatsParticipant.py @@ -22,6 +22,8 @@ class StatsParticipant: game_started: bool = False reloads: list[str] = field(default_factory=list[str]) + start_time: TIMESTAMP_MS|None = None + finish_time: TIMESTAMP_MS|None = None time_limit: timedelta|None = None @property diff --git a/app/statistics3/statistics3.py b/app/statistics3/statistics3.py index 0e23413..55a9e9c 100644 --- a/app/statistics3/statistics3.py +++ b/app/statistics3/statistics3.py @@ -160,8 +160,8 @@ def main(): .execution_options(sqlite_readonly = True)) try: - if args.beginning is not None and len(args.beginning) > 0: - start_time = datetime.fromisoformat(args.beginning) + if args.beginning is not None and len(args.beginning.strip()) > 0: + start_time = datetime.fromisoformat(args.beginning.strip()) else: start_time = None except Exception as e: diff --git a/static/src/LevelEditor/LevelViewScene.js b/static/src/LevelEditor/LevelViewScene.js index bad74a9..0847cd9 100644 --- a/static/src/LevelEditor/LevelViewScene.js +++ b/static/src/LevelEditor/LevelViewScene.js @@ -55,11 +55,18 @@ class LevelViewScene extends BaseScene */ customSettings() { - const url = new URL(window.location.href); + const searchParams = new URL(window.location.href).searchParams; // Make the screen print friendly - if(!url.searchParams.has('darkMode')) + if(!searchParams.has('darkMode')) this.setBrightMode(); + + // Visualize the simulation state + if(searchParams.has('hidePower')) + { + this.state.outputStateVisible = false; + this.state.wireStateVisible = false; + } this.showTextAnchors = false; this.splittersAlwaysVisible = false; @@ -155,8 +162,7 @@ class LevelViewScene extends BaseScene else this.circuit = new Circuit(this, this.levelFile.fileContent, this.splittersAlwaysVisible); - this.circuit.calculateOutputs(); - this.circuit.wireDrawer.drawWires(); + this.updateLevelState(false); // Show the wire length if enabled if(this.showWireLen) From c8c49401b07146e80a80a6e60f6573d1507c88cb Mon Sep 17 00:00:00 2001 From: Jannled <7737131+Jannled@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:15:59 +0100 Subject: [PATCH 3/3] Bump version to 2.1.4 --- Dockerfile | 2 +- app/gameConfig.py | 2 +- app/statistics3/LogEventValidator.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4818745..20fc466 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ ARG PROMETHEUS_MULTIPROC_DIR="/tmp/prometheus_multiproc" MAINTAINER Max Planck Institute for Security and Privacy LABEL org.opencontainers.image.authors="Max Planck Institute for Security and Privacy" # NOTE Also change the version in config.py -LABEL org.opencontainers.image.version="2.1.3" +LABEL org.opencontainers.image.version="2.1.4" LABEL org.opencontainers.image.licenses="AGPL-3.0-only" 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." LABEL org.opencontainers.image.source="https://github.com/emsec/ReverSim" diff --git a/app/gameConfig.py b/app/gameConfig.py index 75f4644..3fbdca1 100644 --- a/app/gameConfig.py +++ b/app/gameConfig.py @@ -16,7 +16,7 @@ # CONFIG Current Log File Version. # NOTE Also change this in the Dockerfile -LOGFILE_VERSION = "2.1.3" # Major.Milestone.Subversion +LOGFILE_VERSION = "2.1.4" # Major.Milestone.Subversion PSEUDONYM_LENGTH = 32 LEVEL_ENCODING = 'UTF-8' # was Windows-1252 diff --git a/app/statistics3/LogEventValidator.py b/app/statistics3/LogEventValidator.py index 16bac39..8ebc168 100644 --- a/app/statistics3/LogEventValidator.py +++ b/app/statistics3/LogEventValidator.py @@ -432,7 +432,7 @@ def click_continue(self, assert event.object == ClickableObjects.CONTINUE if statsParticipant.activePhase.phaseType == PhaseType.AltTask: - logging.info('End of Phase AltTask') + logging.debug('End of Phase AltTask') # TODO return # If it is a level continue @@ -447,7 +447,7 @@ def click_continue(self, # Else this must be the end of a Phase else: - logging.info(f'End of Phase {statsParticipant.activePhase.phaseType}') + logging.debug(f'End of Phase {statsParticipant.activePhase.phaseType}') # TODO def click_skip(self,