cct-cli 0.7.9.0__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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from .mathtext import frac_part as F, sub, sup
|
|
2
|
+
|
|
3
|
+
FORMULAS = [
|
|
4
|
+
{
|
|
5
|
+
"name": "First Order Rate Constant",
|
|
6
|
+
"tag": "Kinetics",
|
|
7
|
+
"formula": ["k = ", F("2.303", "t"), f" log(R{sub(0)}/R)"],
|
|
8
|
+
"meaning": "Gives the rate constant of a first order reaction from concentrations at two times.",
|
|
9
|
+
"variables": "k = rate constant, t = time elapsed, R0 = initial concentration, R = concentration at time t",
|
|
10
|
+
"units": f"k -> time{sup(-1)} (e.g. s{sup(-1)}, min{sup(-1)})",
|
|
11
|
+
"conditions": "Valid only for reactions that are first order overall.",
|
|
12
|
+
"mistakes": "Forgetting the 2.303 factor when converting ln to log10; mixing up R0 and R inside the log.",
|
|
13
|
+
"trick": "log(R0/R) is always positive since R0 > R for a forward reaction — a negative result signals swapped values.",
|
|
14
|
+
"memory": "\"2.303 over t, log the ratio\" — say it as one phrase.",
|
|
15
|
+
"example": "R0 = 0.8 M, R = 0.4 M, t = 20 min -> k = (2.303/20) log(2) = 0.0347 min-1",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "First Order Half-Life",
|
|
19
|
+
"tag": "Kinetics",
|
|
20
|
+
"formula": ["t\u00bd = ", F("0.693", "k")],
|
|
21
|
+
"meaning": "Time required for concentration to fall to half its initial value; constant for first order reactions.",
|
|
22
|
+
"variables": "t1/2 = half-life, k = rate constant",
|
|
23
|
+
"units": "t1/2 -> same time unit used for k-1",
|
|
24
|
+
"conditions": "Independent of initial concentration — a hallmark of first order kinetics.",
|
|
25
|
+
"mistakes": "Using this formula for zero or second order reactions, where half-life depends on concentration.",
|
|
26
|
+
"trick": "0.693 is just ln(2) — recall ln(2) \u2248 0.693 to rebuild the formula instantly.",
|
|
27
|
+
"memory": "\"Half-life never lies\" for first order — always the same value.",
|
|
28
|
+
"example": "k = 0.0347 min-1 -> t1/2 = 0.693/0.0347 \u2248 20 min",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "Zero Order Rate Law",
|
|
32
|
+
"tag": "Kinetics",
|
|
33
|
+
"formula": ["k = ", F(f"R{sub(0)} \u2212 R", "t")],
|
|
34
|
+
"meaning": "Rate constant for a zero order reaction, where rate is independent of concentration.",
|
|
35
|
+
"variables": "R0 = initial concentration, R = concentration at time t, t = time",
|
|
36
|
+
"units": f"k -> mol L{sup(-1)} time{sup(-1)}",
|
|
37
|
+
"conditions": "Applies when reaction rate does not change with reactant concentration (e.g. surface-catalysed reactions).",
|
|
38
|
+
"mistakes": "Applying the first-order log formula by mistake; forgetting that k here carries concentration units.",
|
|
39
|
+
"trick": "Zero order graphs are a straight line of [R] vs t — spot it visually before calculating.",
|
|
40
|
+
"memory": "\"Zero order, straight decline.\"",
|
|
41
|
+
"example": "R0 = 1.0 M, R = 0.8 M, t = 20 min -> k = (1.0-0.8)/20 = 0.01 mol L-1 min-1",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "Second Order Rate Law",
|
|
45
|
+
"tag": "Kinetics",
|
|
46
|
+
"formula": [F("1", "R"), " = ", F("1", f"R{sub(0)}"), " + kt"],
|
|
47
|
+
"meaning": "Relates concentration to time for a second order reaction.",
|
|
48
|
+
"variables": "R0 = initial concentration, R = concentration at time t, k = rate constant, t = time",
|
|
49
|
+
"units": f"k -> L mol{sup(-1)} time{sup(-1)}",
|
|
50
|
+
"conditions": "Applies when rate depends on the square of one reactant's concentration (or product of two reactants).",
|
|
51
|
+
"mistakes": "Forgetting to invert both concentrations before subtracting; unit mismatch with first-order k.",
|
|
52
|
+
"trick": "A straight line of 1/[R] vs t confirms second order behaviour.",
|
|
53
|
+
"memory": "\"Flip it, then add kt.\"",
|
|
54
|
+
"example": "R0 = 1 M, k = 0.02 L mol-1 min-1, t = 20 min -> 1/R = 1 + 0.4 = 1.4 -> R \u2248 0.714 M",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "Arrhenius Equation",
|
|
58
|
+
"tag": "Kinetics",
|
|
59
|
+
"formula": ["k = A e", "^(\u2212Ea/RT)"],
|
|
60
|
+
"meaning": "Describes how rate constant depends on temperature and activation energy.",
|
|
61
|
+
"variables": "k = rate constant, A = pre-exponential factor, Ea = activation energy, R = gas constant, T = temperature (K)",
|
|
62
|
+
"units": "Ea -> J/mol (use R = 8.314 J/mol/K) or kJ/mol (use R = 0.008314 kJ/mol/K)",
|
|
63
|
+
"conditions": "Temperature must be in Kelvin; assumes Ea is constant over the range considered.",
|
|
64
|
+
"mistakes": "Mixing kJ and J units for Ea and R; using \u00b0C instead of Kelvin.",
|
|
65
|
+
"trick": f"For two temperatures, skip solving for A — use ln(k{sub(2)}/k{sub(1)}) = \u2212Ea/R (1/T{sub(2)} \u2212 1/T{sub(1)}).",
|
|
66
|
+
"memory": "\"Bigger T, bigger k — always increasing.\"",
|
|
67
|
+
"example": "Ea = 75 kJ/mol, k1 at 300 K, T2 = 310 K -> k2 > k1, computed via the two-point form.",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "Mole-Mass Relation",
|
|
71
|
+
"tag": "Mole Concept",
|
|
72
|
+
"formula": ["n = ", F("m", "M")],
|
|
73
|
+
"meaning": "Converts a given mass of a substance into number of moles.",
|
|
74
|
+
"variables": "n = number of moles, m = given mass, M = molar mass",
|
|
75
|
+
"units": "n -> mol, m -> g, M -> g/mol",
|
|
76
|
+
"conditions": "M must correspond to the correct chemical formula (watch for hydrates, ions).",
|
|
77
|
+
"mistakes": "Using atomic mass instead of molar mass for compounds; forgetting water of crystallisation in hydrates.",
|
|
78
|
+
"trick": "Sanity check: multiply n x M and confirm it returns the original mass.",
|
|
79
|
+
"memory": "\"Moles = mass over molar mass — m over M.\"",
|
|
80
|
+
"example": "5 g of NaOH (M = 40 g/mol) -> n = 5/40 = 0.125 mol",
|
|
81
|
+
},
|
|
82
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.7.9.0 — fs_cache.py: safe, self-invalidating filesystem metadata
|
|
3
|
+
cache (requirement #12 — SMART CACHING).
|
|
4
|
+
|
|
5
|
+
Caches the expensive-to-rebuild, cheap-to-key results CAT asks for over
|
|
6
|
+
and over inside one agent run:
|
|
7
|
+
|
|
8
|
+
* directory listings (keyed by path; invalidated by that directory's
|
|
9
|
+
mtime AND by explicit mutation notifications)
|
|
10
|
+
* workspace index (keyed by root + a cheap signature of the tree)
|
|
11
|
+
|
|
12
|
+
Correctness rules:
|
|
13
|
+
* NEVER return stale data after a known mutation: every write/rename/
|
|
14
|
+
delete tool call in agent.py notifies invalidate_path(), and each cache
|
|
15
|
+
hit re-checks the directory's own mtime first — an external change made
|
|
16
|
+
outside CAT is picked up automatically.
|
|
17
|
+
* TTL is short (2 s) on purpose: it collapses repeated scans WITHIN one
|
|
18
|
+
agent step burst without ever serving yesterday's view of the world.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
print("This is a library file and is not meant to be run directly.")
|
|
23
|
+
import sys
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import threading
|
|
28
|
+
import time
|
|
29
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
30
|
+
|
|
31
|
+
DEFAULT_TTL = 2.0 # seconds
|
|
32
|
+
MAX_ENTRIES = 256
|
|
33
|
+
|
|
34
|
+
_lock = threading.Lock()
|
|
35
|
+
_store: Dict[str, Tuple[float, Any]] = {} # key -> (expires_at, value)
|
|
36
|
+
_signatures: Dict[str, Tuple] = {} # key -> validity signature
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _dir_signature(path: str) -> Tuple:
|
|
40
|
+
"""Cheap validity probe: directory mtime (+ its own existence). An
|
|
41
|
+
external add/remove/update inside the dir changes st_mtime, so the
|
|
42
|
+
cached listing goes stale immediately."""
|
|
43
|
+
try:
|
|
44
|
+
st = os.stat(path)
|
|
45
|
+
return (st.st_mtime_ns, st.st_size)
|
|
46
|
+
except OSError:
|
|
47
|
+
return ("missing",)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_cached(key: str):
|
|
51
|
+
with _lock:
|
|
52
|
+
entry = _store.get(key)
|
|
53
|
+
if entry is None:
|
|
54
|
+
return None
|
|
55
|
+
expires_at, value = entry
|
|
56
|
+
if expires_at < time.time():
|
|
57
|
+
_store.pop(key, None)
|
|
58
|
+
_signatures.pop(key, None)
|
|
59
|
+
return None
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def put_cached(key: str, value, ttl: float = DEFAULT_TTL,
|
|
64
|
+
signature: Optional[Tuple] = None) -> None:
|
|
65
|
+
with _lock:
|
|
66
|
+
if len(_store) >= MAX_ENTRIES:
|
|
67
|
+
# Drop the soonest-expiring entries first.
|
|
68
|
+
for k in sorted(_store, key=lambda k: _store[k][0])[:32]:
|
|
69
|
+
_store.pop(k, None)
|
|
70
|
+
_signatures.pop(k, None)
|
|
71
|
+
_store[key] = (time.time() + max(0.05, ttl), value)
|
|
72
|
+
if signature is not None:
|
|
73
|
+
_signatures[key] = signature
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def valid_signature(key: str, signature: Tuple) -> bool:
|
|
77
|
+
with _lock:
|
|
78
|
+
return _signatures.get(key) == signature
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def invalidate_path(path: str) -> None:
|
|
82
|
+
"""Called by every mutating tool (write/create/delete/rename) so no
|
|
83
|
+
cached listing can ever outlive a known change. Invalidates the path
|
|
84
|
+
itself and all cached ancestors (a new file changes the parent's
|
|
85
|
+
listing too)."""
|
|
86
|
+
path = os.path.normpath(os.path.abspath(str(path)))
|
|
87
|
+
with _lock:
|
|
88
|
+
for key in [k for k in _store if k.startswith("dirls:")]:
|
|
89
|
+
try:
|
|
90
|
+
cached_path = key[len("dirls:"):]
|
|
91
|
+
norm = os.path.normpath(os.path.abspath(cached_path))
|
|
92
|
+
except Exception:
|
|
93
|
+
continue
|
|
94
|
+
if norm == path or path.startswith(norm + os.sep) \
|
|
95
|
+
or norm.startswith(path + os.sep):
|
|
96
|
+
_store.pop(key, None)
|
|
97
|
+
_signatures.pop(key, None)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def clear() -> None:
|
|
101
|
+
with _lock:
|
|
102
|
+
_store.clear()
|
|
103
|
+
_signatures.clear()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def cached_dir_listing(path: str, builder: Callable[[], Any],
|
|
107
|
+
ttl: float = DEFAULT_TTL):
|
|
108
|
+
"""TTL+mtime-guarded directory listing. `builder` does the real scan;
|
|
109
|
+
it only runs when there is no fresh, still-valid cached copy."""
|
|
110
|
+
try:
|
|
111
|
+
real = os.path.abspath(path)
|
|
112
|
+
except Exception:
|
|
113
|
+
return builder()
|
|
114
|
+
key = "dirls:" + real
|
|
115
|
+
sig = _dir_signature(real)
|
|
116
|
+
cached = get_cached(key)
|
|
117
|
+
if cached is not None and valid_signature(key, sig):
|
|
118
|
+
return cached
|
|
119
|
+
value = builder()
|
|
120
|
+
put_cached(key, value, ttl=ttl, signature=sig)
|
|
121
|
+
return value
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT v0.7.9.0 — fs_watcher.py: real-time filesystem watching for the
|
|
3
|
+
live File Explorer.
|
|
4
|
+
|
|
5
|
+
Architecture (the whole point of this module):
|
|
6
|
+
|
|
7
|
+
REAL FILESYSTEM
|
|
8
|
+
|
|
|
9
|
+
watcher (watchdog Observer, or a stdlib snapshot-poll fallback)
|
|
10
|
+
|
|
|
11
|
+
debounced filesystem events -> callback(set_of_changed_paths)
|
|
12
|
+
|
|
|
13
|
+
Workspace state manager (ui/app.py bridges to the UI thread)
|
|
14
|
+
|
|
|
15
|
+
Explorer tree refresh
|
|
16
|
+
|
|
17
|
+
The callback is ALWAYS invoked on a background thread — it must never
|
|
18
|
+
touch widgets. ui/app.py's `_on_fs_events` marshals the paths back to
|
|
19
|
+
the UI thread with call_from_thread and posts a WorkspaceFilesChanged
|
|
20
|
+
Message, exactly like every other worker-to-UI crossing in this app.
|
|
21
|
+
|
|
22
|
+
Design notes:
|
|
23
|
+
|
|
24
|
+
* watchdog's Observer is used when importable (real OS push events on
|
|
25
|
+
Windows — ReadDirectoryChangesW — plus inotify/FSEvents elsewhere).
|
|
26
|
+
When it isn't installed, a low-overhead stdlib poller takes over: it
|
|
27
|
+
walks only the active workspace root every POLL_INTERVAL seconds,
|
|
28
|
+
compares name/mtime snapshots, and reports only actual differences.
|
|
29
|
+
That fallback is throttled by construction (one walk per interval,
|
|
30
|
+
nothing between ticks) — never a busy loop.
|
|
31
|
+
* Events are debounced: bursts of related changes (a save writing a
|
|
32
|
+
temp file then renaming it over the target; `git checkout` touching
|
|
33
|
+
hundreds of files) coalesce into one callback ~DEBOUNCE seconds after
|
|
34
|
+
the last raw event, so the Explorer reloads once, not N times.
|
|
35
|
+
* One watcher instance per app. `start(root)` stops whatever root was
|
|
36
|
+
being watched before (new workspace => stop old watcher => start new
|
|
37
|
+
watcher), so the Explorer can never observe a stale folder.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
print("This is a library file and is not meant to be run directly.")
|
|
42
|
+
import sys
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
import os
|
|
46
|
+
import threading
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from watchdog.observers import Observer
|
|
50
|
+
from watchdog.events import FileSystemEventHandler as _WdHandler
|
|
51
|
+
WATCHDOG_AVAILABLE = True
|
|
52
|
+
except Exception:
|
|
53
|
+
WATCHDOG_AVAILABLE = False
|
|
54
|
+
Observer = None
|
|
55
|
+
|
|
56
|
+
class _WdHandler: # stand-in so the class body below still parses
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class WorkspaceWatcher:
|
|
61
|
+
"""Watches one workspace root and reports debounced change sets.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
callback : callable(set[str])
|
|
66
|
+
Invoked with the set of changed absolute paths. Always called
|
|
67
|
+
from a background thread (the Observer's emitter thread or the
|
|
68
|
+
fallback poller thread). Exceptions raised inside it must be
|
|
69
|
+
its own responsibility.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
DEBOUNCE = 0.35 # seconds of quiet before flushing a burst
|
|
73
|
+
POLL_INTERVAL = 2.0 # fallback poll cadence (no watchdog)
|
|
74
|
+
|
|
75
|
+
def __init__(self, callback):
|
|
76
|
+
self._callback = callback
|
|
77
|
+
self._root = None
|
|
78
|
+
self._lock = threading.Lock()
|
|
79
|
+
self._pending = set()
|
|
80
|
+
self._debounce_timer = None
|
|
81
|
+
self._observer = None
|
|
82
|
+
self._poll_thread = None
|
|
83
|
+
self._poll_stop = threading.Event()
|
|
84
|
+
self._snapshot = {}
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------------ start --
|
|
87
|
+
def start(self, root):
|
|
88
|
+
"""(Re)points the watcher at `root`. Stops the previous watch
|
|
89
|
+
first, so switching workspaces never leaves an old Observer (or
|
|
90
|
+
old poller) running against a folder that is no longer open."""
|
|
91
|
+
root = os.path.abspath(os.path.expanduser(root))
|
|
92
|
+
if not os.path.isdir(root):
|
|
93
|
+
return False
|
|
94
|
+
self.stop()
|
|
95
|
+
with self._lock:
|
|
96
|
+
self._root = root
|
|
97
|
+
self._pending.clear()
|
|
98
|
+
if WATCHDOG_AVAILABLE:
|
|
99
|
+
return self._start_observer(root)
|
|
100
|
+
return self._start_poller(root)
|
|
101
|
+
|
|
102
|
+
def _start_observer(self, root):
|
|
103
|
+
try:
|
|
104
|
+
observer = Observer(timeout=0.5)
|
|
105
|
+
observer.daemon = True
|
|
106
|
+
handler = _Handler(self)
|
|
107
|
+
# watch() on the root itself; recursive covers subfolders.
|
|
108
|
+
# Windows/ReadDirectoryChangesW delivers create/delete/
|
|
109
|
+
# rename/modify for everything below root.
|
|
110
|
+
observer.schedule(handler, root, recursive=True)
|
|
111
|
+
observer.start()
|
|
112
|
+
self._observer = observer
|
|
113
|
+
return True
|
|
114
|
+
except Exception:
|
|
115
|
+
# Watchdog exists but the platform refused the watch (rare
|
|
116
|
+
# — e.g. a drive gone mid-call): fall back to polling.
|
|
117
|
+
self._observer = None
|
|
118
|
+
return self._start_poller(root)
|
|
119
|
+
|
|
120
|
+
def _start_poller(self, root):
|
|
121
|
+
self._poll_stop.clear()
|
|
122
|
+
try:
|
|
123
|
+
self._snapshot = self._scan(root)
|
|
124
|
+
except Exception:
|
|
125
|
+
self._snapshot = {}
|
|
126
|
+
thread = threading.Thread(
|
|
127
|
+
target=self._poll_loop, args=(root,),
|
|
128
|
+
name="cat-fs-poll", daemon=True)
|
|
129
|
+
thread.start()
|
|
130
|
+
self._poll_thread = thread
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------- stop --
|
|
134
|
+
def stop(self):
|
|
135
|
+
"""Fully tears down whichever mechanism is running. Safe to call
|
|
136
|
+
twice / when never started."""
|
|
137
|
+
with self._lock:
|
|
138
|
+
self._root = None
|
|
139
|
+
self._cancel_debounce_locked()
|
|
140
|
+
observer = self._observer
|
|
141
|
+
self._observer = None
|
|
142
|
+
if observer is not None:
|
|
143
|
+
try:
|
|
144
|
+
observer.stop()
|
|
145
|
+
observer.join(timeout=2.0)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
self._poll_stop.set()
|
|
149
|
+
thread = self._poll_thread
|
|
150
|
+
self._poll_thread = None
|
|
151
|
+
if thread is not None and thread.is_alive():
|
|
152
|
+
thread.join(timeout=2.0)
|
|
153
|
+
with self._lock:
|
|
154
|
+
self._pending.clear()
|
|
155
|
+
self._snapshot = {}
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def root(self):
|
|
159
|
+
return self._root
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def active(self):
|
|
163
|
+
return self._observer is not None or self._poll_thread is not None
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def event_based(self):
|
|
167
|
+
"""True when backed by real OS events (watchdog); False when the
|
|
168
|
+
throttled poll fallback is in charge."""
|
|
169
|
+
return self._observer is not None
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------- event collection --
|
|
172
|
+
def report(self, path):
|
|
173
|
+
"""Thread-safe event intake. Called by the watchdog handler for
|
|
174
|
+
every raw filesystem event (and usable directly for explicit
|
|
175
|
+
internal TOOL_FILE_* notifications). Bursts are coalesced and
|
|
176
|
+
flushed once the stream goes quiet for DEBOUNCE seconds."""
|
|
177
|
+
if path:
|
|
178
|
+
with self._lock:
|
|
179
|
+
if self._root is None:
|
|
180
|
+
return
|
|
181
|
+
self._pending.add(os.path.abspath(path))
|
|
182
|
+
self._cancel_debounce_locked()
|
|
183
|
+
timer = threading.Timer(self.DEBOUNCE, self._flush)
|
|
184
|
+
timer.daemon = True
|
|
185
|
+
timer.start()
|
|
186
|
+
self._debounce_timer = timer
|
|
187
|
+
|
|
188
|
+
def _cancel_debounce_locked(self):
|
|
189
|
+
timer = self._debounce_timer
|
|
190
|
+
self._debounce_timer = None
|
|
191
|
+
if timer is not None:
|
|
192
|
+
timer.cancel()
|
|
193
|
+
|
|
194
|
+
def _flush(self):
|
|
195
|
+
with self._lock:
|
|
196
|
+
batch = self._pending
|
|
197
|
+
self._pending = set()
|
|
198
|
+
if batch and self._callback is not None:
|
|
199
|
+
try:
|
|
200
|
+
self._callback(batch)
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
# ---------------------------------------------------- poll fallback --
|
|
205
|
+
def _scan(self, root):
|
|
206
|
+
"""One shallow-everything snapshot: {path: mtime}. Directories
|
|
207
|
+
get st_mtime too (Windows keeps dir mtimes fresh on content
|
|
208
|
+
churn), so folder create/delete shows up as a diff without any
|
|
209
|
+
special handling. Symlinks are never followed."""
|
|
210
|
+
snap = {}
|
|
211
|
+
stack = [root]
|
|
212
|
+
while stack:
|
|
213
|
+
current = stack.pop()
|
|
214
|
+
try:
|
|
215
|
+
snap[current] = os.stat(current).st_mtime
|
|
216
|
+
with os.scandir(current) as entries:
|
|
217
|
+
for entry in entries:
|
|
218
|
+
try:
|
|
219
|
+
if entry.is_symlink():
|
|
220
|
+
continue
|
|
221
|
+
if entry.is_dir(follow_symlinks=False):
|
|
222
|
+
stack.append(entry.path)
|
|
223
|
+
else:
|
|
224
|
+
snap[entry.path] = entry.stat().st_mtime
|
|
225
|
+
except OSError:
|
|
226
|
+
continue
|
|
227
|
+
except (OSError, PermissionError):
|
|
228
|
+
continue
|
|
229
|
+
return snap
|
|
230
|
+
|
|
231
|
+
def _poll_loop(self, root):
|
|
232
|
+
while not self._poll_stop.wait(self.POLL_INTERVAL):
|
|
233
|
+
if self._root != root:
|
|
234
|
+
return
|
|
235
|
+
try:
|
|
236
|
+
fresh = self._scan(root)
|
|
237
|
+
except Exception:
|
|
238
|
+
continue
|
|
239
|
+
old = self._snapshot
|
|
240
|
+
changed = {p for p in fresh.keys() ^ old.keys()}
|
|
241
|
+
for p in fresh.keys() & old.keys():
|
|
242
|
+
if fresh[p] != old[p]:
|
|
243
|
+
changed.add(p)
|
|
244
|
+
if changed:
|
|
245
|
+
self._snapshot = fresh
|
|
246
|
+
for p in list(changed)[:64]: # cap the burst size
|
|
247
|
+
self.report(p)
|
|
248
|
+
else:
|
|
249
|
+
# Keep mtimes fresh even when nothing changed, so the
|
|
250
|
+
# next comparison stays cheap.
|
|
251
|
+
self._snapshot = fresh
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class _Handler(_WdHandler):
|
|
255
|
+
"""Watchdog adapter: forwards every interesting event kind into the
|
|
256
|
+
watcher's debounced intake."""
|
|
257
|
+
|
|
258
|
+
def __init__(self, watcher):
|
|
259
|
+
super().__init__()
|
|
260
|
+
self._watcher = watcher
|
|
261
|
+
|
|
262
|
+
def on_created(self, event):
|
|
263
|
+
self._emit(event.dest_path or event.src_path)
|
|
264
|
+
|
|
265
|
+
def on_deleted(self, event):
|
|
266
|
+
self._emit(event.src_path)
|
|
267
|
+
|
|
268
|
+
def on_modified(self, event):
|
|
269
|
+
self._emit(event.src_path)
|
|
270
|
+
|
|
271
|
+
def on_moved(self, event):
|
|
272
|
+
self._emit(event.src_path)
|
|
273
|
+
self._emit(event.dest_path)
|
|
274
|
+
|
|
275
|
+
def _emit(self, path):
|
|
276
|
+
if path:
|
|
277
|
+
self._watcher.report(path)
|
calc_terminal/game.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Element Quiz — a small multiple-choice game bolted onto the same
|
|
3
|
+
premium panel/gradient rendering as the rest of Chemistry Calc
|
|
4
|
+
Terminal, so learning periodic-table facts feels like part of the
|
|
5
|
+
same app instead of a bolted-on mini-game.
|
|
6
|
+
|
|
7
|
+
Question types are generated from the same ELEMENTS table atomsim.py
|
|
8
|
+
already uses for the live Bohr simulation (Z, symbol, name, shells,
|
|
9
|
+
mass), so there's exactly one source of truth for element data.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import random
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from . import theme
|
|
16
|
+
from . import sound
|
|
17
|
+
from .atomsim import ELEMENTS
|
|
18
|
+
from .engine import WIDTH
|
|
19
|
+
LETTERS = ["A", "B", "C", "D"]
|
|
20
|
+
START_LIVES = 3
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _shells_str(shells):
|
|
24
|
+
return "-".join(str(s) for s in shells)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _distractor_pool(exclude_z):
|
|
28
|
+
return [z for z in ELEMENTS if z != exclude_z]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _make_options(correct, pool_values, n=4):
|
|
32
|
+
others = random.sample([v for v in pool_values if v != correct], min(n - 1, len(pool_values) - 1))
|
|
33
|
+
options = others + [correct]
|
|
34
|
+
random.shuffle(options)
|
|
35
|
+
return options
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def make_question(difficulty=1):
|
|
39
|
+
"""Build one multiple-choice question dict:
|
|
40
|
+
{prompt, options: [...], correct_index, explain}"""
|
|
41
|
+
z = random.choice(list(ELEMENTS.keys()))
|
|
42
|
+
sym, name, shells, mass = ELEMENTS[z]
|
|
43
|
+
qtype = random.choice([
|
|
44
|
+
"symbol_from_name", "name_from_symbol", "z_from_name",
|
|
45
|
+
"name_from_z", "shells_from_name", "mass_from_name",
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
if qtype == "symbol_from_name":
|
|
49
|
+
prompt = f"What is the chemical symbol for {theme.text(name, bold=True)}?"
|
|
50
|
+
correct = sym
|
|
51
|
+
pool = [ELEMENTS[k][0] for k in ELEMENTS]
|
|
52
|
+
explain = f"{name} \u2192 {sym} (Z={z})"
|
|
53
|
+
elif qtype == "name_from_symbol":
|
|
54
|
+
prompt = f"Which element has the symbol {theme.cyan(sym, bold=True)}?"
|
|
55
|
+
correct = name
|
|
56
|
+
pool = [ELEMENTS[k][1] for k in ELEMENTS]
|
|
57
|
+
explain = f"{sym} \u2192 {name} (Z={z})"
|
|
58
|
+
elif qtype == "z_from_name":
|
|
59
|
+
prompt = f"What is the atomic number of {theme.text(name, bold=True)}?"
|
|
60
|
+
correct = str(z)
|
|
61
|
+
pool = [str(k) for k in ELEMENTS]
|
|
62
|
+
explain = f"{name} has {z} protons \u2192 Z = {z}"
|
|
63
|
+
elif qtype == "name_from_z":
|
|
64
|
+
prompt = f"Which element has atomic number {theme.orange(str(z), bold=True)}?"
|
|
65
|
+
correct = name
|
|
66
|
+
pool = [ELEMENTS[k][1] for k in ELEMENTS]
|
|
67
|
+
explain = f"Z={z} \u2192 {name} ({sym})"
|
|
68
|
+
elif qtype == "shells_from_name":
|
|
69
|
+
prompt = f"What is the electron shell structure (K-L-M...) of {theme.text(name, bold=True)}?"
|
|
70
|
+
correct = _shells_str(shells)
|
|
71
|
+
pool_z = random.sample(list(ELEMENTS.keys()), min(12, len(ELEMENTS)))
|
|
72
|
+
pool = [_shells_str(ELEMENTS[k][2]) for k in pool_z]
|
|
73
|
+
explain = f"{name} ({sym}, Z={z}) \u2192 {_shells_str(shells)}"
|
|
74
|
+
else: # mass_from_name
|
|
75
|
+
prompt = f"What is the approximate atomic mass of {theme.text(name, bold=True)}?"
|
|
76
|
+
correct = str(mass)
|
|
77
|
+
pool = [str(ELEMENTS[k][3]) for k in ELEMENTS]
|
|
78
|
+
explain = f"{name} ({sym}) \u2248 {mass} u"
|
|
79
|
+
|
|
80
|
+
options = _make_options(correct, pool)
|
|
81
|
+
if correct not in options:
|
|
82
|
+
options[-1] = correct
|
|
83
|
+
random.shuffle(options)
|
|
84
|
+
correct_index = options.index(correct)
|
|
85
|
+
return {"prompt": prompt, "options": options, "correct_index": correct_index, "explain": explain}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _hearts(lives):
|
|
89
|
+
return theme.red("\u2665 " * lives, bold=True) + theme.faint("\u2661 " * (START_LIVES - lives))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _streak_badge(streak):
|
|
93
|
+
if streak >= 8:
|
|
94
|
+
return theme.gradient(f"\U0001f525 x{streak} ON FIRE", theme.ORANGE, theme.RED, bold=True)
|
|
95
|
+
if streak >= 4:
|
|
96
|
+
return theme.orange(f"\U0001f525 x{streak} streak", bold=True)
|
|
97
|
+
if streak >= 1:
|
|
98
|
+
return theme.dim(f"streak x{streak}")
|
|
99
|
+
return theme.faint("streak x0")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _grade(score):
|
|
103
|
+
if score >= 900:
|
|
104
|
+
return "S", theme.gradient("S \u2014 PERIODIC LEGEND", theme.CYAN, theme.PURPLE, bold=True)
|
|
105
|
+
if score >= 600:
|
|
106
|
+
return "A", theme.green("A \u2014 Sharp chemist", bold=True)
|
|
107
|
+
if score >= 350:
|
|
108
|
+
return "B", theme.cyan("B \u2014 Solid grasp", bold=True)
|
|
109
|
+
if score >= 150:
|
|
110
|
+
return "C", theme.orange("C \u2014 Getting there", bold=True)
|
|
111
|
+
return "D", theme.faint("D \u2014 Back to the formula library")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def run_game(app):
|
|
115
|
+
theme.clear_screen()
|
|
116
|
+
print()
|
|
117
|
+
print(theme.panel([
|
|
118
|
+
theme.gradient("\u2697 ELEMENT QUIZ \u2697", theme.CYAN, theme.PURPLE, bold=True),
|
|
119
|
+
"",
|
|
120
|
+
theme.dim("Multiple choice \u2014 symbols, names, atomic numbers, shells & mass."),
|
|
121
|
+
theme.dim(f"You have {START_LIVES} lives. Answer fast for a speed bonus."),
|
|
122
|
+
theme.dim("Type a letter (A-D) to answer, or /quit to stop anytime."),
|
|
123
|
+
], title="game", color=theme.PURPLE, width=WIDTH, title_gradient=(theme.CYAN, theme.PURPLE)))
|
|
124
|
+
|
|
125
|
+
score = 0
|
|
126
|
+
streak = 0
|
|
127
|
+
best_streak_this_run = 0
|
|
128
|
+
lives = START_LIVES
|
|
129
|
+
qn = 0
|
|
130
|
+
|
|
131
|
+
while lives > 0:
|
|
132
|
+
qn += 1
|
|
133
|
+
q = make_question()
|
|
134
|
+
print()
|
|
135
|
+
header = (theme.faint(f"Q{qn}") + " " + _hearts(lives) + " " +
|
|
136
|
+
theme.dim(f"score {score}") + " " + _streak_badge(streak))
|
|
137
|
+
print(" " + header)
|
|
138
|
+
print(theme.panel(
|
|
139
|
+
[q["prompt"], ""] + [f" {theme.cyan(LETTERS[i])}. {opt}" for i, opt in enumerate(q["options"])],
|
|
140
|
+
title=f"question {qn}", color=theme.CYAN, width=WIDTH,
|
|
141
|
+
))
|
|
142
|
+
start = time.time()
|
|
143
|
+
try:
|
|
144
|
+
raw = input(theme.dim(" answer \u25b8 ")).strip()
|
|
145
|
+
except (EOFError, KeyboardInterrupt):
|
|
146
|
+
print()
|
|
147
|
+
break
|
|
148
|
+
elapsed = time.time() - start
|
|
149
|
+
low = raw.strip().lower()
|
|
150
|
+
if low in ("/quit", "/back", "/exit", "quit"):
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
pick = None
|
|
154
|
+
if low and low[0].upper() in LETTERS:
|
|
155
|
+
pick = LETTERS.index(low[0].upper())
|
|
156
|
+
|
|
157
|
+
if pick == q["correct_index"]:
|
|
158
|
+
speed_bonus = 40 if elapsed < 3 else (20 if elapsed < 6 else 0)
|
|
159
|
+
base = 60
|
|
160
|
+
streak += 1
|
|
161
|
+
best_streak_this_run = max(best_streak_this_run, streak)
|
|
162
|
+
gained = base + speed_bonus + streak * 5
|
|
163
|
+
score += gained
|
|
164
|
+
sound.play("success")
|
|
165
|
+
tag = " \u26a1 speed bonus!" if speed_bonus == 40 else ""
|
|
166
|
+
print(" " + theme.green(f"\u2713 Correct! +{gained}{tag}", bold=True))
|
|
167
|
+
print(" " + theme.dim(q["explain"]))
|
|
168
|
+
else:
|
|
169
|
+
lives -= 1
|
|
170
|
+
streak = 0
|
|
171
|
+
sound.play("error")
|
|
172
|
+
correct_letter = LETTERS[q["correct_index"]]
|
|
173
|
+
print(" " + theme.red(f"\u2717 Not quite \u2014 correct answer was {correct_letter}.", bold=True))
|
|
174
|
+
print(" " + theme.dim(q["explain"]))
|
|
175
|
+
if lives == 0:
|
|
176
|
+
print(" " + theme.orange("Out of lives!", bold=True))
|
|
177
|
+
|
|
178
|
+
app.game_high_score = max(app.game_high_score, score)
|
|
179
|
+
app.game_best_streak = max(app.game_best_streak, best_streak_this_run)
|
|
180
|
+
_, grade_line = _grade(score)
|
|
181
|
+
|
|
182
|
+
print()
|
|
183
|
+
print(theme.panel([
|
|
184
|
+
theme.gradient("ROUND OVER", theme.PURPLE, theme.CYAN, bold=True),
|
|
185
|
+
"",
|
|
186
|
+
theme.text(f"Final score: {score}", bold=True),
|
|
187
|
+
theme.dim(f"Best streak this round: {best_streak_this_run}"),
|
|
188
|
+
theme.dim(f"Session high score: {app.game_high_score} \u00b7 Session best streak: {app.game_best_streak}"),
|
|
189
|
+
"",
|
|
190
|
+
grade_line,
|
|
191
|
+
], title="results", color=theme.PURPLE, width=WIDTH, title_gradient=(theme.PURPLE, theme.CYAN)))
|
|
192
|
+
print()
|
|
193
|
+
input(theme.faint(" Press Enter to return home\u2026"))
|