icoa-cli 2.19.21 → 2.19.22

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.
@@ -51,25 +51,55 @@ export function registerLangCommand(program) {
51
51
  }
52
52
  saveConfig({ language: code });
53
53
  printSuccess(`Language set to: ${LANG_NAMES[code] || code}`);
54
- // If demo exam in progress, restart with fresh random pick in new language.
55
- // (Progress is lost because questions are randomly picked from a 30-question pool
56
- // and each attempt shuffles option positions mid-exam translation would
57
- // desync the answer key.)
54
+ // If demo in progress, re-translate each drawn question in place using
55
+ // sourceNumber + sourceOrder so the user's answers and option positions are
56
+ // preserved. If an older state lacks these fields (pre-v2.19.22), fall back
57
+ // to restart-with-fresh-pick so nothing crashes.
58
58
  const state = getExamState();
59
59
  if (state && state.session.examId === 'demo-free') {
60
60
  try {
61
- const { pickDemoQuestions, getLocalizedDemoSession, DEMO_PICK_SIZE } = await import('../lib/demo-exam.js');
62
- const freshQuestions = pickDemoQuestions(DEMO_PICK_SIZE);
63
- state.questions = freshQuestions;
64
- state.answers = {};
65
- state.session.examName = getLocalizedDemoSession().examName;
66
- state.session.startedAt = new Date().toISOString();
67
- state._lastQ = 1;
61
+ const { pickDemoQuestions, getLocalizedDemoSession, getLocalizedDemoQuestions, getLocalizedExplanations, DEMO_PICK_SIZE, } = await import('../lib/demo-exam.js');
68
62
  const { saveExamState } = await import('../lib/exam-state.js');
69
- saveExamState(state);
70
- console.log();
71
- console.log(chalk.green(` Demo restarted in ${LANG_NAMES[code] || code}.`));
72
- console.log(chalk.white(' Type: exam q 1'));
63
+ const canRetranslate = state.questions.every((q) => q.sourceNumber != null && Array.isArray(q.sourceOrder) && q.sourceOrder.length === 4);
64
+ if (canRetranslate) {
65
+ const pool = getLocalizedDemoQuestions();
66
+ const explanations = getLocalizedExplanations();
67
+ state.questions = state.questions.map((q) => {
68
+ const src = pool.find((p) => p.number === q.sourceNumber);
69
+ if (!src || !q.sourceOrder)
70
+ return q;
71
+ return {
72
+ ...q,
73
+ text: src.text,
74
+ category: src.category,
75
+ options: {
76
+ A: src.options[q.sourceOrder[0]],
77
+ B: src.options[q.sourceOrder[1]],
78
+ C: src.options[q.sourceOrder[2]],
79
+ D: src.options[q.sourceOrder[3]],
80
+ },
81
+ explanation: explanations[q.sourceNumber],
82
+ };
83
+ });
84
+ state.session.examName = getLocalizedDemoSession().examName;
85
+ saveExamState(state);
86
+ const currentQ = state._lastQ || 1;
87
+ console.log();
88
+ console.log(chalk.green(` Demo continues in ${LANG_NAMES[code] || code}. Your progress is kept.`));
89
+ console.log(chalk.white(` Resume: exam q ${currentQ}`));
90
+ }
91
+ else {
92
+ // Legacy state from before v2.19.22 — safely reset
93
+ state.questions = pickDemoQuestions(DEMO_PICK_SIZE);
94
+ state.answers = {};
95
+ state.session.examName = getLocalizedDemoSession().examName;
96
+ state.session.startedAt = new Date().toISOString();
97
+ state._lastQ = 1;
98
+ saveExamState(state);
99
+ console.log();
100
+ console.log(chalk.green(` Demo restarted in ${LANG_NAMES[code] || code}.`));
101
+ console.log(chalk.white(' Type: exam q 1'));
102
+ }
73
103
  }
74
104
  catch {
75
105
  console.log(chalk.gray(' Language changed. Type: demo'));
@@ -127,16 +127,24 @@ function shuffle(arr) {
127
127
  }
128
128
  return out;
129
129
  }
130
- /** Shuffle the four options within a single question; recompute the answer letter. */
130
+ /**
131
+ * Shuffle the four options within a single question; recompute the answer letter
132
+ * and record `sourceOrder` so the shuffle can be replayed against a different
133
+ * translation of the same question (used by lang-switch to keep answers).
134
+ */
131
135
  function shuffleQuestionOptions(q) {
132
136
  if (!q.answer)
133
137
  return q;
134
- const correctText = q.options[q.answer];
135
138
  const keys = ['A', 'B', 'C', 'D'];
136
- const values = shuffle(keys.map((k) => q.options[k]));
137
- const newOptions = { A: values[0], B: values[1], C: values[2], D: values[3] };
138
- const newAnswer = keys.find((k) => newOptions[k] === correctText);
139
- return { ...q, options: newOptions, answer: newAnswer };
139
+ const sourceOrder = shuffle(keys);
140
+ const newOptions = {
141
+ A: q.options[sourceOrder[0]],
142
+ B: q.options[sourceOrder[1]],
143
+ C: q.options[sourceOrder[2]],
144
+ D: q.options[sourceOrder[3]],
145
+ };
146
+ const newAnswer = keys[sourceOrder.indexOf(q.answer)];
147
+ return { ...q, options: newOptions, answer: newAnswer, sourceOrder };
140
148
  }
141
149
  /**
142
150
  * Pick `n` random questions from the pool, shuffle each question's options,
@@ -152,12 +160,14 @@ export function pickDemoQuestions(n = DEMO_PICK_SIZE) {
152
160
  answer: DEMO_ANSWERS[q.number],
153
161
  explanation: explanations[q.number],
154
162
  }));
155
- // Shuffle pool, pick n, shuffle each question's options, renumber 1..n
163
+ // Shuffle pool, pick n, shuffle each question's options, renumber 1..n.
164
+ // Keep the original pool number as `sourceNumber` so lang-switch can re-translate
165
+ // this exact question without losing the user's answer.
156
166
  const picked = shuffle(enriched).slice(0, n);
157
- return picked.map((q, i) => ({
158
- ...shuffleQuestionOptions(q),
159
- number: i + 1,
160
- }));
167
+ return picked.map((q, i) => {
168
+ const shuffled = shuffleQuestionOptions(q);
169
+ return { ...shuffled, sourceNumber: q.number, number: i + 1 };
170
+ });
161
171
  }
162
172
  /**
163
173
  * Get localized explanations for demo questions.
@@ -152,6 +152,8 @@ export interface ExamQuestion {
152
152
  };
153
153
  answer?: 'A' | 'B' | 'C' | 'D';
154
154
  explanation?: string;
155
+ sourceNumber?: number;
156
+ sourceOrder?: ('A' | 'B' | 'C' | 'D')[];
155
157
  }
156
158
  export interface ExamSession {
157
159
  examId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.21",
3
+ "version": "2.19.22",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {