-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathengine.py
More file actions
324 lines (264 loc) · 12.2 KB
/
engine.py
File metadata and controls
324 lines (264 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
import json
import os
import time
import dataclasses
from typing import Optional, List, Dict, Any, Union
import config
# --- EVENTS / ACTIONS ---
@dataclasses.dataclass
class ActionRenderEditor:
"""UI should render the code editor."""
task_info: str
hint_text: str
initial_code: str
task_status: str # 'pending', 'completed', 'skipped'
completed_count: int
skipped_count: int
# Optional fields for future use or internal tracking
task_id: int = 0
@dataclasses.dataclass
class ActionRenderCelebration:
"""UI should render the celebration screen."""
completed_count: int
skipped_count: int
has_skipped: bool
@dataclasses.dataclass
class ActionShowMessage:
"""UI should suspend curses and show a message."""
title: str
content: str
type: str # 'success', 'error', 'info', 'solution', 'reset'
wait_for_enter: bool = True
clear_screen: bool = True
@dataclasses.dataclass
class ActionExit:
"""UI should exit the application."""
exit_code: int = 0
@dataclasses.dataclass
class ActionCustomView:
view_name: str
# --- DATA HELPERS ---
def get_default_progress():
return {
"current_step": None, # Now stores UUID (str) or None (for first start)
"completed_tasks": [], # List of UUIDs
"skipped_tasks": [], # List of UUIDs
"user_code": {}, # Map UUID -> Code
}
def validate_progress_data(data):
"""Ensures progress data has the correct schema, migrating if necessary."""
default = get_default_progress()
if not isinstance(data, dict): return default
# 1. Ensure keys exist
for key, value in default.items():
if key not in data:
data[key] = value
# 2. Schema Migration (completed -> completed_tasks)
if "completed" in data and "completed_tasks" not in data:
data["completed_tasks"] = data.pop("completed")
if "skipped" in data and "skipped_tasks" not in data:
data["skipped_tasks"] = data.pop("skipped")
# 3. Type checks
if not isinstance(data.get("completed_tasks"), list): data["completed_tasks"] = []
if not isinstance(data.get("skipped_tasks"), list): data["skipped_tasks"] = []
if not isinstance(data.get("user_code"), dict): data["user_code"] = {}
return data
# --- SIMULATION ENGINE ---
class SimulationEngine:
def __init__(self):
# Define base directory
self.base_dir = os.path.dirname(os.path.abspath(__file__))
# User data stored in platform-specific location
from config import get_user_data_dir
user_data_dir = get_user_data_dir()
self.progress_file = os.path.join(user_data_dir, 'progress.json')
self.progress_backup = os.path.join(user_data_dir, 'progress.backup.json')
# Initialize Curriculum Manager
from curriculum_manager import CurriculumManager
self.cm = CurriculumManager(os.path.join(self.base_dir, 'curriculum'))
self.cm.load()
self.progress = self._load_progress()
self.last_run_result = None
def _load_progress(self) -> Dict:
data = get_default_progress()
if os.path.exists(self.progress_file):
try:
with open(self.progress_file, 'r', encoding='utf-8') as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
if os.path.exists(self.progress_backup):
try:
with open(self.progress_backup, 'r', encoding='utf-8') as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError): pass
return validate_progress_data(data)
def _save_progress(self):
# Backup
if os.path.exists(self.progress_file):
try:
import shutil
shutil.copy2(self.progress_file, self.progress_backup)
except OSError: pass
try:
with open(self.progress_file, 'w', encoding='utf-8') as f:
json.dump(self.progress, f, indent=4, ensure_ascii=False)
except Exception as e:
pass
def _get_current_state_info(self):
current_step_id = self.progress.get("current_step")
completed = self.progress.get("completed_tasks", [])
skipped = self.progress.get("skipped_tasks", [])
# If new profile or migration reset, default to first lesson
step = None
if current_step_id:
step = self.cm.get_lesson_by_uuid(current_step_id)
if not step:
# Only fallback to first lesson for NEW users (no history)
is_new_user = len(completed) == 0 and len(skipped) == 0
if is_new_user:
step = self.cm.get_first_lesson()
if step:
current_step_id = step.uuid
self.progress["current_step"] = current_step_id
# Don't save yet, wait for interaction
return self.progress, current_step_id, completed, skipped, step
def get_next_action(self) -> Union[ActionRenderEditor, ActionRenderCelebration, ActionExit, ActionShowMessage]:
progress, current_step_id, completed, skipped, step = self._get_current_state_info()
if not step:
# Celebration Screen
has_skipped = len(skipped) > 0
return ActionRenderCelebration(
completed_count=len(completed),
skipped_count=len(skipped),
has_skipped=has_skipped
)
# Determine status
is_completed = current_step_id in completed
is_skipped = current_step_id in skipped
task_status = "completed" if is_completed else ("skipped" if is_skipped else "pending")
# Build Task Info String (Legacy Compat)
status_badge = config.UI.BADGE_SUCCESS if is_completed else (config.UI.BADGE_SKIPPED if is_skipped else "")
task_info = f"{config.UI.LABEL_SECTION} {step.category}\n"
task_info += f"{config.UI.LABEL_TASK} {step.numeric_id}: {step.title}{status_badge}\n"
task_info += f"\n{config.UI.LABEL_QUESTION} {step.description}"
saved_code = progress.get("user_code", {}).get(str(current_step_id), "")
return ActionRenderEditor(
task_info=task_info,
hint_text=step.hint,
initial_code=saved_code,
task_status=task_status,
completed_count=len(completed),
skipped_count=len(skipped),
task_id=current_step_id
)
def process_input(self, user_input: Optional[str]) -> Any:
progress, current_step_id, completed, skipped, step = self._get_current_state_info()
# --- COMMAND HANDLING ---
# 1. RESET
if user_input == "RESET_ALL":
self.progress = get_default_progress()
self._save_progress()
return ActionShowMessage("İLERLEME SIFIRLANDI", "Tüm ilerleme silindi.", "reset", wait_for_enter=False)
# 2. DEV MESSAGE
if user_input == "DEV_MESSAGE":
return ActionCustomView("dev_message")
# 3. NAVIGATION (PREV/NEXT)
if user_input == "PREV_TASK":
if current_step_id is None:
# From celebration, go to last lesson
if self.cm.lessons:
self.progress["current_step"] = self.cm.lessons[-1].uuid
self._save_progress()
else:
prev_lesson = self.cm.get_prev_lesson(current_step_id)
if prev_lesson:
self.progress["current_step"] = prev_lesson.uuid
self._save_progress()
return self.get_next_action()
if user_input == "NEXT_TASK":
if current_step_id is None:
# Already on celebration, stay there
return self.get_next_action()
next_lesson = self.cm.get_next_lesson(current_step_id)
if next_lesson:
# Normal case: go to next lesson (if allowed)
can_advance = (current_step_id in completed or current_step_id in skipped)
if next_lesson.uuid in completed or next_lesson.uuid in skipped:
can_advance = True
if can_advance:
self.progress["current_step"] = next_lesson.uuid
self._save_progress()
else:
# No next lesson = we're on the last one
# If current is done/skipped, go to celebration
if current_step_id in completed or current_step_id in skipped:
self.progress["current_step"] = None
self._save_progress()
return self.get_next_action()
if user_input == "GOTO_FIRST_SKIPPED":
if skipped:
first_skipped_uuid = skipped[0]
if self.cm.get_lesson_by_uuid(first_skipped_uuid):
self.progress["current_step"] = first_skipped_uuid
self._save_progress()
return self.get_next_action()
if user_input == "SHOW_SOLUTION":
if step:
return ActionShowMessage("📖 ÇÖZÜM", f"\n{step.solution_code}\n", "solution")
# 4. SKIP (Double Enter defined as None in UI)
if user_input is None:
if not step:
return ActionExit()
is_skipped = current_step_id in skipped
msg_title = "📖 ÇÖZÜM (Daha önce atlanmış görev)" if is_skipped else "⏩ SORU ATLANDI"
# Mark as skipped if not already
if not is_skipped:
self.progress["skipped_tasks"].append(current_step_id)
# ALWAYS advance to next lesson or celebration
next_l = self.cm.get_next_lesson(current_step_id)
if next_l:
self.progress["current_step"] = next_l.uuid
else:
self.progress["current_step"] = None # Trigger celebration
self._save_progress()
return ActionShowMessage(
title=msg_title,
content=f"✅ Bu sorunun DOĞRU ÇÖZÜMÜ:\n\n{step.solution_code}\n",
type="info",
wait_for_enter=True
)
# --- CODE SUBMISSION HANDLING ---
# If we are here, user_input is Code String (and not a command like NEXT_TASK)
if step is None:
return ActionShowMessage("HATA", "Geçersiz görev.", "error")
# Save User Code
self.progress["user_code"][str(current_step_id)] = user_input
self._save_progress()
# Execute Code
from sandbox.executor import run_safe
validator_path = step.validator_script if step.validator_script and os.path.exists(step.validator_script) else None
result = run_safe(user_input, validator_path)
self.last_run_result = result
stdout_val = result["stdout"]
is_valid = result["is_valid"]
error_message = result["error_message"]
if is_valid:
if current_step_id not in completed:
self.progress["completed_tasks"].append(current_step_id)
if current_step_id in skipped:
self.progress["skipped_tasks"].remove(current_step_id)
next_l = self.cm.get_next_lesson(current_step_id)
if next_l:
self.progress["current_step"] = next_l.uuid
else:
self.progress["current_step"] = None # Trigger celebration
self._save_progress()
msg = "Görev başarıyla tamamlandı."
if stdout_val:
msg += f"\nÇıktı:\n{stdout_val}"
return ActionShowMessage("TEBRİKLER! DOĞRU CEVAP.", msg, "success", wait_for_enter=False)
else:
msg = error_message if error_message else "Sonuç beklendiği gibi değil."
if stdout_val:
msg += f"\nKod Çıktısı: {stdout_val}"
return ActionShowMessage("HATA VEYA YANLIŞ CEVAP", msg, "error")