Repository: st3v3nmw/obsidian-spaced-repetition
Pull request #820 (2023-12-25 09:13:29)

Support richer set of flashcard ordering during review
[FEAT] Support richer set of flashcard ordering during review; e.g. random card from random deck: #814 
[FEAT] Support flashcards ordering of new cards first: #610 


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index f89266c..cbdb968 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,6 +6,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
 
 #### [Unreleased]
 
+-   Feat: Support richer set of flashcard ordering during review; e.g. random card from random deck [`#814`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/814)
 -   Bug fix Problem with nested list item's indentation [`#800`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/800)
 -   Bug fix Problem with nested list item's indentation [`#812`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/812)
 -   Bug Cloze Breaks When }} Encountered [`#799`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/799)
diff --git a/src/Deck.ts b/src/Deck.ts
index fe42fb1..66f06d2 100644
--- a/src/Deck.ts
+++ b/src/Deck.ts
@@ -54,7 +54,6 @@ export class Deck {
     private getQuestionCardCountForCardListType(question: Question, cards: Card[]): number {
         let result: number = 0;
         for (let i = 0; i < cards.length; i++) {
-            const card = cards[i];
             if (Object.is(question, cards[i].question)) result++;
         }
         return result;
diff --git a/src/DeckTreeIterator.ts b/src/DeckTreeIterator.ts
index 0da88f7..4aef6f0 100644
--- a/src/DeckTreeIterator.ts
+++ b/src/DeckTreeIterator.ts
@@ -2,16 +2,18 @@ import { Card } from "./Card";
 import { CardListType, Deck } from "./Deck";
 import { Question } from "./Question";
 import { TopicPath } from "./TopicPath";
-import { globalRandomNumberProvider } from "./util/RandomNumberProvider";
-
-export enum CardListOrder {
-    NewFirst,
-    DueFirst,
-    Random,
+import { WeightedRandomNumber, globalRandomNumberProvider } from "./util/RandomNumberProvider";
+
+export enum CardOrder {
+    NewFirstSequential,
+    NewFirstRandom,
+    DueFirstSequential,
+    DueFirstRandom,
+    EveryCardRandomDeckAndCard,
 }
-export enum OrderMethod {
-    Sequential,
-    Random,
+export enum DeckOrder {
+    PrevDeckComplete_Sequential,
+    PrevDeckComplete_Random,
 }
 export enum IteratorDeckSource {
     UpdatedByIterator,
@@ -19,14 +21,12 @@ export enum IteratorDeckSource {
 }
 
 export interface IIteratorOrder {
-    // Choose decks in sequential order, or randomly
-    deckOrder: OrderMethod;
+    // Within a deck this specifies the order the cards should be reviewed
+    // e.g. new first, going sequentially
+    cardOrder: CardOrder;
 
-    // Within a deck, choose: new cards first, due cards first, or randomly
-    cardListOrder: CardListOrder;
-
-    // Within a card list (i.e. either new or due), choose cards sequentially or randomly
-    cardOrder: OrderMethod;
+    // Choose decks in sequential order, or randomly
+    deckOrder: DeckOrder;
 }
 
 export interface IDeckTreeIterator {
@@ -46,22 +46,24 @@ class SingleDeckIterator {
     preferredCardListType: CardListType;
     cardIdx?: number;
     cardListType?: CardListType;
+    weightedRandomNumber: WeightedRandomNumber;
 
     get hasCurrentCard(): boolean {
         return this.cardIdx != null;
     }
 
     get currentCard(): Card {
-        if (this.cardIdx == null) return null;
-        return this.deck.getCard(this.cardIdx, this.cardListType);
+        let result: Card = null;
+        if (this.cardIdx != null) result = this.deck.getCard(this.cardIdx, this.cardListType);
+        return result;
     }
 
     constructor(iteratorOrder: IIteratorOrder) {
         this.iteratorOrder = iteratorOrder;
-        this.preferredCardListType =
-            this.iteratorOrder.cardListOrder == CardListOrder.DueFirst
-                ? CardListType.DueCard
-                : CardListType.NewCard;
+        this.preferredCardListType = SingleDeckIterator.getCardListTypeForIterator(
+            this.iteratorOrder,
+        );
+        this.weightedRandomNumber = WeightedRandomNumber.create();
     }
 
     setDeck(deck: Deck): void {
@@ -69,47 +71,81 @@ class SingleDeckIterator {
         this.setCardListType(null);
     }
 
-    private setCardListType(cardListType?: CardListType): void {
+    //
+    // 0 <= cardIndex < newFlashcards.length + dueFlashcards.length
+    //
+    setNewOrDueCardIdx(cardIndex: number): void {
+        let cardListType: CardListType = CardListType.NewCard;
+        let index: number = cardIndex;
+        if (cardIndex >= this.deck.newFlashcards.length) {
+            cardListType = CardListType.DueCard;
+            index = cardIndex - this.deck.newFlashcards.length;
+        }
+        this.setCardListType(cardListType, index);
+    }
+
+    private setCardListType(cardListType?: CardListType, cardIdx: number = null): void {
         this.cardListType = cardListType;
-        this.cardIdx = null;
+        this.cardIdx = cardIdx;
     }
 
     nextCard(): boolean {
-        // First return cards in the preferred list
-        if (this.cardListType == null) {
-            this.setCardListType(this.preferredCardListType);
-        }
+        if (this.iteratorOrder.cardOrder == CardOrder.EveryCardRandomDeckAndCard) {
+            this.nextRandomCard();
+        } else {
+            // First return cards in the preferred list
+            if (this.cardListType == null) {
+                this.setCardListType(this.preferredCardListType);
+            }
 
-        if (!this.nextCardWithinList()) {
-            if (this.cardListType == this.preferredCardListType) {
-                // Nothing left in the preferred list, so try the non-preferred list type
-                this.setCardListType(Deck.otherListType(this.cardListType));
-                if (!this.nextCardWithinList()) {
-                    this.setCardListType(null);
+            if (!this.nextCardWithinCurrentList()) {
+                if (this.cardListType == this.preferredCardListType) {
+                    // Nothing left in the preferred list, so try the non-preferred list type
+                    this.setCardListType(Deck.otherListType(this.cardListType));
+                    if (!this.nextCardWithinCurrentList()) {
+                        this.setCardListType(null);
+                    }
+                } else {
+                    this.cardIdx = null;
                 }
-            } else {
-                this.cardIdx = null;
             }
         }
+
         return this.cardIdx != null;
     }
 
-    private nextCardWithinList(): boolean {
-        let result: boolean = false;
+    private nextRandomCard(): void {
+        const newCount: number = this.deck.newFlashcards.length;
+        const dueCount: number = this.deck.dueFlashcards.length;
+        if (newCount + dueCount > 0) {
+            // Generate a random number such that the probability of picking an individual card is the same
+            // regardless of whether the card is in the new/due list, or which list has more cards
+            // I.e. we don't pick the new/due list first at 50/50 and then a random card within it
+            const weights: Partial<Record<CardListType, number>> = {};
+            if (newCount > 0) weights[CardListType.NewCard] = newCount;
+            if (dueCount > 0) weights[CardListType.DueCard] = dueCount;
+            const [cardListType, index] = this.weightedRandomNumber.getRandomValues(weights);
+            this.setCardListType(cardListType, index);
+        } else {
+            this.setCardListType(null);
+        }
+    }
+
+    private nextCardWithinCurrentList(): boolean {
         const cardList: Card[] = this.deck.getCardListForCardType(this.cardListType);
 
-        // Delete the current card so we don't return it again
-        if (this.hasCurrentCard) {
-            this.deleteCurrentCard();
-        }
-        result = cardList.length > 0;
+        const result: boolean = cardList.length > 0;
         if (result) {
             switch (this.iteratorOrder.cardOrder) {
-                case OrderMethod.Sequential:
+                case CardOrder.DueFirstSequential:
+                case CardOrder.NewFirstSequential:
+                    // We always pick the card with index 0
+                    // Sequential retrieval occurs by the caller deleting the card at this index after it is used
                     this.cardIdx = 0;
                     break;
 
-                case OrderMethod.Random:
+                case CardOrder.DueFirstRandom:
+                case CardOrder.NewFirstRandom:
                     this.cardIdx = globalRandomNumberProvider.getInteger(0, cardList.length - 1);
                     break;
             }
@@ -158,17 +194,32 @@ class SingleDeckIterator {
     private ensureCurrentCard() {
         if (this.cardIdx == null || this.cardListType == null) throw "no current card";
     }
+
+    private static getCardListTypeForIterator(iteratorOrder: IIteratorOrder): CardListType | null {
+        let result: CardListType = null;
+        switch (iteratorOrder.cardOrder) {
+            case CardOrder.DueFirstRandom:
+            case CardOrder.DueFirstSequential:
+                result = CardListType.DueCard;
+                break;
+
+            case CardOrder.NewFirstRandom:
+            case CardOrder.NewFirstSequential:
+                result = CardListType.NewCard;
+                break;
+        }
+        return result;
+    }
 }
 
 export class DeckTreeIterator implements IDeckTreeIterator {
-    deckTree: Deck;
-    preferredCardListType: CardListType;
-    iteratorOrder: IIteratorOrder;
-    deckSource: IteratorDeckSource;
+    private iteratorOrder: IIteratorOrder;
+    private deckSource: IteratorDeckSource;
 
-    singleDeckIterator: SingleDeckIterator;
-    deckArray: Deck[];
-    deckIdx?: number;
+    private singleDeckIterator: SingleDeckIterator;
+    private deckArray: Deck[];
+    private deckIdx?: number;
+    private weightedRandomNumber: WeightedRandomNumber;
 
     get hasCurrentCard(): boolean {
         return this.deckIdx != null && this.singleDeckIterator.hasCurrentCard;
@@ -180,25 +231,39 @@ export class DeckTreeIterator implements IDeckTreeIterator {
     }
 
     get currentCard(): Card {
-        if (this.deckIdx == null || !this.singleDeckIterator.hasCurrentCard) return null;
-        return this.singleDeckIterator.currentCard;
+        let result: Card = null;
+        if (this.deckIdx != null && this.singleDeckIterator.hasCurrentCard)
+            result = this.singleDeckIterator.currentCard;
+        return result;
     }
 
     constructor(iteratorOrder: IIteratorOrder, deckSource: IteratorDeckSource) {
         this.singleDeckIterator = new SingleDeckIterator(iteratorOrder);
         this.iteratorOrder = iteratorOrder;
         this.deckSource = deckSource;
+        this.weightedRandomNumber = WeightedRandomNumber.create();
     }
 
     setDeck(deck: Deck): void {
         // We don't want to change the supplied deck, so first clone
         if (this.deckSource == IteratorDeckSource.CloneBeforeUse) deck = deck.clone();
 
-        this.deckTree = deck;
-        this.deckArray = deck.toDeckArray();
+        this.deckArray = DeckTreeIterator.filterForDecksWithCards(deck.toDeckArray());
         this.setDeckIdx(null);
     }
 
+    private static filterForDecksWithCards(sourceArray: Deck[]): Deck[] {
+        const result: Deck[] = [];
+        for (let idx = 0; idx < sourceArray.length; idx++) {
+            const deck: Deck = sourceArray[idx];
+            const hasAnyCards = deck.getCardCount(CardListType.All, false) > 0;
+            if (hasAnyCards) {
+                result.push(deck);
+            }
+        }
+        return result;
+    }
+
     private setDeckIdx(deckIdx?: number): void {
         this.deckIdx = deckIdx;
         if (deckIdx != null) this.singleDeckIterator.setDeck(this.deckArray[deckIdx]);
@@ -206,23 +271,80 @@ export class DeckTreeIterator implements IDeckTreeIterator {
 
     nextCard(): boolean {
         let result: boolean = false;
-        if (this.deckIdx == null) {
-            this.setDeckIdx(0);
+
+        // Delete the current card so we don't return it again
+        if (this.hasCurrentCard) {
+            this.singleDeckIterator.deleteCurrentCard();
         }
-        while (this.deckIdx < this.deckArray.length) {
-            if (this.singleDeckIterator.nextCard()) {
-                result = true;
-                break;
+
+        if (this.iteratorOrder.cardOrder == CardOrder.EveryCardRandomDeckAndCard) {
+            result = this.nextCard_EveryCardRandomDeck();
+        } else {
+            // If we are just starting, then depending on settings we want to either start from the first deck,
+            // or a random deck
+            if (this.deckIdx == null) {
+                this.chooseNextDeck(true);
             }
-            this.deckIdx++;
-            if (this.deckIdx < this.deckArray.length) {
-                this.singleDeckIterator.setDeck(this.deckArray[this.deckIdx]);
+            while (this.deckIdx < this.deckArray.length) {
+                if (this.singleDeckIterator.nextCard()) {
+                    result = true;
+                    break;
+                }
+                this.chooseNextDeck(false);
             }
         }
         if (!result) this.deckIdx = null;
         return result;
     }
 
+    private chooseNextDeck(firstTime: boolean): void {
+        switch (this.iteratorOrder.deckOrder) {
+            case DeckOrder.PrevDeckComplete_Sequential:
+                this.deckIdx = firstTime ? 0 : this.deckIdx + 1;
+                break;
+
+            case DeckOrder.PrevDeckComplete_Random: {
+                // Equal probability of picking any deck that has cards within
+                const weights: Record<number, number> = {};
+                let hasDeck: boolean = false;
+                for (let i = 0; i < this.deckArray.length; i++) {
+                    if (this.deckArray[i].getCardCount(CardListType.All, false)) {
+                        weights[i] = 1;
+                        hasDeck = true;
+                    }
+                }
+                if (hasDeck) {
+                    const [deckIdx, _] = this.weightedRandomNumber.getRandomValues(weights);
+                    this.deckIdx = deckIdx;
+                } else {
+                    // Our signal that no deck with cards present
+                    this.deckIdx = this.deckArray.length;
+                }
+                break;
+            }
+        }
+        if (this.deckIdx < this.deckArray.length) {
+            this.singleDeckIterator.setDeck(this.deckArray[this.deckIdx]);
+        }
+    }
+
+    private nextCard_EveryCardRandomDeck(): boolean {
+        // Make the chance of picking a specific deck proportional to the number of cards within
+        const weights: Record<number, number> = {};
+        for (let i = 0; i < this.deckArray.length; i++) {
+            const cardCount: number = this.deckArray[i].getCardCount(CardListType.All, false);
+            if (cardCount) {
+                weights[i] = cardCount;
+            }
+        }
+        if (Object.keys(weights).length == 0) return false;
+
+        const [deckIdx, cardIdx] = this.weightedRandomNumber.getRandomValues(weights);
+        this.setDeckIdx(deckIdx);
+        this.singleDeckIterator.setNewOrDueCardIdx(cardIdx);
+        return true;
+    }
+
     deleteCurrentQuestion(): boolean {
         this.singleDeckIterator.deleteCurrentQuestion();
         return this.nextCard();
@@ -236,4 +358,13 @@ export class DeckTreeIterator implements IDeckTreeIterator {
     moveCurrentCardToEndOfList(): void {
         this.singleDeckIterator.moveCurrentCardToEndOfList();
     }
+
+    private removeCurrentDeckIfEmpty(): void {
+        if (this.currentDeck.getCardCount(CardListType.All, false) == 0) {
+            this.deckArray.splice(this.deckIdx, 1);
+
+            // There is no change to deckIdx, but this now is a different deck
+            if (this.deckIdx < this.deckArray.length) this.setDeckIdx(this.deckIdx);
+        }
+    }
 }
diff --git a/src/DeckTreeStatsCalculator.ts b/src/DeckTreeStatsCalculator.ts
index e810b09..5338719 100644
--- a/src/DeckTreeStatsCalculator.ts
+++ b/src/DeckTreeStatsCalculator.ts
@@ -1,11 +1,11 @@
 import { Deck } from "./Deck";
 import {
-    CardListOrder,
+    CardOrder,
+    DeckOrder,
     DeckTreeIterator,
     IDeckTreeIterator,
     IIteratorOrder,
     IteratorDeckSource,
-    OrderMethod,
 } from "./DeckTreeIterator";
 import { Card } from "./Card";
 import { Stats } from "./stats";
@@ -17,9 +17,8 @@ export class DeckTreeStatsCalculator {
     calculate(deckTree: Deck): Stats {
         // Order doesn't matter as long as we iterate over everything
         const iteratorOrder: IIteratorOrder = {
-            deckOrder: OrderMethod.Sequential,
-            cardListOrder: CardListOrder.DueFirst,
-            cardOrder: OrderMethod.Sequential,
+            deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+            cardOrder: CardOrder.DueFirstSequential,
         };
         const iterator: IDeckTreeIterator = new DeckTreeIterator(
             iteratorOrder,
diff --git a/src/lang/locale/ar.ts b/src/lang/locale/ar.ts
index 5d5257e..fdc6769 100644
--- a/src/lang/locale/ar.ts
+++ b/src/lang/locale/ar.ts
@@ -82,6 +82,18 @@ export default {
     RESET_DEFAULT: "إعادة تعيين إلى الافتراضي",
     CARD_MODAL_WIDTH_PERCENT: "نسبة عرض البطاقة",
     RANDOMIZE_CARD_ORDER: "ترتيب بطاقة عشوائي أثناء المراجعة؟",
+    REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review",
+    REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
+    REVIEW_DECK_ORDER: "Order decks are displayed during review",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL:
+        "Sequentially (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM:
+        "Randomly (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
     DISABLE_CLOZE_CARDS: "؟cloze تعطيل بطاقات",
     CONVERT_HIGHLIGHTS_TO_CLOZES: "Convert ==hightlights== to clozes?",
     CONVERT_BOLD_TEXT_TO_CLOZES: "Convert **bolded text** to clozes?",
diff --git a/src/lang/locale/cz.ts b/src/lang/locale/cz.ts
index 028af54..cd74186 100644
--- a/src/lang/locale/cz.ts
+++ b/src/lang/locale/cz.ts
@@ -85,6 +85,18 @@ export default {
     RESET_DEFAULT: "Resetovat výchozí nastavení",
     CARD_MODAL_WIDTH_PERCENT: "Šířka kartiček v procentech",
     RANDOMIZE_CARD_ORDER: "Náhodně změnit pořadí kartiček během revize?",
+    REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review",
+    REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
+    REVIEW_DECK_ORDER: "Order decks are displayed during review",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL:
+        "Sequentially (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM:
+        "Randomly (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
     DISABLE_CLOZE_CARDS: "Vypnout cloze kartičky?",
     CONVERT_HIGHLIGHTS_TO_CLOZES: "Převést ==zvýraznění== na clozes?",
     CONVERT_BOLD_TEXT_TO_CLOZES: "Převést **tučný text** na clozes?",
diff --git a/src/lang/locale/de.ts b/src/lang/locale/de.ts
index a064254..b538441 100644
--- a/src/lang/locale/de.ts
+++ b/src/lang/locale/de.ts
@@ -91,6 +91,18 @@ export default {
     RESET_DEFAULT: "Standardeinstellung wiederherstellen",
     CARD_MODAL_WIDTH_PERCENT: "Breite einer Lernkarte in Prozent",
     RANDOMIZE_CARD_ORDER: "Während der Wiederhoung die Reihenfolge zufällig mischen?",
+    REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review",
+    REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
+    REVIEW_DECK_ORDER: "Order decks are displayed during review",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL:
+        "Sequentially (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM:
+        "Randomly (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
     DISABLE_CLOZE_CARDS: "Lückentextkarten (cloze deletions) deaktivieren?",
     CONVERT_HIGHLIGHTS_TO_CLOZES: "==Hervorgehobenen== Text in Lückentextkarten umwandeln?",
     CONVERT_BOLD_TEXT_TO_CLOZES: "**Fettgedruckten** Text in Lückentextkarten umwandeln?",
diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts
index b5a7878..4a5e76a 100644
--- a/src/lang/locale/en.ts
+++ b/src/lang/locale/en.ts
@@ -84,6 +84,18 @@ export default {
     RESET_DEFAULT: "Reset to default",
     CARD_MODAL_WIDTH_PERCENT: "Flashcard Width Percentage",
     RANDOMIZE_CARD_ORDER: "Randomize card order during review?",
+    REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review",
+    REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
+    REVIEW_DECK_ORDER: "Order decks are displayed during review",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL:
+        "Sequentially (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM:
+        "Randomly (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
     DISABLE_CLOZE_CARDS: "Disable cloze cards?",
     CONVERT_HIGHLIGHTS_TO_CLOZES: "Convert ==hightlights== to clozes?",
     CONVERT_BOLD_TEXT_TO_CLOZES: "Convert **bolded text** to clozes?",
diff --git a/src/lang/locale/es.ts b/src/lang/locale/es.ts
index f6be7b3..a4c9116 100644
--- a/src/lang/locale/es.ts
+++ b/src/lang/locale/es.ts
@@ -85,6 +85,18 @@ export default {
     RESET_DEFAULT: "Reiniciar a la configuración por defecto",
     CARD_MODAL_WIDTH_PERCENT: "Porcentaje del ancho de las tarjetas de memoria",
     RANDOMIZE_CARD_ORDER: "¿Aleatorizar el orden de las tarjetas para revisión?",
+    REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review",
+    REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
+    REVIEW_DECK_ORDER: "Order decks are displayed during review",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL:
+        "Sequentially (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM:
+        "Randomly (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
     DISABLE_CLOZE_CARDS: "¿Deshabilitar deletreo de huecos en las tarjetas?",
     CONVERT_HIGHLIGHTS_TO_CLOZES: "¿Convertir ==resaltados== a deletreo de huecos?",
     CONVERT_BOLD_TEXT_TO_CLOZES: "¿Convertir **texto en negrita** a deletreo de huecos?",
diff --git a/src/lang/locale/ja.ts b/src/lang/locale/ja.ts
index 5dd15da..47a2476 100644
--- a/src/lang/locale/ja.ts
+++ b/src/lang/locale/ja.ts
@@ -88,6 +88,18 @@ export default {
     RESET_DEFAULT: "デフォルト値にリセットする",
     CARD_MODAL_WIDTH_PERCENT: "フラッシュカードの横サイズのパーセンテージ",
     RANDOMIZE_CARD_ORDER: "レビュー中のカードの順番をランダムにしますか？",
+    REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review",
+    REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
+    REVIEW_DECK_ORDER: "Order decks are displayed during review",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL:
+        "Sequentially (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM:
+        "Randomly (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
     DISABLE_CLOZE_CARDS: "穴埋めカードを無効化しますか？",
     CONVERT_HIGHLIGHTS_TO_CLOZES: "==ハイライト==を穴埋めとして使用しますか？",
     CONVERT_BOLD_TEXT_TO_CLOZES: "**ボールド体**を穴埋めとして使用しますか？",
diff --git a/src/lang/locale/ko.ts b/src/lang/locale/ko.ts
index 43e1dc8..f3408ac 100644
--- a/src/lang/locale/ko.ts
+++ b/src/lang/locale/ko.ts
@@ -86,6 +86,18 @@ export default {
     RESET_DEFAULT: "기본값으로 초기화",
     CARD_MODAL_WIDTH_PERCENT: "플래시카드 너비 비율",
     RANDOMIZE_CARD_ORDER: "리뷰중인 카드의 순서를 랜덤으로 두시겠습니까?",
+    REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review",
+    REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
+    REVIEW_DECK_ORDER: "Order decks are displayed during review",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL:
+        "Sequentially (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM:
+        "Randomly (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
     DISABLE_CLOZE_CARDS: "빈 칸 채우기 카드를 비활성화하시겠습니까?",
     CONVERT_HIGHLIGHTS_TO_CLOZES: "==hightlights== 를 빈 칸 채우기로 전환하시겠습니까?",
     CONVERT_BOLD_TEXT_TO_CLOZES: "**bolded text** 를 빈 칸 채우기로 전환하시겠습니까?",
diff --git a/src/lang/locale/pt-br.ts b/src/lang/locale/pt-br.ts
index bda8952..2d309ad 100644
--- a/src/lang/locale/pt-br.ts
+++ b/src/lang/locale/pt-br.ts
@@ -88,6 +88,18 @@ export default {
     RESET_DEFAULT: "Reiniciar para a pré-definição",
     CARD_MODAL_WIDTH_PERCENT: "Porcentagem de Largura do Flashcard",
     RANDOMIZE_CARD_ORDER: "Aleatorizar a ordem das cartas durante a revisão?",
+    REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review",
+    REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
+    REVIEW_DECK_ORDER: "Order decks are displayed during review",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL:
+        "Sequentially (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM:
+        "Randomly (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
     DISABLE_CLOZE_CARDS: "Desabilitar cartas que usam omissão de palavras?",
     CONVERT_HIGHLIGHTS_TO_CLOZES: "Converter ==marca-texto== em omissões?",
     CONVERT_BOLD_TEXT_TO_CLOZES: "Converter **texto em negrito** em omissões?",
diff --git a/src/lang/locale/ru.ts b/src/lang/locale/ru.ts
index b05b533..6bf7d66 100644
--- a/src/lang/locale/ru.ts
+++ b/src/lang/locale/ru.ts
@@ -91,6 +91,18 @@ export default {
     RESET_DEFAULT: "Настройки по умолчанию",
     CARD_MODAL_WIDTH_PERCENT: "Ширина карточки в процентах",
     RANDOMIZE_CARD_ORDER: "Случайный порядок карточек во время повторения?",
+    REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review",
+    REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
+    REVIEW_DECK_ORDER: "Order decks are displayed during review",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL:
+        "Sequentially (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM:
+        "Randomly (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
     DISABLE_CLOZE_CARDS: "Выключить карты с пропусками (пример: [...])?",
     CONVERT_HIGHLIGHTS_TO_CLOZES: "Конвертировать ==выделенный текст== в пропуски (пример: [...])?",
     CONVERT_BOLD_TEXT_TO_CLOZES: "Конвертировать **жирный текст** в пропуски (пример: [...])?",
diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts
index 1dd9395..390ba1f 100644
--- a/src/lang/locale/zh-cn.ts
+++ b/src/lang/locale/zh-cn.ts
@@ -79,6 +79,18 @@ export default {
     RESET_DEFAULT: "重置为默认",
     CARD_MODAL_WIDTH_PERCENT: "卡片宽度百分比",
     RANDOMIZE_CARD_ORDER: "复习时随机显示卡片？",
+    REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review",
+    REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
+    REVIEW_DECK_ORDER: "Order decks are displayed during review",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL:
+        "Sequentially (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM:
+        "Randomly (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
     DISABLE_CLOZE_CARDS: "不进行完形填空？",
     CONVERT_HIGHLIGHTS_TO_CLOZES: "将 ==高亮== 转换为完形填空？",
     CONVERT_BOLD_TEXT_TO_CLOZES: "将 **粗体** 转换为完形填空？",
diff --git a/src/lang/locale/zh-tw.ts b/src/lang/locale/zh-tw.ts
index 35ba171..93c2fe3 100644
--- a/src/lang/locale/zh-tw.ts
+++ b/src/lang/locale/zh-tw.ts
@@ -79,6 +79,18 @@ export default {
     RESET_DEFAULT: "重置為預設值",
     CARD_MODAL_WIDTH_PERCENT: "卡片寬度百分比",
     RANDOMIZE_CARD_ORDER: "復習時隨機顯示卡片？",
+    REVIEW_CARD_ORDER_WITHIN_DECK: "Order cards in a deck are displayed during review",
+    REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL: "Sequentially within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL: "Sequentially within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_NEW_FIRST_RANDOM: "Randomly within a deck (All new cards first)",
+    REVIEW_CARD_ORDER_DUE_FIRST_RANDOM: "Randomly within a deck (All due cards first)",
+    REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
+    REVIEW_DECK_ORDER: "Order decks are displayed during review",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL:
+        "Sequentially (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM:
+        "Randomly (once all cards in previous deck reviewed)",
+    REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD: "Random card from random deck",
     DISABLE_CLOZE_CARDS: "停用填空克漏字卡片？",
     CONVERT_HIGHLIGHTS_TO_CLOZES: "將 ==高亮== 轉換為填空克漏字？",
     CONVERT_BOLD_TEXT_TO_CLOZES: "將 **粗體** 轉換為填空克漏字？",
diff --git a/src/main.ts b/src/main.ts
index 9000f8c..e5a6d69 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,7 +1,7 @@
 import { Notice, Plugin, TAbstractFile, TFile, getAllTags, FrontMatterCache } from "obsidian";
 import * as graph from "pagerank.js";
 
-import { SRSettingTab, SRSettings, DEFAULT_SETTINGS } from "src/settings";
+import { SRSettingTab, SRSettings, DEFAULT_SETTINGS, upgradeSettings } from "src/settings";
 import { FlashcardModal } from "src/gui/flashcard-modal";
 import { StatsModal } from "src/gui/stats-modal";
 import { ReviewQueueListView, REVIEW_QUEUE_VIEW_TYPE } from "src/gui/sidebar";
@@ -19,12 +19,12 @@ import {
     IFlashcardReviewSequencer as IFlashcardReviewSequencer,
 } from "./FlashcardReviewSequencer";
 import {
-    CardListOrder,
+    CardOrder,
     DeckTreeIterator,
     IDeckTreeIterator,
     IIteratorOrder,
     IteratorDeckSource,
-    OrderMethod,
+    DeckOrder,
 } from "./DeckTreeIterator";
 import { CardScheduleCalculator } from "./CardSchedule";
 import { Note } from "./Note";
@@ -304,10 +304,15 @@ export default class SRPlugin extends Plugin {
     }
 
     private static createDeckTreeIterator(settings: SRSettings): IDeckTreeIterator {
+        let cardOrder: CardOrder = CardOrder[settings.flashcardCardOrder as keyof typeof CardOrder];
+        if (cardOrder === undefined) cardOrder = CardOrder.DueFirstSequential;
+        let deckOrder: DeckOrder = DeckOrder[settings.flashcardDeckOrder as keyof typeof DeckOrder];
+        if (deckOrder === undefined) deckOrder = DeckOrder.PrevDeckComplete_Sequential;
+        console.log(`createDeckTreeIterator: CardOrder: ${cardOrder}, DeckOrder: ${deckOrder}`);
+
         const iteratorOrder: IIteratorOrder = {
-            deckOrder: OrderMethod.Sequential,
-            cardListOrder: CardListOrder.DueFirst,
-            cardOrder: settings.randomizeCardOrder ? OrderMethod.Random : OrderMethod.Sequential,
+            deckOrder,
+            cardOrder,
         };
         return new DeckTreeIterator(iteratorOrder, IteratorDeckSource.UpdatedByIterator);
     }
@@ -696,7 +701,9 @@ export default class SRPlugin extends Plugin {
     }
 
     async loadPluginData(): Promise<void> {
-        this.data = Object.assign({}, DEFAULT_DATA, await this.loadData());
+        const loadedData: PluginData = await this.loadData();
+        if (loadedData?.settings) upgradeSettings(loadedData.settings);
+        this.data = Object.assign({}, DEFAULT_DATA, loadedData);
         this.data.settings = Object.assign({}, DEFAULT_SETTINGS, this.data.settings);
     }
 
diff --git a/src/settings.ts b/src/settings.ts
index dcb121f..d5bcfe8 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -15,6 +15,8 @@ export interface SRSettings {
     flashcardHeightPercentage: number;
     flashcardWidthPercentage: number;
     randomizeCardOrder: boolean;
+    flashcardCardOrder: string;
+    flashcardDeckOrder: string;
     convertHighlightsToClozes: boolean;
     convertBoldTextToClozes: boolean;
     convertCurlyBracketsToClozes: boolean;
@@ -55,7 +57,10 @@ export const DEFAULT_SETTINGS: SRSettings = {
     showContextInCards: true,
     flashcardHeightPercentage: Platform.isMobile ? 100 : 80,
     flashcardWidthPercentage: Platform.isMobile ? 100 : 40,
-    randomizeCardOrder: true,
+    randomizeCardOrder: null,
+    flashcardCardOrder: "DueFirstRandom",
+    flashcardDeckOrder: "PrevDeckComplete_Sequential",
+
     convertHighlightsToClozes: true,
     convertBoldTextToClozes: false,
     convertCurlyBracketsToClozes: false,
@@ -84,6 +89,23 @@ export const DEFAULT_SETTINGS: SRSettings = {
     showDebugMessages: false,
 };
 
+export function upgradeSettings(settings: SRSettings) {
+    if (
+        settings.randomizeCardOrder != null &&
+        settings.flashcardCardOrder == null &&
+        settings.flashcardDeckOrder == null
+    ) {
+        console.log(`loadPluginData: Upgrading settings: ${settings.randomizeCardOrder}`);
+        settings.flashcardCardOrder = settings.randomizeCardOrder
+            ? "DueFirstRandom"
+            : "DueFirstSequential";
+        settings.flashcardDeckOrder = "PrevDeckComplete_Sequential";
+
+        // After the upgrade, we don't need the old attribute any more
+        settings.randomizeCardOrder = null;
+    }
+}
+
 // https://github.com/mgmeyers/obsidian-kanban/blob/main/src/Settings.ts
 let applyDebounceTimer = 0;
 function applySettingsUpdate(callback: () => void): void {
@@ -242,11 +264,55 @@ export class SRSettingTab extends PluginSettingTab {
                     });
             });
 
-        new Setting(containerEl).setName(t("RANDOMIZE_CARD_ORDER")).addToggle((toggle) =>
-            toggle
-                .setValue(this.plugin.data.settings.randomizeCardOrder)
+        new Setting(this.containerEl)
+            .setName(t("REVIEW_CARD_ORDER_WITHIN_DECK"))
+            .addDropdown((dropdown) =>
+                dropdown
+                    .addOptions({
+                        NewFirstSequential: t("REVIEW_CARD_ORDER_NEW_FIRST_SEQUENTIAL"),
+                        DueFirstSequential: t("REVIEW_CARD_ORDER_DUE_FIRST_SEQUENTIAL"),
+                        NewFirstRandom: t("REVIEW_CARD_ORDER_NEW_FIRST_RANDOM"),
+                        DueFirstRandom: t("REVIEW_CARD_ORDER_DUE_FIRST_RANDOM"),
+                        EveryCardRandomDeckAndCard: t("REVIEW_CARD_ORDER_RANDOM_DECK_AND_CARD"),
+                    })
+                    .setValue(this.plugin.data.settings.flashcardCardOrder)
+                    .onChange(async (value) => {
+                        this.plugin.data.settings.flashcardCardOrder = value;
+                        await this.plugin.savePluginData();
+
+                        // Need to redisplay as changing this setting affects the "deck order" setting
+                        this.display();
+                    }),
+            );
+
+        const deckOrderEnabled: boolean =
+            this.plugin.data.settings.flashcardCardOrder != "EveryCardRandomDeckAndCard";
+        new Setting(this.containerEl).setName(t("REVIEW_DECK_ORDER")).addDropdown((dropdown) =>
+            dropdown
+                .addOptions(
+                    deckOrderEnabled
+                        ? {
+                              PrevDeckComplete_Sequential: t(
+                                  "REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_SEQUENTIAL",
+                              ),
+                              PrevDeckComplete_Random: t(
+                                  "REVIEW_DECK_ORDER_PREV_DECK_COMPLETE_RANDOM",
+                              ),
+                          }
+                        : {
+                              EveryCardRandomDeckAndCard: t(
+                                  "REVIEW_DECK_ORDER_RANDOM_DECK_AND_CARD",
+                              ),
+                          },
+                )
+                .setValue(
+                    deckOrderEnabled
+                        ? this.plugin.data.settings.flashcardDeckOrder
+                        : "EveryCardRandomDeckAndCard",
+                )
+                .setDisabled(!deckOrderEnabled)
                 .onChange(async (value) => {
-                    this.plugin.data.settings.randomizeCardOrder = value;
+                    this.plugin.data.settings.flashcardDeckOrder = value;
                     await this.plugin.savePluginData();
                 }),
         );
diff --git a/src/util/RandomNumberProvider.ts b/src/util/RandomNumberProvider.ts
index aa14f86..b5fb5f0 100644
--- a/src/util/RandomNumberProvider.ts
+++ b/src/util/RandomNumberProvider.ts
@@ -1,3 +1,5 @@
+import { getTypedObjectEntries } from "./utils";
+
 export interface IRandomNumberProvider {
     getInteger(lowerBound: number, upperBound: number): number;
 }
@@ -16,11 +18,59 @@ export class StaticRandomNumberProvider implements IRandomNumberProvider {
 
     getInteger(lowerBound: number, upperBound: number): number {
         if (lowerBound != this.expectedLowerBound || upperBound != this.expectedUpperBound)
-            throw `lowerBound: ${lowerBound}/${this.expectedLowerBound}, upperBound: ${upperBound}/${this.expectedUpperBound}`;
+            throw `lowerBound: A${lowerBound}/E${this.expectedLowerBound}, upperBound: A${upperBound}/E${this.expectedUpperBound}`;
         return this.next;
     }
 }
 
+export class WeightedRandomNumber {
+    private provider: IRandomNumberProvider;
+
+    constructor(provider: IRandomNumberProvider) {
+        this.provider = provider;
+    }
+
+    static create(): WeightedRandomNumber {
+        return new WeightedRandomNumber(globalRandomNumberProvider);
+    }
+
+    //
+    // weights is a dictionary:
+    //      first number - a key that can be returned
+    //      second number - the "bucket size" - this is a weight that influences the probability of the
+    //          first number being returned
+    //
+    // returns:
+    //      first number - one of the keys from the weights parameter
+    //      second number - an "index" value; 0 <= index < bucketSize
+    getRandomValues(weights: Record<number, number>): [number, number] {
+        const total: number = WeightedRandomNumber.calcTotalOfCount(weights);
+        if (Object.values(weights).some((i) => !Number.isInteger(i) || i < 0))
+            throw "All weights must be positive integers";
+
+        const v: number = this.provider.getInteger(0, total - 1);
+        let x: number = 0;
+        for (const kvp in weights) {
+            const [value, count] = [Number(kvp), weights[kvp] as number];
+            if (v < x + count) {
+                // x <= v < x + count
+                const index: number = v - x;
+                return [value, index];
+            }
+            x += count;
+        }
+        throw "";
+    }
+
+    private static calcTotalOfCount(weights: Record<number, number>): number {
+        const total: number =
+            getTypedObjectEntries(weights)
+                .map(([_, count]) => count)
+                .reduce((a, b) => a + b, 0) || 0;
+        return total;
+    }
+}
+
 export let globalRandomNumberProvider: IRandomNumberProvider = new RandomNumberProvider();
 export const staticRandomNumberProvider: StaticRandomNumberProvider =
     new StaticRandomNumberProvider();
@@ -35,6 +85,8 @@ export function setupNextRandomNumber(info: IStaticRandom) {
     staticRandomNumberProvider.expectedLowerBound = info.lower;
     staticRandomNumberProvider.expectedUpperBound = info.upper;
     staticRandomNumberProvider.next = info.next;
+}
 
+export function setupStaticRandomNumberProvider() {
     globalRandomNumberProvider = staticRandomNumberProvider;
 }


Test patch:
diff --git a/tests/unit/DeckTreeIterator.test.ts b/tests/unit/DeckTreeIterator.test.ts
index 018294b..c245603 100644
--- a/tests/unit/DeckTreeIterator.test.ts
+++ b/tests/unit/DeckTreeIterator.test.ts
@@ -4,39 +4,32 @@ import { DEFAULT_SETTINGS } from "src/settings";
 import { SampleItemDecks } from "./SampleItems";
 import { TopicPath } from "src/TopicPath";
 import {
-    CardListOrder,
+    CardOrder,
     DeckTreeIterator,
     IIteratorOrder,
     IteratorDeckSource,
-    OrderMethod,
+    DeckOrder,
 } from "src/DeckTreeIterator";
 import {
     StaticDateProvider,
     globalDateProvider,
     setupStaticDateProvider_20230906,
 } from "src/util/DateProvider";
-import { setupNextRandomNumber } from "src/util/RandomNumberProvider";
+import {
+    setupNextRandomNumber,
+    setupStaticRandomNumberProvider,
+} from "src/util/RandomNumberProvider";
 
-export var order_DueFirst_Sequential: IIteratorOrder = {
-    cardOrder: OrderMethod.Sequential,
-    cardListOrder: CardListOrder.DueFirst,
-    deckOrder: OrderMethod.Sequential,
-};
-var order_DueFirst_Random: IIteratorOrder = {
-    cardOrder: OrderMethod.Random,
-    cardListOrder: CardListOrder.DueFirst,
-    deckOrder: OrderMethod.Sequential,
-};
-var order_NewFirst_Sequential: IIteratorOrder = {
-    cardOrder: OrderMethod.Sequential,
-    cardListOrder: CardListOrder.NewFirst,
-    deckOrder: OrderMethod.Sequential,
+let order_DueFirst_Sequential: IIteratorOrder = {
+    cardOrder: CardOrder.DueFirstSequential,
+    deckOrder: DeckOrder.PrevDeckComplete_Sequential,
 };
 
 var iterator: DeckTreeIterator;
 
 beforeAll(() => {
     setupStaticDateProvider_20230906();
+    setupStaticRandomNumberProvider();
 });
 
 describe("setDeck", () => {
@@ -47,7 +40,10 @@ Q2::A2
 Q3::A3`;
         let deck: Deck = await SampleItemDecks.createDeckFromText(text, new TopicPath(["Root"]));
         let iterator: DeckTreeIterator = new DeckTreeIterator(
-            order_NewFirst_Sequential,
+            {
+                cardOrder: CardOrder.NewFirstSequential,
+                deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+            },
             IteratorDeckSource.UpdatedByIterator,
         );
         iterator.setDeck(deck);
@@ -56,7 +52,7 @@ Q3::A3`;
 });
 
 describe("nextCard", () => {
-    describe("Sequential ordering", () => {
+    describe("DeckOrder.PrevDeckComplete_Sequential; Sequential card ordering", () => {
         describe("Due cards before new cards", () => {
             test("Single topic, new cards only", async () => {
                 let text: string = `
@@ -68,7 +64,10 @@ Q3::A3`;
                     new TopicPath(["Root"]),
                 );
                 let iterator: DeckTreeIterator = new DeckTreeIterator(
-                    order_DueFirst_Sequential,
+                    {
+                        cardOrder: CardOrder.DueFirstSequential,
+                        deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+                    },
                     IteratorDeckSource.UpdatedByIterator,
                 );
                 iterator.setDeck(deck);
@@ -101,7 +100,10 @@ Q6::A6`;
                         new TopicPath(["Root"]),
                     );
                     iterator = new DeckTreeIterator(
-                        order_DueFirst_Sequential,
+                        {
+                            cardOrder: CardOrder.DueFirstSequential,
+                            deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+                        },
                         IteratorDeckSource.UpdatedByIterator,
                     );
                     iterator.setDeck(deck);
@@ -141,7 +143,10 @@ Q6::A6`;
                         new TopicPath(["Root"]),
                     );
                     iterator = new DeckTreeIterator(
-                        order_DueFirst_Sequential,
+                        {
+                            cardOrder: CardOrder.DueFirstSequential,
+                            deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+                        },
                         IteratorDeckSource.UpdatedByIterator,
                     );
                     iterator.setDeck(deck);
@@ -181,7 +186,10 @@ Q3::A3`;
                     new TopicPath(["Root"]),
                 );
                 let iterator: DeckTreeIterator = new DeckTreeIterator(
-                    order_NewFirst_Sequential,
+                    {
+                        cardOrder: CardOrder.NewFirstSequential,
+                        deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+                    },
                     IteratorDeckSource.UpdatedByIterator,
                 );
                 iterator.setDeck(deck);
@@ -213,7 +221,10 @@ Q6::A6`;
                         new TopicPath(["Root"]),
                     );
                     iterator = new DeckTreeIterator(
-                        order_NewFirst_Sequential,
+                        {
+                            cardOrder: CardOrder.NewFirstSequential,
+                            deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+                        },
                         IteratorDeckSource.UpdatedByIterator,
                     );
                     iterator.setDeck(deck);
@@ -245,7 +256,10 @@ Q6::A6`;
                         new TopicPath(["Root"]),
                     );
                     iterator = new DeckTreeIterator(
-                        order_DueFirst_Sequential,
+                        {
+                            cardOrder: CardOrder.DueFirstSequential,
+                            deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+                        },
                         IteratorDeckSource.UpdatedByIterator,
                     );
                     iterator.setDeck(deck);
@@ -285,7 +299,10 @@ Q6::A6`;
                         new TopicPath(["Root"]),
                     );
                     iterator = new DeckTreeIterator(
-                        order_NewFirst_Sequential,
+                        {
+                            cardOrder: CardOrder.NewFirstSequential,
+                            deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+                        },
                         IteratorDeckSource.UpdatedByIterator,
                     );
                     iterator.setDeck(deck);
@@ -313,7 +330,7 @@ Q6::A6`;
         });
     });
 
-    describe("Random ordering", () => {
+    describe("DeckOrder.PrevDeckComplete_Sequential; Random card ordering", () => {
         describe("Due cards before new cards", () => {
             test("All new cards", async () => {
                 let text: string = `
@@ -329,7 +346,10 @@ Q6::A6`;
                     new TopicPath(["Root"]),
                 );
                 iterator = new DeckTreeIterator(
-                    order_DueFirst_Random,
+                    {
+                        cardOrder: CardOrder.DueFirstRandom,
+                        deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+                    },
                     IteratorDeckSource.UpdatedByIterator,
                 );
                 iterator.setDeck(deck);
@@ -375,7 +395,10 @@ QS3::Q <!--SR:!2023-09-02,4,270-->`;
                     new TopicPath(["Root"]),
                 );
                 iterator = new DeckTreeIterator(
-                    order_DueFirst_Random,
+                    {
+                        cardOrder: CardOrder.DueFirstRandom,
+                        deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+                    },
                     IteratorDeckSource.UpdatedByIterator,
                 );
                 iterator.setDeck(deck);
@@ -419,6 +442,132 @@ QS3::Q <!--SR:!2023-09-02,4,270-->`;
             });
         });
     });
+
+    describe("DeckOrder.PrevDeckComplete_Random", () => {
+        test("CardOrder.NewFirstSequential", async () => {
+            let text: string = `
+#flashcards Q1::A1
+#flashcards Q2::A2 <!--SR:!2023-09-02,4,270-->
+#flashcards Q3::A3
+
+#flashcards/science Q4::A4 <!--SR:!2023-09-02,4,270-->
+#flashcards/science Q5::A5
+
+#flashcards/science/physics Q6::A6
+#flashcards/science/physics Q7::A7
+
+#flashcards/science/chemistry Q8::A8
+                        `;
+            let deck: Deck = await SampleItemDecks.createDeckFromText(
+                text,
+                new TopicPath(["Root"]),
+            );
+            iterator = new DeckTreeIterator(
+                {
+                    cardOrder: CardOrder.NewFirstSequential,
+                    deckOrder: DeckOrder.PrevDeckComplete_Random,
+                },
+                IteratorDeckSource.UpdatedByIterator,
+            );
+            iterator.setDeck(deck);
+
+            // New root deck's cards first Q1/Q3, then due cards - Q2
+            setupNextRandomNumber({ lower: 0, upper: 3, next: 0 });
+            nextCardThenCheck("Q1");
+            nextCardThenCheck("Q3");
+            nextCardThenCheck("Q2");
+
+            // 3 decks with cards present to choose from (hence we expect the random number provider to be asked
+            // for a random number 0... 2):
+            //      [0=#flashcards/science, 1=#flashcards/science/physics, 2=#flashcards/science/chemistry]
+            // Have the random number provider return 1 when next asked; deck 1 corresponds to - #flashcards/science/physics
+            setupNextRandomNumber({ lower: 0, upper: 2, next: 1 });
+            nextCardThenCheck("Q6");
+            nextCardThenCheck("Q7");
+
+            // 2 decks to choose from [#flashcards/science, #flashcards/science/chemistry]
+            // Then random deck - #flashcards/science/chemistry
+            setupNextRandomNumber({ lower: 0, upper: 1, next: 1 });
+            nextCardThenCheck("Q8");
+
+            // 1 deck to choose from [#flashcards/science]
+            // Then subdeck #flashcards/science/chemistry
+            setupNextRandomNumber({ lower: 0, upper: 0, next: 0 });
+            nextCardThenCheck("Q5");
+            nextCardThenCheck("Q4");
+
+            // Check no more
+            expect(iterator.nextCard()).toEqual(false);
+        });
+    });
+
+    describe("DeckOrder.EveryCardRandomDeckAndCard", () => {
+        test("Simple test", async () => {
+            let text: string = `
+#flashcards Q1::A1
+#flashcards Q2::A2 <!--SR:!2023-09-02,4,270-->
+#flashcards Q3::A3
+
+#flashcards/science Q4::A4 <!--SR:!2023-09-02,4,270-->
+#flashcards/science Q5::A5
+
+#flashcards/science/physics Q6::A6
+#flashcards/science/physics Q7::A7
+
+#flashcards/science/chemistry Q8::A8
+                        `;
+            let deck: Deck = await SampleItemDecks.createDeckFromText(
+                text,
+                new TopicPath(["Root"]),
+            );
+            iterator = new DeckTreeIterator(
+                {
+                    cardOrder: CardOrder.EveryCardRandomDeckAndCard,
+                    deckOrder: null,
+                },
+                IteratorDeckSource.UpdatedByIterator,
+            );
+            iterator.setDeck(deck);
+
+            // 8 cards to choose from (hence we expect the random number provider to be asked
+            // for a random number 0... 7):
+            //      [0=Q1, 1=Q3, 2=Q2, 3=Q5, 4=Q4, 5=Q6, 6=Q7, 7=Q8]
+            // Have the random number provider return 5 when next asked; 5 corresponds to Q6
+            setupNextRandomNumber({ lower: 0, upper: 7, next: 5 });
+            nextCardThenCheck("Q6");
+
+            //      [0=Q1, 1=Q3, 2=Q2, 3=Q5, 4=Q4, 5=Q7, 6=Q8]
+            setupNextRandomNumber({ lower: 0, upper: 6, next: 3 });
+            nextCardThenCheck("Q5");
+
+            //      [0=Q1, 1=Q3, 2=Q2, 3=Q4, 4=Q7, 5=Q8]
+            setupNextRandomNumber({ lower: 0, upper: 5, next: 1 });
+            nextCardThenCheck("Q3");
+
+            //      [0=Q1, 1=Q2, 2=Q4, 3=Q7, 4=Q8]
+            setupNextRandomNumber({ lower: 0, upper: 4, next: 0 });
+            nextCardThenCheck("Q1");
+
+            //      [0=Q2, 1=Q4, 2=Q7, 3=Q8]
+            setupNextRandomNumber({ lower: 0, upper: 3, next: 3 });
+            nextCardThenCheck("Q8");
+
+            //      [0=Q2, 1=Q4, 2=Q7]
+            setupNextRandomNumber({ lower: 0, upper: 2, next: 1 });
+            nextCardThenCheck("Q4");
+
+            //      [0=Q2, 1=Q7]
+            setupNextRandomNumber({ lower: 0, upper: 1, next: 1 });
+            nextCardThenCheck("Q7");
+
+            //      [0=Q2]
+            setupNextRandomNumber({ lower: 0, upper: 0, next: 0 });
+            nextCardThenCheck("Q2");
+
+            // Check no more
+            expect(iterator.nextCard()).toEqual(false);
+        });
+    });
 });
 
 describe("hasCurrentCard", () => {
@@ -429,7 +578,10 @@ describe("hasCurrentCard", () => {
         Q3::A3`;
         let deck: Deck = await SampleItemDecks.createDeckFromText(text, new TopicPath(["Root"]));
         let iterator: DeckTreeIterator = new DeckTreeIterator(
-            order_NewFirst_Sequential,
+            {
+                cardOrder: CardOrder.NewFirstSequential,
+                deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+            },
             IteratorDeckSource.UpdatedByIterator,
         );
         iterator.setDeck(deck);
@@ -443,7 +595,10 @@ describe("hasCurrentCard", () => {
         Q3::A3`;
         let deck: Deck = await SampleItemDecks.createDeckFromText(text, new TopicPath(["Root"]));
         let iterator: DeckTreeIterator = new DeckTreeIterator(
-            order_NewFirst_Sequential,
+            {
+                cardOrder: CardOrder.NewFirstSequential,
+                deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+            },
             IteratorDeckSource.UpdatedByIterator,
         );
         iterator.setDeck(deck);
@@ -451,16 +606,23 @@ describe("hasCurrentCard", () => {
         expect(iterator.hasCurrentCard).toEqual(true);
     });
 });
-/* describe("deleteCurrentCard", () => {
+
+describe("deleteCurrentCard", () => {
     test("Delete after all cards iterated - exception throw", async () => {
         let text: string = `
-        Q1::A1
-        Q2::A2 usim
-        Q3::A3`;
+Q1::A1
+Q2::A2
+Q3::A3`;
         let deck: Deck = await SampleItemDecks.createDeckFromText(text, new TopicPath(["Root"]));
-        iterator = new DeckTreeSequentialIterator(order_NewFirst_Sequential);
+        iterator = new DeckTreeIterator(
+            {
+                cardOrder: CardOrder.NewFirstSequential,
+                deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+            },
+            IteratorDeckSource.UpdatedByIterator,
+        );
         iterator.setDeck(deck);
-        
+
         expect(iterator.nextCard()).toEqual(true);
         expect(iterator.nextCard()).toEqual(true);
         expect(iterator.nextCard()).toEqual(true);
@@ -474,23 +636,28 @@ describe("hasCurrentCard", () => {
 
     test("Delete card, with single card remaining after it", async () => {
         let text: string = `
-        Q1::A1
-        Q2::A2
-        Q3::A3`;
+Q1::A1
+Q2::A2
+Q3::A3`;
         let deck: Deck = await SampleItemDecks.createDeckFromText(text, new TopicPath(["Root"]));
         expect(deck.newFlashcards.length).toEqual(3);
-        iterator = new DeckTreeSequentialIterator(order_NewFirst_Sequential);
+        iterator = new DeckTreeIterator(
+            {
+                cardOrder: CardOrder.NewFirstSequential,
+                deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+            },
+            IteratorDeckSource.UpdatedByIterator,
+        );
         iterator.setDeck(deck);
-        
+
         nextCardThenCheck("Q1");
         nextCardThenCheck("Q2");
         expect(iterator.deleteCurrentCard()).toEqual(true);
         expect(iterator.currentCard.front).toEqual("Q3");
         expect(iterator.deleteCurrentCard()).toEqual(false);
     });
-
 });
- */
+
 function nextCardThenCheck(expectedFront: string): void {
     expect(iterator.nextCard()).toEqual(true);
     expect(iterator.currentCard.front).toEqual(expectedFront);
diff --git a/tests/unit/FlashcardReviewSequencer.test.ts b/tests/unit/FlashcardReviewSequencer.test.ts
index 87e2a3a..9207c8b 100644
--- a/tests/unit/FlashcardReviewSequencer.test.ts
+++ b/tests/unit/FlashcardReviewSequencer.test.ts
@@ -1,5 +1,7 @@
 import { CardScheduleCalculator } from "src/CardSchedule";
 import {
+    CardOrder,
+    DeckOrder,
     DeckTreeIterator,
     IDeckTreeIterator,
     IIteratorOrder,
@@ -24,7 +26,11 @@ import {
 import moment from "moment";
 import { INoteEaseList, NoteEaseList } from "src/NoteEaseList";
 import { QuestionPostponementList, IQuestionPostponementList } from "src/QuestionPostponementList";
-import { order_DueFirst_Sequential } from "./DeckTreeIterator.test";
+
+let order_DueFirst_Sequential: IIteratorOrder = {
+    cardOrder: CardOrder.DueFirstSequential,
+    deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+};
 
 let clozeQuestion1: string = "This single ==question== turns into ==3 separate== ==cards==";
 let clozeQuestion1Card1: RegExp = /This single.+\.\.\..+turns into 3 separate cards/;
diff --git a/tests/unit/util/RandomNumberProvider.test.ts b/tests/unit/util/RandomNumberProvider.test.ts
new file mode 100644
index 0000000..de9478b
--- /dev/null
+++ b/tests/unit/util/RandomNumberProvider.test.ts
@@ -0,0 +1,75 @@
+import {
+    IStaticRandom,
+    WeightedRandomNumber,
+    setupNextRandomNumber,
+    setupStaticRandomNumberProvider,
+} from "src/util/RandomNumberProvider";
+
+let provider: WeightedRandomNumber;
+
+beforeAll(() => {
+    setupStaticRandomNumberProvider();
+    provider = WeightedRandomNumber.create();
+});
+
+describe("WeightedRandomNumber", () => {
+    test("Single weight", () => {
+        const weights: Record<number, number> = {
+            10: 1,
+        };
+        check_getRandomValues(weights, { lower: 0, upper: 0, next: 0 }, 10, 0);
+    });
+
+    test("Two weights", () => {
+        // We want the value 10 to be returned 1% of the time and 20 the remaining 99%
+        const weights: Record<number, number> = {
+            10: 1,
+            20: 99,
+        };
+        check_getRandomValues(weights, { lower: 0, upper: 99, next: 0 }, 10, 0);
+
+        check_getRandomValues(weights, { lower: 0, upper: 99, next: 1 }, 20, 0);
+        check_getRandomValues(weights, { lower: 0, upper: 99, next: 99 }, 20, 98);
+    });
+
+    test("4 weights", () => {
+        const weights: Record<number, number> = {
+            10: 5,
+            20: 33,
+            30: 1,
+            40: 11,
+        };
+        check_getRandomValues(weights, { lower: 0, upper: 49, next: 0 }, 10, 0);
+        check_getRandomValues(weights, { lower: 0, upper: 49, next: 4 }, 10, 4);
+        check_getRandomValues(weights, { lower: 0, upper: 49, next: 5 }, 20, 0);
+        check_getRandomValues(weights, { lower: 0, upper: 49, next: 37 }, 20, 32);
+        check_getRandomValues(weights, { lower: 0, upper: 49, next: 38 }, 30, 0);
+        check_getRandomValues(weights, { lower: 0, upper: 49, next: 39 }, 40, 0);
+        check_getRandomValues(weights, { lower: 0, upper: 49, next: 49 }, 40, 10);
+    });
+
+    test("Invalid weights parameter", () => {
+        const weights: Record<number, number> = {
+            10: 5,
+            20: 33,
+            30: 0,
+            40: 11,
+        };
+        const t = () => {
+            check_getRandomValues(weights, { lower: 0, upper: 49, next: 0 }, 10, 10);
+        };
+        expect(t).toThrow();
+    });
+});
+
+function check_getRandomValues(
+    weights: Record<number, number>,
+    info: IStaticRandom,
+    expectedValue: number,
+    expectedIndex: number,
+) {
+    setupNextRandomNumber(info);
+    const [value, index] = provider.getRandomValues(weights);
+    expect(value).toEqual(expectedValue);
+    expect(index).toEqual(expectedIndex);
+}