Эх сурвалжийг харах

Randomize anti-forget word order across review rounds

彭峰 2 долоо хоног өмнө
parent
commit
bcf682318d

+ 34 - 0
projects/xiaoshu-mobile/src/app/core/review-word-order.spec.ts

@@ -0,0 +1,34 @@
+import { nextReviewWordOrder } from './review-word-order';
+
+describe('review word order', () => {
+  it('uses ten distinct orders for the same four words without changing the source list', () => {
+    const words = ['apple', 'banana', 'cherry', 'date'];
+    let history: string[][] = [];
+    for (let round = 0; round < 10; round += 1) {
+      const result = nextReviewWordOrder(words, history, (word) => word, () => 0);
+      expect(result.words.slice().sort()).toEqual(words);
+      if (round === 0) expect(result.words).not.toEqual(words);
+      expect(history).not.toContain(result.words);
+      history = result.history;
+    }
+    expect(words).toEqual(['apple', 'banana', 'cherry', 'date']);
+  });
+
+  it('alternates two words after both possible orders have been used', () => {
+    let history: string[][] = [];
+    const orders: string[][] = [];
+    for (let round = 0; round < 4; round += 1) {
+      const result = nextReviewWordOrder(['a', 'b'], history, (word) => word, () => 0);
+      orders.push(result.words);
+      history = result.history;
+    }
+    expect(orders[0]).not.toEqual(orders[1]);
+    expect(orders[1]).not.toEqual(orders[2]);
+    expect(orders[2]).not.toEqual(orders[3]);
+  });
+
+  it('ignores saved orders from a different word set', () => {
+    const result = nextReviewWordOrder(['a', 'b'], [['a', 'c']], (word) => word, () => 0);
+    expect(result.history).toEqual([result.words]);
+  });
+});

+ 45 - 0
projects/xiaoshu-mobile/src/app/core/review-word-order.ts

@@ -0,0 +1,45 @@
+export interface ReviewWordOrder<T> {
+  words: T[];
+  history: string[][];
+}
+
+/** Pick an unused order for this word set while enough distinct permutations exist. */
+export function nextReviewWordOrder<T>(
+  words: T[],
+  previous: string[][],
+  idOf: (word: T) => string,
+  random: () => number = Math.random,
+): ReviewWordOrder<T> {
+  const ids = words.map(idOf);
+  const sortedIds = [...ids].sort();
+  const sameWords = (order: string[]) => order.length === ids.length
+    && [...order].sort().every((id, index) => id === sortedIds[index]);
+  const history = previous.filter(sameWords).slice(-10);
+  const used = new Set(history.map((order) => JSON.stringify(order)));
+  if (ids.length > 1 && !history.length) used.add(JSON.stringify(ids));
+  const indices = words.map((_, index) => index);
+  for (let index = indices.length - 1; index > 0; index -= 1) {
+    const target = Math.floor(random() * (index + 1));
+    [indices[index], indices[target]] = [indices[target], indices[index]];
+  }
+
+  const signature = () => indices.map((index) => ids[index]);
+  const advance = () => {
+    let pivot = indices.length - 2;
+    while (pivot >= 0 && indices[pivot] >= indices[pivot + 1]) pivot -= 1;
+    if (pivot < 0) { indices.reverse(); return; }
+    let swap = indices.length - 1;
+    while (indices[swap] <= indices[pivot]) swap -= 1;
+    [indices[pivot], indices[swap]] = [indices[swap], indices[pivot]];
+    indices.splice(pivot + 1, indices.length - pivot - 1, ...indices.slice(pivot + 1).reverse());
+  };
+
+  for (let attempt = 0; attempt <= used.size && used.has(JSON.stringify(signature())); attempt += 1) advance();
+  if (used.has(JSON.stringify(signature())) && history.length) {
+    // Fewer than eleven permutations exist: still change the order from the last entry.
+    const last = JSON.stringify(history[history.length - 1]);
+    for (let attempt = 0; attempt < Math.max(1, history.length) && JSON.stringify(signature()) === last; attempt += 1) advance();
+  }
+  const order = signature();
+  return { words: indices.map((index) => words[index]), history: [...history, order].slice(-10) };
+}

+ 3 - 1
projects/xiaoshu-mobile/src/app/features/learning/learning-page.component.ts

@@ -1557,7 +1557,9 @@ export class LearningPageComponent implements OnInit, OnDestroy {
     this.api.post<unknown>('e_record_detail', { id: this.query('id') }).pipe(finalize(() => this.loading.set(false))).subscribe({
       next: ({ result, addon }) => {
         const words = this.findList(result).map((item) => this.toWord(item)).filter((word) => word.word);
-        const key = `xiaoshu.review.order.${this.session.student()?.userId ?? this.session.user()?.userId}.${this.query('id')}`;
+        const user = this.session.user();
+        const learnerId = Number(user?.groupId) === 3 ? this.session.student()?.userId : user?.userId;
+        const key = `xiaoshu.review.order.${learnerId}.${this.query('id')}`;
         let previous: string[][] = [];
         try {
           const stored: unknown = JSON.parse(localStorage.getItem(key) ?? '[]');