gini-toolkit 6.0.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
gini/ui/diagnose_game.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Generic Diagnose-game UI — one widget that drives ANY game via a GameSpec + a case source + a
|
|
2
|
+
signature renderer. The confusion matrix, buttons, modes (practice/graded), and scoring are shared;
|
|
3
|
+
each game only supplies its classes, its cases (real telemetry), and how to draw a signature.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Qt, QTimer
|
|
8
|
+
from PySide6.QtGui import QColor, QPainter
|
|
9
|
+
from PySide6.QtWidgets import (
|
|
10
|
+
QHBoxLayout, QLabel, QLineEdit, QListWidget, QPushButton, QVBoxLayout, QWidget,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from ..domain.diagnose import GRADED, PRACTICE, DiagnoseSession
|
|
14
|
+
from .theme.manager import scale_css as _scss
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfusionGrid(QWidget):
|
|
18
|
+
"""true(row) × predicted(col) matrix; diagonal = correct. Axis ticks use the spec's abbrevs."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, theme, parent=None) -> None:
|
|
21
|
+
super().__init__(parent)
|
|
22
|
+
self.theme = theme
|
|
23
|
+
self._m: dict = {}
|
|
24
|
+
self._classes: list = []
|
|
25
|
+
self._abbr: dict = {}
|
|
26
|
+
self.setMinimumSize(300, 240)
|
|
27
|
+
|
|
28
|
+
def set_matrix(self, m, classes, abbrev=None) -> None:
|
|
29
|
+
self._m = m or {}
|
|
30
|
+
self._classes = classes or []
|
|
31
|
+
self._abbr = abbrev or {}
|
|
32
|
+
self.update()
|
|
33
|
+
|
|
34
|
+
def _tick(self, cls) -> str:
|
|
35
|
+
return self._abbr.get(cls, cls)
|
|
36
|
+
|
|
37
|
+
def paintEvent(self, _e) -> None: # noqa: N802
|
|
38
|
+
t = self.theme.theme
|
|
39
|
+
p = QPainter(self)
|
|
40
|
+
p.setRenderHint(QPainter.Antialiasing)
|
|
41
|
+
cls = self._classes
|
|
42
|
+
n = len(cls)
|
|
43
|
+
if n == 0:
|
|
44
|
+
return
|
|
45
|
+
lab = 46
|
|
46
|
+
cell = min((self.width() - lab) / n, (self.height() - lab) / n)
|
|
47
|
+
mx = max([v for v in self._m.values()] + [1])
|
|
48
|
+
for r, tru in enumerate(cls):
|
|
49
|
+
p.setPen(QColor(t.muted))
|
|
50
|
+
p.drawText(0, int(lab + r * cell), lab - 4, int(cell),
|
|
51
|
+
Qt.AlignRight | Qt.AlignVCenter, self._tick(tru))
|
|
52
|
+
for c, pred in enumerate(cls):
|
|
53
|
+
v = self._m.get((tru, pred), 0)
|
|
54
|
+
x, y = lab + c * cell, lab + r * cell
|
|
55
|
+
base = t.accent_for("green" if r == c else "red")
|
|
56
|
+
col = QColor(base); col.setAlpha(int(30 + 200 * (v / mx)) if v else 12)
|
|
57
|
+
p.setBrush(col)
|
|
58
|
+
p.setPen(QColor(t.accent if r == c else t.line))
|
|
59
|
+
p.drawRect(int(x), int(y), int(cell) - 2, int(cell) - 2)
|
|
60
|
+
if v:
|
|
61
|
+
p.setPen(QColor(t.text))
|
|
62
|
+
p.drawText(int(x), int(y), int(cell) - 2, int(cell) - 2,
|
|
63
|
+
Qt.AlignCenter, str(v))
|
|
64
|
+
p.save()
|
|
65
|
+
p.translate(lab, lab - 6)
|
|
66
|
+
for c, pred in enumerate(cls):
|
|
67
|
+
p.setPen(QColor(t.muted))
|
|
68
|
+
p.drawText(int(c * cell), -14, int(cell), 14, Qt.AlignCenter, self._tick(pred))
|
|
69
|
+
p.restore()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DiagnoseGameWidget(QWidget):
|
|
73
|
+
"""spec + source()->[Case] + a renderer widget (show_signature(sig)). Reused by every game."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, theme, spec, source, renderer, live=False, deck: int = 10) -> None:
|
|
76
|
+
super().__init__()
|
|
77
|
+
self.theme = theme
|
|
78
|
+
self.spec = spec
|
|
79
|
+
self._source = source
|
|
80
|
+
self._renderer = renderer
|
|
81
|
+
self.live = live
|
|
82
|
+
self._deck = deck
|
|
83
|
+
self._answer = spec.answer
|
|
84
|
+
self._estimate = (spec.answer == "estimate")
|
|
85
|
+
self._dynamic = spec.answer in ("spot", "rank") # buttons/chips rebuilt per case
|
|
86
|
+
self._has_matrix = (spec.answer == "class")
|
|
87
|
+
self._session = DiagnoseSession(spec, source(), mode=PRACTICE, deck=deck)
|
|
88
|
+
self._class_btns: list = []
|
|
89
|
+
self._opt_btns: list = []
|
|
90
|
+
self._opt_by: dict = {}
|
|
91
|
+
self._order: list = []
|
|
92
|
+
|
|
93
|
+
t = theme.theme
|
|
94
|
+
lay = QHBoxLayout(self)
|
|
95
|
+
# left: mystery + controls
|
|
96
|
+
col = QVBoxLayout()
|
|
97
|
+
col.addWidget(self._lbl(spec.prompt, t.accent_for("purple"), 12, True))
|
|
98
|
+
col.addWidget(renderer, 1)
|
|
99
|
+
self._msg = QLabel(spec.prompt)
|
|
100
|
+
self._msg.setWordWrap(True)
|
|
101
|
+
self._msg.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;"))
|
|
102
|
+
col.addWidget(self._msg)
|
|
103
|
+
if self._estimate:
|
|
104
|
+
row = QHBoxLayout(); row.setSpacing(6)
|
|
105
|
+
self._input = QLineEdit()
|
|
106
|
+
self._input.setPlaceholderText("your estimate" + (f" ({spec.unit})" if spec.unit else ""))
|
|
107
|
+
self._input.setStyleSheet(
|
|
108
|
+
f"QLineEdit{{color:{t.text};background:{t.panel};border:1px solid {t.line};"
|
|
109
|
+
"border-radius:7px;padding:5px 9px;}")
|
|
110
|
+
self._input.returnPressed.connect(self._submit_estimate)
|
|
111
|
+
self._submit = QPushButton("Submit"); self._submit.setStyleSheet(self._btn_css())
|
|
112
|
+
self._submit.clicked.connect(self._submit_estimate)
|
|
113
|
+
row.addWidget(self._input, 1); row.addWidget(self._submit)
|
|
114
|
+
col.addLayout(row)
|
|
115
|
+
elif self._dynamic:
|
|
116
|
+
# candidates come from each case; a host widget we clear + refill per mystery
|
|
117
|
+
self._opts_host = QWidget()
|
|
118
|
+
self._opts_row = QHBoxLayout(self._opts_host)
|
|
119
|
+
self._opts_row.setContentsMargins(0, 0, 0, 0); self._opts_row.setSpacing(5)
|
|
120
|
+
col.addWidget(self._opts_host)
|
|
121
|
+
if self._answer == "rank":
|
|
122
|
+
self._order_lbl = QLabel("your order: —")
|
|
123
|
+
self._order_lbl.setStyleSheet(_scss(f"color:{t.text};font-size:12px;"))
|
|
124
|
+
col.addWidget(self._order_lbl)
|
|
125
|
+
self._clear_order_btn = QPushButton("Clear order")
|
|
126
|
+
self._clear_order_btn.setStyleSheet(self._btn_css())
|
|
127
|
+
self._clear_order_btn.clicked.connect(self._rank_clear)
|
|
128
|
+
col.addWidget(self._clear_order_btn, 0, Qt.AlignLeft)
|
|
129
|
+
else: # class: fixed buttons
|
|
130
|
+
btns = QHBoxLayout(); btns.setSpacing(5)
|
|
131
|
+
for cls in spec.classes:
|
|
132
|
+
b = QPushButton(cls); b.setStyleSheet(self._btn_css())
|
|
133
|
+
b.clicked.connect(lambda _c=False, k=cls: self._guess(k))
|
|
134
|
+
btns.addWidget(b); self._class_btns.append(b)
|
|
135
|
+
self._opt_btns = self._class_btns
|
|
136
|
+
col.addLayout(btns)
|
|
137
|
+
mode_row = QHBoxLayout(); mode_row.setSpacing(5)
|
|
138
|
+
mode_row.addWidget(self._lbl("mode:", t.muted, 11))
|
|
139
|
+
self._mode_btns = {}
|
|
140
|
+
for m in (PRACTICE, GRADED):
|
|
141
|
+
b = QPushButton(m); b.setCheckable(True); b.setStyleSheet(self._toggle_css())
|
|
142
|
+
b.clicked.connect(lambda _c=False, mm=m: self._set_mode(mm))
|
|
143
|
+
mode_row.addWidget(b); self._mode_btns[m] = b
|
|
144
|
+
self._mode_btns[PRACTICE].setChecked(True)
|
|
145
|
+
mode_row.addStretch(1)
|
|
146
|
+
col.addLayout(mode_row)
|
|
147
|
+
lay.addLayout(col, 1)
|
|
148
|
+
|
|
149
|
+
# right: score + scoreboard (confusion matrix for class, recent-guesses for estimate)
|
|
150
|
+
right = QVBoxLayout()
|
|
151
|
+
self._score = QLabel("score 0 / 0")
|
|
152
|
+
self._score.setStyleSheet(_scss(f"color:{t.text};font-size:13px;font-weight:600;"))
|
|
153
|
+
right.addWidget(self._score)
|
|
154
|
+
if self._has_matrix:
|
|
155
|
+
right.addWidget(self._lbl("confusion matrix · true (row) × your guess (col)",
|
|
156
|
+
t.muted, 11))
|
|
157
|
+
self._conf = ConfusionGrid(theme)
|
|
158
|
+
right.addWidget(self._conf, 1)
|
|
159
|
+
else:
|
|
160
|
+
right.addWidget(self._lbl("recent · your answer vs actual", t.muted, 11))
|
|
161
|
+
self._recent = QListWidget()
|
|
162
|
+
self._recent.setStyleSheet(
|
|
163
|
+
f"QListWidget{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
|
|
164
|
+
"border-radius:8px;font-family:monospace;font-size:12px;padding:4px;}")
|
|
165
|
+
right.addWidget(self._recent, 1)
|
|
166
|
+
self._conf = None
|
|
167
|
+
self._reset_btn = QPushButton("Reset")
|
|
168
|
+
self._reset_btn.setStyleSheet(self._btn_css())
|
|
169
|
+
self._reset_btn.clicked.connect(self._reset)
|
|
170
|
+
right.addWidget(self._reset_btn, 0, Qt.AlignRight)
|
|
171
|
+
lay.addLayout(right, 1)
|
|
172
|
+
|
|
173
|
+
self._advance = QTimer(self); self._advance.setSingleShot(True)
|
|
174
|
+
self._advance.timeout.connect(self._new_mystery)
|
|
175
|
+
self._new_mystery()
|
|
176
|
+
|
|
177
|
+
# -- styling ----------------------------------------------------------- #
|
|
178
|
+
def _lbl(self, text, color, size=11, bold=False) -> QLabel:
|
|
179
|
+
lbl = QLabel(text)
|
|
180
|
+
lbl.setStyleSheet(_scss(f"color:{color};font-size:{size}px;"
|
|
181
|
+
+ ("font-weight:600;" if bold else "")))
|
|
182
|
+
return lbl
|
|
183
|
+
|
|
184
|
+
def _btn_css(self) -> str:
|
|
185
|
+
t = self.theme.theme
|
|
186
|
+
return (f"QPushButton{{color:{t.text};background:{t.panel2};border:1px solid {t.line};"
|
|
187
|
+
"border-radius:7px;padding:5px 11px;}"
|
|
188
|
+
f"QPushButton:hover{{border-color:{t.accent};}}"
|
|
189
|
+
f"QPushButton:disabled{{color:{t.faint};}}")
|
|
190
|
+
|
|
191
|
+
def _toggle_css(self) -> str:
|
|
192
|
+
t = self.theme.theme
|
|
193
|
+
return (f"QPushButton{{color:{t.muted};background:{t.panel2};border:1px solid {t.line};"
|
|
194
|
+
"border-radius:7px;padding:3px 10px;font-size:11px;}"
|
|
195
|
+
f"QPushButton:checked{{color:{t.text};border-color:{t.accent};}}")
|
|
196
|
+
|
|
197
|
+
# -- flow -------------------------------------------------------------- #
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _clear_layout(layout) -> None:
|
|
200
|
+
while layout.count():
|
|
201
|
+
it = layout.takeAt(0)
|
|
202
|
+
w = it.widget()
|
|
203
|
+
if w is not None:
|
|
204
|
+
w.setParent(None)
|
|
205
|
+
|
|
206
|
+
def _render_options(self, case) -> None:
|
|
207
|
+
"""Rebuild the per-case candidate buttons/chips (spot + rank)."""
|
|
208
|
+
self._clear_layout(self._opts_row)
|
|
209
|
+
self._opt_btns = []; self._opt_by = {}; self._order = []
|
|
210
|
+
opts = case.options if case.options else self.spec.classes
|
|
211
|
+
for opt in opts:
|
|
212
|
+
b = QPushButton(str(opt)); b.setStyleSheet(self._btn_css())
|
|
213
|
+
if self._answer == "rank":
|
|
214
|
+
b.clicked.connect(lambda _c=False, o=opt: self._rank_click(o))
|
|
215
|
+
else:
|
|
216
|
+
b.clicked.connect(lambda _c=False, o=opt: self._guess(o))
|
|
217
|
+
self._opts_row.addWidget(b)
|
|
218
|
+
self._opt_btns.append(b); self._opt_by[opt] = b
|
|
219
|
+
self._opts_row.addStretch(1)
|
|
220
|
+
if self._answer == "rank":
|
|
221
|
+
self._order_lbl.setText("your order: —")
|
|
222
|
+
|
|
223
|
+
def _rank_click(self, opt) -> None:
|
|
224
|
+
if opt in self._order:
|
|
225
|
+
return
|
|
226
|
+
self._order.append(opt)
|
|
227
|
+
b = self._opt_by.get(opt)
|
|
228
|
+
if b is not None:
|
|
229
|
+
b.setEnabled(False)
|
|
230
|
+
self._order_lbl.setText("your order: " + " → ".join(str(o) for o in self._order))
|
|
231
|
+
if len(self._order) == len(self._opt_btns): # complete -> submit
|
|
232
|
+
self._guess(list(self._order))
|
|
233
|
+
|
|
234
|
+
def _rank_clear(self) -> None:
|
|
235
|
+
self._order = []
|
|
236
|
+
for b in self._opt_btns:
|
|
237
|
+
b.setEnabled(True)
|
|
238
|
+
self._order_lbl.setText("your order: —")
|
|
239
|
+
|
|
240
|
+
def _set_answer_enabled(self, on: bool) -> None:
|
|
241
|
+
if self._estimate:
|
|
242
|
+
self._input.setEnabled(on); self._submit.setEnabled(on)
|
|
243
|
+
if on:
|
|
244
|
+
self._input.clear(); self._input.setFocus()
|
|
245
|
+
return
|
|
246
|
+
for b in self._opt_btns:
|
|
247
|
+
b.setEnabled(on)
|
|
248
|
+
if self._answer == "rank":
|
|
249
|
+
self._clear_order_btn.setEnabled(on)
|
|
250
|
+
|
|
251
|
+
def _set_mode(self, mode) -> None:
|
|
252
|
+
for m, b in self._mode_btns.items():
|
|
253
|
+
b.setChecked(m == mode)
|
|
254
|
+
self._session = DiagnoseSession(self.spec, self._source(), mode=mode, deck=self._deck)
|
|
255
|
+
self._clear_board()
|
|
256
|
+
self._score.setText("score 0 / 0")
|
|
257
|
+
self._new_mystery()
|
|
258
|
+
|
|
259
|
+
def _new_mystery(self) -> None:
|
|
260
|
+
self._session.set_cases(self._source())
|
|
261
|
+
case = self._session.next()
|
|
262
|
+
if case is not None:
|
|
263
|
+
self._renderer.show_signature(case.signature)
|
|
264
|
+
self._msg.setText(self.spec.prompt)
|
|
265
|
+
if self._dynamic:
|
|
266
|
+
self._render_options(case)
|
|
267
|
+
self._set_answer_enabled(True)
|
|
268
|
+
return
|
|
269
|
+
self._renderer.show_signature(None)
|
|
270
|
+
self._set_answer_enabled(False)
|
|
271
|
+
if self._session.finished:
|
|
272
|
+
hit, tot = self._session.score()
|
|
273
|
+
self._msg.setText(f"Run complete — {hit}/{tot} "
|
|
274
|
+
f"({round(self._session.accuracy() * 100)}%). Reset for a new run.")
|
|
275
|
+
else:
|
|
276
|
+
self._msg.setText("No cases yet — launch some programs to play, or switch to Demo.")
|
|
277
|
+
|
|
278
|
+
def _submit_estimate(self) -> None:
|
|
279
|
+
raw = self._input.text().strip()
|
|
280
|
+
try:
|
|
281
|
+
val = int(raw, 0) # accepts 0x… hex and decimal
|
|
282
|
+
except ValueError:
|
|
283
|
+
try:
|
|
284
|
+
val = float(raw)
|
|
285
|
+
except ValueError:
|
|
286
|
+
self._msg.setText("Enter a number (decimal or 0x… hex).")
|
|
287
|
+
return
|
|
288
|
+
self._guess(val)
|
|
289
|
+
|
|
290
|
+
def _guess(self, answer) -> None:
|
|
291
|
+
v = self._session.guess(answer)
|
|
292
|
+
if not v:
|
|
293
|
+
return
|
|
294
|
+
sub = f" ({v['subtitle']})" if v.get("subtitle") else ""
|
|
295
|
+
hint = f" · hint: {v['hint']}" if v.get("hint") else ""
|
|
296
|
+
if self._has_matrix:
|
|
297
|
+
verdict = "✓ correct" if v["correct"] else f"✗ it was {v['truth']}"
|
|
298
|
+
self._msg.setText(verdict + sub + hint)
|
|
299
|
+
self._conf.set_matrix(self._session.matrix(), self.spec.classes, self.spec.abbrev)
|
|
300
|
+
elif self._answer == "rank":
|
|
301
|
+
pct = round((v.get("partial") or 0) * 100)
|
|
302
|
+
verdict = "✓ perfect order" if v["correct"] else f"✗ ({pct}% ordered)"
|
|
303
|
+
self._msg.setText(f"{verdict} — actual: {' → '.join(map(str, v['truth']))}{hint}")
|
|
304
|
+
self._recent.insertItem(0, f"{'✓' if v['correct'] else f'{pct}%'} "
|
|
305
|
+
f"{' '.join(map(str, answer))}")
|
|
306
|
+
else: # estimate + spot
|
|
307
|
+
verdict = "✓ correct" if v["correct"] else f"✗ it was {v['truth']}"
|
|
308
|
+
self._msg.setText(f"{verdict} — you said {answer}{sub}{hint}")
|
|
309
|
+
self._recent.insertItem(0, f"{'✓' if v['correct'] else '✗'} you {answer} "
|
|
310
|
+
f"actual {v['truth']}")
|
|
311
|
+
self._update_score()
|
|
312
|
+
self._advance.start(950)
|
|
313
|
+
|
|
314
|
+
def _update_score(self) -> None:
|
|
315
|
+
hit, tot = self._session.score()
|
|
316
|
+
txt = f"score {hit} / {tot} · accuracy {round(self._session.accuracy() * 100)}%"
|
|
317
|
+
if self._estimate:
|
|
318
|
+
txt += f" · mean error {round(self._session.mean_abs_error(), 1)}"
|
|
319
|
+
elif self._answer == "rank":
|
|
320
|
+
txt += f" · order {round(self._session.mean_order_score() * 100)}%"
|
|
321
|
+
rem = self._session.remaining()
|
|
322
|
+
if rem is not None:
|
|
323
|
+
txt += f" · {rem} left"
|
|
324
|
+
self._score.setText(txt)
|
|
325
|
+
|
|
326
|
+
def _clear_board(self) -> None:
|
|
327
|
+
if self._has_matrix:
|
|
328
|
+
self._conf.set_matrix({}, self.spec.classes, self.spec.abbrev)
|
|
329
|
+
else:
|
|
330
|
+
self._recent.clear()
|
|
331
|
+
|
|
332
|
+
def _reset(self) -> None:
|
|
333
|
+
self._session.reset()
|
|
334
|
+
self._clear_board()
|
|
335
|
+
self._score.setText("score 0 / 0")
|
|
336
|
+
self._new_mystery()
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Process Fingerprints — behavioral-signature view over the Machine Lab's real telemetry.
|
|
2
|
+
|
|
3
|
+
• Explore — a radar (signature) for the selected process + a scatter BOARD where programs self-sort
|
|
4
|
+
into CPU-bound vs IO-bound / compute vs kernel.
|
|
5
|
+
• Classify game — the shared DiagnoseGameWidget (domain/diagnose.py) fed by the process case source:
|
|
6
|
+
a mystery radar with the name hidden; guess the class, scored by a confusion matrix vs the oracle.
|
|
7
|
+
|
|
8
|
+
Live: a FingerprintAccumulator ingests procdump states + the syscall/trap rings each poll. Demo: the
|
|
9
|
+
canned demo_features(), so the panel and the game work fully offline.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from PySide6.QtCore import Qt, QTimer, Signal
|
|
14
|
+
from PySide6.QtWidgets import (
|
|
15
|
+
QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, QTabWidget, QVBoxLayout, QWidget,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ..domain.fingerprint import FingerprintAccumulator, demo_features, fingerprint, scatter_xy
|
|
19
|
+
from ..domain.games.process_game import PROCESS_SPEC, demo_cases, live_cases
|
|
20
|
+
from ..domain.xv6 import parse_sctrace, parse_traptrace
|
|
21
|
+
from .diagnose_game import DiagnoseGameWidget
|
|
22
|
+
from .game_renderers import RadarChart, ScatterBoard
|
|
23
|
+
from .theme import ThemeManager, icons
|
|
24
|
+
from .theme.manager import scale_css as _scss
|
|
25
|
+
|
|
26
|
+
_PALETTE = ["blue", "green", "purple", "amber", "teal", "pink", "orange", "indigo", "red", "cyan"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FingerprintLab(QWidget):
|
|
30
|
+
refreshed = Signal()
|
|
31
|
+
|
|
32
|
+
def __init__(self, parent, theme: ThemeManager, device, state, live=False) -> None:
|
|
33
|
+
super().__init__(parent)
|
|
34
|
+
self.setWindowFlag(Qt.Window, True)
|
|
35
|
+
self.theme = theme
|
|
36
|
+
self.device = device
|
|
37
|
+
self.state = state
|
|
38
|
+
self.live = live
|
|
39
|
+
self._closed = False
|
|
40
|
+
self._busy = False
|
|
41
|
+
self._acc = FingerprintAccumulator()
|
|
42
|
+
self._seen_sc: set = set()
|
|
43
|
+
self._seen_tr: set = set()
|
|
44
|
+
|
|
45
|
+
t = theme.theme
|
|
46
|
+
self.setWindowTitle(f"Process Fingerprints Lab — {device.name}")
|
|
47
|
+
self.resize(780, 620)
|
|
48
|
+
self.setStyleSheet(f"QWidget{{background:{t.bg};}}")
|
|
49
|
+
root = QVBoxLayout(self)
|
|
50
|
+
|
|
51
|
+
head = QHBoxLayout()
|
|
52
|
+
ic = QLabel(); ic.setPixmap(icons.render_pixmap("metrics", t.accent_for("purple"), 22))
|
|
53
|
+
title = QLabel(f" Process Fingerprints Lab — {device.name}")
|
|
54
|
+
title.setStyleSheet(_scss(f"color:{t.text};font-size:16px;font-weight:600;"))
|
|
55
|
+
head.addWidget(ic); head.addWidget(title); head.addStretch(1)
|
|
56
|
+
self._reset_btn = QPushButton("Reset")
|
|
57
|
+
self._reset_btn.setStyleSheet(self._btn_css())
|
|
58
|
+
self._reset_btn.clicked.connect(self._reset)
|
|
59
|
+
head.addWidget(self._reset_btn)
|
|
60
|
+
chip = QLabel("live" if live else "offline demo")
|
|
61
|
+
chip.setStyleSheet(f"color:{t.success if live else t.muted};background:{t.panel2};"
|
|
62
|
+
f"border:1px solid {t.line};border-radius:9px;padding:2px 10px;"
|
|
63
|
+
"font-size:11px;")
|
|
64
|
+
head.addWidget(chip)
|
|
65
|
+
root.addLayout(head)
|
|
66
|
+
|
|
67
|
+
self._tabs = QTabWidget()
|
|
68
|
+
self._tabs.setStyleSheet(
|
|
69
|
+
f"QTabWidget::pane{{border:1px solid {t.line};border-radius:8px;}}"
|
|
70
|
+
f"QTabBar::tab{{background:{t.panel2};color:{t.muted};padding:6px 14px;"
|
|
71
|
+
f"border:1px solid {t.line};border-bottom:none;border-top-left-radius:7px;"
|
|
72
|
+
"border-top-right-radius:7px;margin-right:2px;}"
|
|
73
|
+
f"QTabBar::tab:selected{{color:{t.text};background:{t.panel};}}")
|
|
74
|
+
root.addWidget(self._tabs, 1)
|
|
75
|
+
self._build_explore()
|
|
76
|
+
# the classify game is the shared engine, fed by the process case source (live or demo)
|
|
77
|
+
self._game = DiagnoseGameWidget(theme, PROCESS_SPEC, self._game_source, RadarChart(theme),
|
|
78
|
+
live=self.live)
|
|
79
|
+
self._tabs.addTab(self._game, "Classify game")
|
|
80
|
+
|
|
81
|
+
self.refreshed.connect(self._refresh_views)
|
|
82
|
+
self._poll = QTimer(self); self._poll.timeout.connect(self._tick)
|
|
83
|
+
self._refresh_views()
|
|
84
|
+
if self.live:
|
|
85
|
+
self._poll.start(1200)
|
|
86
|
+
|
|
87
|
+
# -- styling ----------------------------------------------------------- #
|
|
88
|
+
def _btn_css(self) -> str:
|
|
89
|
+
t = self.theme.theme
|
|
90
|
+
return (f"QPushButton{{color:{t.text};background:{t.panel2};border:1px solid {t.line};"
|
|
91
|
+
"border-radius:7px;padding:4px 12px;}"
|
|
92
|
+
f"QPushButton:hover{{border-color:{t.accent};}}")
|
|
93
|
+
|
|
94
|
+
def _lbl(self, text) -> QLabel:
|
|
95
|
+
lbl = QLabel(text)
|
|
96
|
+
lbl.setStyleSheet(_scss(f"color:{self.theme.theme.muted};font-size:11px;font-weight:600;"))
|
|
97
|
+
return lbl
|
|
98
|
+
|
|
99
|
+
# -- Explore tab ------------------------------------------------------- #
|
|
100
|
+
def _build_explore(self) -> None:
|
|
101
|
+
t = self.theme.theme
|
|
102
|
+
page = QWidget(); lay = QHBoxLayout(page)
|
|
103
|
+
left = QVBoxLayout()
|
|
104
|
+
left.addWidget(self._lbl("processes"))
|
|
105
|
+
self._list = QListWidget()
|
|
106
|
+
self._list.setStyleSheet(
|
|
107
|
+
f"QListWidget{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
|
|
108
|
+
"border-radius:8px;font-size:12px;padding:3px;}"
|
|
109
|
+
f"QListWidget::item:selected{{background:{t.panel2};color:{t.text};}}")
|
|
110
|
+
self._list.setFixedWidth(150)
|
|
111
|
+
self._list.currentRowChanged.connect(lambda _r: self._draw_radar())
|
|
112
|
+
left.addWidget(self._list, 1)
|
|
113
|
+
lay.addLayout(left)
|
|
114
|
+
|
|
115
|
+
mid = QVBoxLayout()
|
|
116
|
+
mid.addWidget(self._lbl("signature · radar"))
|
|
117
|
+
self._radar = RadarChart(self.theme)
|
|
118
|
+
mid.addWidget(self._radar, 1)
|
|
119
|
+
lay.addLayout(mid, 1)
|
|
120
|
+
|
|
121
|
+
right = QVBoxLayout()
|
|
122
|
+
right.addWidget(self._lbl("behavior map · all processes"))
|
|
123
|
+
self._board = ScatterBoard(self.theme)
|
|
124
|
+
right.addWidget(self._board, 1)
|
|
125
|
+
lay.addLayout(right, 1)
|
|
126
|
+
self._tabs.addTab(page, "Explore")
|
|
127
|
+
|
|
128
|
+
# -- data -------------------------------------------------------------- #
|
|
129
|
+
def _tick(self) -> None:
|
|
130
|
+
if self._closed or self._busy:
|
|
131
|
+
return
|
|
132
|
+
import threading
|
|
133
|
+
threading.Thread(target=self._accumulate, daemon=True).start()
|
|
134
|
+
|
|
135
|
+
def _accumulate(self) -> None:
|
|
136
|
+
self._busy = True
|
|
137
|
+
try:
|
|
138
|
+
self.state.refresh()
|
|
139
|
+
procs = self.state.latest.procs if self.state.latest else []
|
|
140
|
+
sc = self._new_events(self._read("sc"), parse_sctrace, self._seen_sc,
|
|
141
|
+
lambda e: (e.pid, e.num, e.a0, e.ret))
|
|
142
|
+
tr = self._new_events(self._read("traps"), parse_traptrace, self._seen_tr,
|
|
143
|
+
lambda e: (e.pid, e.kind, e.epc, e.tval))
|
|
144
|
+
self._acc.observe(procs, sc, tr, dt=1.2)
|
|
145
|
+
if not self._closed:
|
|
146
|
+
self.refreshed.emit()
|
|
147
|
+
except (Exception, RuntimeError):
|
|
148
|
+
pass
|
|
149
|
+
finally:
|
|
150
|
+
self._busy = False
|
|
151
|
+
|
|
152
|
+
def _read(self, method) -> str:
|
|
153
|
+
fn = getattr(self.state.provider, method, None)
|
|
154
|
+
try:
|
|
155
|
+
return fn() if callable(fn) else ""
|
|
156
|
+
except Exception:
|
|
157
|
+
return ""
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _new_events(text, parse, seen, sig):
|
|
161
|
+
out = []
|
|
162
|
+
for e in parse(text):
|
|
163
|
+
s = sig(e)
|
|
164
|
+
if s not in seen:
|
|
165
|
+
seen.add(s); out.append(e)
|
|
166
|
+
if len(seen) > 4000:
|
|
167
|
+
seen.clear()
|
|
168
|
+
return out
|
|
169
|
+
|
|
170
|
+
def _collect(self) -> dict:
|
|
171
|
+
"""{pid: (name, fp)} — accumulator (live) or the canned demo set (offline)."""
|
|
172
|
+
if self.live:
|
|
173
|
+
return {pid: (self._acc.feats[pid].name or f"pid{pid}", fp)
|
|
174
|
+
for pid, fp in self._acc.fingerprints().items()}
|
|
175
|
+
return {f.pid: (f.name, fingerprint(f)) for f in demo_features()}
|
|
176
|
+
|
|
177
|
+
def _game_source(self) -> list:
|
|
178
|
+
"""Cases for the classify game — live fingerprints when running, else the demo deck."""
|
|
179
|
+
if self.live:
|
|
180
|
+
fps = self._acc.fingerprints()
|
|
181
|
+
names = {pid: self._acc.feats[pid].name for pid in fps}
|
|
182
|
+
return live_cases(fps, names)
|
|
183
|
+
return demo_cases()
|
|
184
|
+
|
|
185
|
+
def _color(self, i) -> str:
|
|
186
|
+
return _PALETTE[i % len(_PALETTE)]
|
|
187
|
+
|
|
188
|
+
def _refresh_views(self) -> None:
|
|
189
|
+
data = list(self._collect().items())
|
|
190
|
+
cur = self._list.currentRow()
|
|
191
|
+
self._list.blockSignals(True)
|
|
192
|
+
self._list.clear()
|
|
193
|
+
for pid, (name, _fp) in data:
|
|
194
|
+
QListWidgetItem(f"{name} · {pid}", self._list)
|
|
195
|
+
self._list.setCurrentRow(cur if 0 <= cur < len(data) else (0 if data else -1))
|
|
196
|
+
self._list.blockSignals(False)
|
|
197
|
+
self._draw_radar()
|
|
198
|
+
pts = [(*scatter_xy(fp), name, self._color(i))
|
|
199
|
+
for i, (pid, (name, fp)) in enumerate(data)]
|
|
200
|
+
self._board.set_points(pts)
|
|
201
|
+
|
|
202
|
+
def _draw_radar(self) -> None:
|
|
203
|
+
data = list(self._collect().items())
|
|
204
|
+
row = self._list.currentRow()
|
|
205
|
+
if 0 <= row < len(data):
|
|
206
|
+
_pid, (name, fp) = data[row]
|
|
207
|
+
self._radar.set_series([(name, fp, self._color(row))])
|
|
208
|
+
else:
|
|
209
|
+
self._radar.set_series([])
|
|
210
|
+
|
|
211
|
+
def _reset(self) -> None:
|
|
212
|
+
self._acc.reset(); self._seen_sc.clear(); self._seen_tr.clear()
|
|
213
|
+
self._game._reset()
|
|
214
|
+
self._refresh_views()
|
|
215
|
+
|
|
216
|
+
def closeEvent(self, e) -> None: # noqa: N802
|
|
217
|
+
self._closed = True
|
|
218
|
+
self._poll.stop()
|
|
219
|
+
super().closeEvent(e)
|