wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ExerciseEngine.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <QDebug>
7
8#include "App/Core/Settings.h"
11#include "App/Scene/Scene.h"
13#include "App/Wiring/Port.h"
14
15namespace {
16
17void fillExerciseStepFields(const QString &id, const QJsonObject &stepObj, ExerciseStep &step)
18{
19 const QString stepKey = stepObj.value("key").toString();
20 const QString rawInstruction = stepObj.value("instruction").toString();
21 const QString rawHint = stepObj.value("hint").toString();
22
23 if (stepKey.isEmpty()) {
24 step.instruction = rawInstruction;
25 step.hint = rawHint;
26 } else {
27 const QString keyPrefix = id + QLatin1Char('.') + stepKey + QLatin1Char('.');
28 step.instruction = ExerciseTourResources::translate(keyPrefix + QStringLiteral("instruction"), rawInstruction);
29 step.hint = ExerciseTourResources::translate(keyPrefix + QStringLiteral("hint"), rawHint);
30 }
31
32 for (const QJsonValue &elemVal : stepObj.value("requiredElements").toArray()) {
33 const QJsonObject elemObj = elemVal.toObject();
35 req.typeName = elemObj.value("type").toString();
36 req.minCount = qMax(0, elemObj.value("minCount").toInt(1));
37 step.requiredElements.append(req);
38 }
39
40 for (const QJsonValue &connVal : stepObj.value("requiredConnections").toArray()) {
41 const QJsonObject connObj = connVal.toObject();
43 req.fromTypeName = connObj.value("fromType").toString();
44 req.toTypeName = connObj.value("toType").toString();
45 req.minCount = qMax(0, connObj.value("minCount").toInt(1));
46 step.requiredConnections.append(req);
47 }
48
49 step.click = stepObj.value("click").toVariant().toStringList();
50 step.context = stepObj.value("context").toString();
51}
52
53} // namespace
54
56 : QObject(parent)
57 , m_core(&fillExerciseStepFields,
60{
61}
62
63bool ExerciseEngine::loadFromResource(const QString &resourcePath)
64{
65 return m_core.loadFromResource(resourcePath);
66}
67
69{
70 if (m_scene) {
71 disconnect(m_scene, &Scene::circuitHasChanged, this, &ExerciseEngine::onCircuitChanged);
72 }
73 m_scene = scene;
74 if (m_scene && m_core.isActive()) {
75 connect(m_scene, &Scene::circuitHasChanged, this, &ExerciseEngine::onCircuitChanged);
76 }
77}
78
80{
81 return m_core.currentStepData();
82}
83
85{
86 if (!m_core.start()) {
87 return;
88 }
89 if (m_scene) {
90 connect(m_scene, &Scene::circuitHasChanged, this, &ExerciseEngine::onCircuitChanged);
91 }
92 emitCurrentStep();
93}
94
96{
97 if (!m_core.stop()) {
98 return;
99 }
100 if (m_scene) {
101 disconnect(m_scene, &Scene::circuitHasChanged, this, &ExerciseEngine::onCircuitChanged);
102 }
103 emit exerciseStopped();
104}
105
107{
108 if (m_core.goToPreviousStep()) {
109 emitCurrentStep();
110 }
111}
112
113void ExerciseEngine::performAdvance()
114{
115 const auto result = m_core.advance();
116 if (result.reachedEnd) {
117 markCompleted();
118 return;
119 }
120 if (result.advanced) {
121 emit stepCompleted(result.completedStep);
122 emitCurrentStep();
123 }
124}
125
127{
128 performAdvance();
129}
130
132{
133 if (m_core.retranslate()) {
134 emit retranslated();
135 }
136}
137
138void ExerciseEngine::onCircuitChanged()
139{
140 if (!validateCurrentStep()) {
141 return;
142 }
143 performAdvance();
144}
145
146bool ExerciseEngine::validateCurrentStep() const
147{
148 if (!m_core.isActive() || m_core.totalSteps() == 0 || !m_scene) {
149 return false;
150 }
151 const ExerciseStep &step = m_core.currentStepData();
152
153 // Observe steps (both vectors empty) never auto-advance.
154 if (step.requiredElements.isEmpty() && step.requiredConnections.isEmpty()) {
155 return false;
156 }
157
158 return validateElements(step.requiredElements)
159 && validateConnections(step.requiredConnections);
160}
161
162bool ExerciseEngine::validateElements(const QVector<ExerciseElementRequirement> &reqs) const
163{
164 if (reqs.isEmpty()) {
165 return true;
166 }
167
168 const QVector<GraphicElement *> elements = m_scene->elements();
169
170 for (const ExerciseElementRequirement &req : reqs) {
171 const ElementType required = ElementFactory::textToType(req.typeName);
172 if (required == ElementType::Unknown) {
173 qWarning() << "ExerciseEngine::validateElements: unknown element type" << req.typeName
174 << "in exercise" << m_core.id() << "-- this step can never be satisfied";
175 return false;
176 }
177 int count = 0;
178 for (const GraphicElement *elm : elements) {
179 if (elm->elementType() == required) {
180 ++count;
181 }
182 }
183 if (count < req.minCount) {
184 return false;
185 }
186 }
187 return true;
188}
189
190bool ExerciseEngine::validateConnections(const QVector<ExerciseConnectionRequirement> &reqs) const
191{
192 if (reqs.isEmpty()) {
193 return true;
194 }
195
196 const QVector<GraphicElement *> elements = m_scene->elements();
197
198 for (const ExerciseConnectionRequirement &req : reqs) {
201 if (toType == ElementType::Unknown || fromType == ElementType::Unknown) {
202 qWarning() << "ExerciseEngine::validateConnections: unknown element type in"
203 << (toType == ElementType::Unknown ? req.toTypeName : req.fromTypeName)
204 << "in exercise" << m_core.id() << "-- this step can never be satisfied";
205 return false;
206 }
207
208 int count = 0;
209 for (const GraphicElement *elm : elements) {
210 if (elm->elementType() != toType) {
211 continue;
212 }
213 for (const InputPort *port : elm->inputs()) {
214 for (const Connection *conn : port->connections()) {
215 if (conn->startPort()
216 && conn->startPort()->graphicElement()
217 && conn->startPort()->graphicElement()->elementType() == fromType) {
218 ++count;
219 }
220 }
221 }
222 }
223 if (count < req.minCount) {
224 return false;
225 }
226 }
227 return true;
228}
229
230void ExerciseEngine::emitCurrentStep()
231{
232 emit stepChanged(m_core.currentStep(), m_core.totalSteps(), currentStepData());
233}
234
235void ExerciseEngine::markCompleted()
236{
237 m_core.markCompleted();
238 if (m_scene) {
239 disconnect(m_scene, &Scene::circuitHasChanged, this, &ExerciseEngine::onCircuitChanged);
240 }
241 emit exerciseCompleted();
242}
Connection: a wire that connects an output port to an input port in the circuit scene.
Singleton factory for all circuit element types.
Enums::ElementType ElementType
Definition Enums.h:107
Abstract base class for all graphical circuit elements.
Port classes: Port (base), InputPort, and OutputPort.
Main circuit editing scene with undo/redo and user interaction.
Typed wrappers around QSettings for all application preferences.
static ElementType textToType(const QString &text)
Converts an element type name string to its ElementType enum value.
void retranslated()
void setScene(Scene *scene)
bool loadFromResource(const QString &resourcePath)
void advanceStep()
Manually advances one step (used by the overlay Next/Finish button for observe steps).
void stepChanged(int step, int total, const ExerciseStep &data)
void stepCompleted(int step)
void exerciseCompleted()
const ExerciseStep & currentStepData() const
Returns the data for the current step. Caller must check isActive() first.
ExerciseEngine(QObject *parent=nullptr)
void exerciseStopped()
static QString translate(const QString &key, const QString &fallbackEnglish)
Main circuit editing scene.
Definition Scene.h:56
void circuitHasChanged()
Emitted whenever the circuit changes (element added/removed/moved).
static int exerciseProgress(const QString &exerciseId)
Returns -1 if no record.
Definition Settings.cpp:242
static void setCompletedExercises(const QStringList &ids)
Definition Settings.cpp:237
static void setExerciseProgress(const QString &exerciseId, int step)
Definition Settings.cpp:248
static QStringList completedExercises()
Definition Settings.cpp:232
AdvanceResult advance()
QVector< ExerciseConnectionRequirement > requiredConnections
QVector< ExerciseElementRequirement > requiredElements
QStringList click
Widget/action IDs to activate on step enter.
QString instruction
QString context
"" or "circuit" → overlay on WorkSpace; "bwd" → overlay on BeWavedDolphin.