From 35c0a4e4ae82ec6cbda5c4f1cf33c19274c61f74 Mon Sep 17 00:00:00 2001 From: jar Date: Fri, 19 Sep 2025 20:31:45 +0000 Subject: [PATCH] Add random deck option --- CONTRIBUTORS | 3 +- ftl/core-repo | 2 +- ftl/core/deck-config.ftl | 69 ++++++++++--------- ftl/qt-repo | 2 +- proto/anki/deck_config.proto | 2 + .../src/scheduler/queue/builder/gathering.rs | 56 +++++++++++++++ rslib/src/scheduler/queue/builder/mod.rs | 15 ++++ ts/routes/deck-options/choices.ts | 4 ++ 8 files changed, 117 insertions(+), 36 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index def2c4327..d66842e79 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -187,7 +187,7 @@ Christian Donat Asuka Minato Dillon Baldwin Voczi -Ben Nguyen <105088397+bpnguyen107@users.noreply.github.com> +Ben Nguyen <105088397+bpnguyen107@users.noreply.github.com> Themis Demetriades Luke Bartholomew Gregory Abrasaldo @@ -228,6 +228,7 @@ Matt Brubeck Yaoliang Chen KolbyML Adnane Taghi +jariji ******************** diff --git a/ftl/core-repo b/ftl/core-repo index 0fe0162f4..480ef0da7 160000 --- a/ftl/core-repo +++ b/ftl/core-repo @@ -1 +1 @@ -Subproject commit 0fe0162f4a18e8ef2fbac1d9a33af8e38cf7260e +Subproject commit 480ef0da728c7ea3485c58529ae7ee02be3e5dba diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index 89ca62036..135349fd6 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -102,7 +102,7 @@ deck-config-leech-threshold-tooltip = # See actions-suspend-card and scheduling-tag-only for the wording deck-config-leech-action-tooltip = `Tag Only`: Add a 'leech' tag to the note, and display a pop-up. - + `Suspend Card`: In addition to tagging the note, hide the card until it is manually unsuspended. @@ -123,7 +123,7 @@ deck-config-bury-priority-tooltip = When Anki gathers cards, it first gathers intraday learning cards, then interday learning cards, then review cards, and finally new cards. This affects how burying works: - + - If you have all burying options enabled, the sibling that comes earliest in that list will be shown. For example, a review card will be shown in preference to a new card. @@ -141,18 +141,21 @@ deck-config-new-gather-priority-tooltip-2 = gathered in ascending position. If the daily limit of the selected deck is reached, gathering can stop before all subdecks have been checked. This order is fastest in large collections, and allows you to prioritize subdecks that are closer to the top. - + + `Each from random deck`: Repeatedly picks a random deck and takes the first card from it. + `Ascending position`: Gathers cards by ascending position (due #), which is typically the oldest-added first. - + `Descending position`: Gathers cards by descending position (due #), which is typically the latest-added first. - + `Random notes`: Picks notes at random, then gathers all of its cards. - + `Random cards`: Gathers cards in a random order. deck-config-new-gather-priority-deck = Deck deck-config-new-gather-priority-deck-then-random-notes = Deck, then random notes +deck-config-new-gather-priority-each-from-random-deck = Each from random deck deck-config-new-gather-priority-position-lowest-first = Ascending position deck-config-new-gather-priority-position-highest-first = Descending position deck-config-new-gather-priority-random-notes = Random notes @@ -160,21 +163,21 @@ deck-config-new-gather-priority-random-cards = Random cards deck-config-new-card-sort-order = New card sort order deck-config-new-card-sort-order-tooltip-2 = `Card type, then order gathered`: Shows cards in order of card type number. - Cards of each card type number are shown in the order they were gathered. + Cards of each card type number are shown in the order they were gathered. If you have sibling burying disabled, this will ensure all front→back cards are seen before any back→front cards. This is useful to have all cards of the same note shown in the same session, but not too close to one another. - + `Order gathered`: Shows cards exactly as they were gathered. If sibling burying is disabled, this will typically result in all cards of a note being seen one after the other. - + `Card type, then random`: Shows cards in order of card type number. Cards of each card type number are shown in a random order. This order is useful if you don't want sibling cards to appear too close to each other, but still want the cards to appear in a random order. - + `Random note, then card type`: Picks notes at random, then shows all of its cards in order. - + `Random`: Shows cards in a random order. deck-config-sort-order-card-template-then-random = Card type, then random deck-config-sort-order-random-note-then-template = Random note, then card type @@ -186,7 +189,7 @@ deck-config-new-review-priority-tooltip = When to show new cards in relation to deck-config-interday-step-priority = Interday learning/review order deck-config-interday-step-priority-tooltip = When to show (re)learning cards that cross a day boundary. - + The review limit is always applied first to interday learning cards, and then review cards. This option will control the order the gathered cards are shown in, but interday learning cards will always be gathered first. @@ -213,7 +216,7 @@ deck-config-sort-order-retrievability-ascending = Ascending retrievability deck-config-sort-order-retrievability-descending = Descending retrievability deck-config-display-order-will-use-current-deck = - Anki will use the display order from the deck you + Anki will use the display order from the deck you select to study, and not any subdecks it may have. ## Timer section @@ -240,7 +243,7 @@ deck-config-seconds-to-show-answer = Seconds to show answer for deck-config-seconds-to-show-answer-tooltip-2 = When auto advance is activated, the number of seconds to wait before applying the answer action. Set to 0 to disable. deck-config-question-action-show-answer = Show Answer deck-config-question-action-show-reminder = Show Reminder -deck-config-question-action = Question action +deck-config-question-action = Question action deck-config-question-action-tool-tip = The action to perform after the question is shown, and time has elapsed. deck-config-answer-action = Answer action deck-config-answer-action-tooltip-2 = The action to perform after the answer is shown, and time has elapsed. @@ -405,7 +408,7 @@ deck-config-historical-retention-tooltip = The latter is quite rare, so unless you're using the former option, you probably don't need to adjust this option. deck-config-weights-tooltip2 = - FSRS parameters affect how cards are scheduled. Anki will start with default parameters. You can use + FSRS parameters affect how cards are scheduled. Anki will start with default parameters. You can use the option below to optimize the parameters to best match your performance in decks using this preset. deck-config-reschedule-cards-on-change-tooltip = Affects the entire collection, and is not saved. @@ -420,31 +423,31 @@ deck-config-reschedule-cards-warning = Use this option sparingly, as it will add a review entry to each of your cards, and increase the size of your collection. -deck-config-ignore-before-tooltip-2 = +deck-config-ignore-before-tooltip-2 = If set, cards reviewed before the provided date will be ignored when optimizing FSRS parameters. This can be useful if you imported someone else's scheduling data, or have changed the way you use the answer buttons. deck-config-compute-optimal-weights-tooltip2 = - When you click the Optimize button, FSRS will analyze your review history, and generate parameters that are - optimal for your memory and the content you're studying. If your decks vary wildly in subjective difficulty, it - is recommended to assign them separate presets, as the parameters for easy decks and hard decks will be different. + When you click the Optimize button, FSRS will analyze your review history, and generate parameters that are + optimal for your memory and the content you're studying. If your decks vary wildly in subjective difficulty, it + is recommended to assign them separate presets, as the parameters for easy decks and hard decks will be different. You don't need to optimize your parameters frequently - once every few months is sufficient. - + By default, parameters will be calculated from the review history of all decks using the current preset. You can optionally adjust the search before calculating the parameters, if you'd like to alter which cards are used for optimizing the parameters. deck-config-compute-optimal-retention-tooltip4 = - This tool will attempt to find the desired retention value + This tool will attempt to find the desired retention value that will lead to the most material learnt, in the least amount of time. The calculated number can serve as a reference - when deciding what to set your desired retention to. You may wish to choose a higher desired retention if you’re + when deciding what to set your desired retention to. You may wish to choose a higher desired retention if you’re willing to invest more study time to achieve it. Setting your desired retention lower than the minimum is not recommended, as it will lead to a higher workload, because of the high forgetting rate. deck-config-please-save-your-changes-first = Please save your changes first. -deck-config-a-100-day-interval = +deck-config-a-100-day-interval = { $days -> [one] A 100 day interval will become { $days } day. *[other] A 100 day interval will become { $days } days. } -deck-config-percent-of-reviews = +deck-config-percent-of-reviews = { $reviews -> [one] { $pct }% of { $reviews } review *[other] { $pct }% of { $reviews } reviews @@ -488,18 +491,18 @@ deck-config-bury-if-new-review-or-interday = Bury if new, review, or interday le deck-config-bury-tooltip = Siblings are other cards from the same note (eg forward/reverse cards, or other cloze deletions from the same text). - + When this option is off, multiple cards from the same note may be seen on the same day. When enabled, Anki will automatically *bury* siblings, hiding them until the next day. This option allows you to choose which kinds of cards may be buried when you answer one of their siblings. - + When using the V3 scheduler, interday learning cards can also be buried. Interday learning cards are cards with a current learning step of one or more days. deck-config-seconds-to-show-question-tooltip = When auto advance is activated, the number of seconds to wait before revealing the answer. Set to 0 to disable. deck-config-answer-action-tooltip = The action to perform on the current card before automatically advancing to the next one. deck-config-wait-for-audio-tooltip = Wait for audio to finish before automatically revealing answer or next question. -deck-config-ignore-before-tooltip = +deck-config-ignore-before-tooltip = If set, reviews before the provided date will be ignored when optimizing & evaluating FSRS parameters. This can be useful if you imported someone else's scheduling data, or have changed the way you use the answer buttons. deck-config-compute-optimal-retention-tooltip = @@ -521,7 +524,7 @@ deck-config-compute-optimal-weights-tooltip = If you have decks that vary wildly in difficulty, it is recommended to assign them separate presets, as the parameters for easy decks and hard decks will be different. There is no need to optimize your parameters frequently - once every few months is sufficient. - + By default, parameters will be calculated from the review history of all decks using the current preset. You can optionally adjust the search before calculating the parameters, if you'd like to alter which cards are used for optimizing the parameters. @@ -532,11 +535,11 @@ deck-config-compute-optimal-retention-tooltip2 = if you’re willing to trade more study time for a greater recall rate. Setting your desired retention lower than the minimum is not recommended, as it will lead to more work without benefit. deck-config-compute-optimal-retention-tooltip3 = - This tool assumes that you’re starting with 0 learned cards, and will attempt to find the desired retention value - that will lead to the most material learnt, in the least amount of time. To accurately simulate your learning process, - this feature requires a minimum of 400+ reviews. The calculated number can serve as a reference when deciding what to - set your desired retention to. You may wish to choose a higher desired retention, if you’re willing to trade more study - time for a greater recall rate. Setting your desired retention lower than the minimum is not recommended, as it will + This tool assumes that you’re starting with 0 learned cards, and will attempt to find the desired retention value + that will lead to the most material learnt, in the least amount of time. To accurately simulate your learning process, + this feature requires a minimum of 400+ reviews. The calculated number can serve as a reference when deciding what to + set your desired retention to. You may wish to choose a higher desired retention, if you’re willing to trade more study + time for a greater recall rate. Setting your desired retention lower than the minimum is not recommended, as it will lead to a higher workload, because of the high forgetting rate. deck-config-seconds-to-show-question-tooltip-2 = When auto advance is activated, the number of seconds to wait before revealing the answer. Set to 0 to disable. deck-config-invalid-weights = Parameters must be either left blank to use the defaults, or must be 17 comma-separated numbers. diff --git a/ftl/qt-repo b/ftl/qt-repo index 17216b03d..fd5f98478 160000 --- a/ftl/qt-repo +++ b/ftl/qt-repo @@ -1 +1 @@ -Subproject commit 17216b03db7249600542e388bd4ea124478400e5 +Subproject commit fd5f984785ad07a0d3dbd893ee3d7e3671eaebd6 diff --git a/proto/anki/deck_config.proto b/proto/anki/deck_config.proto index efd8a80d0..76fb65991 100644 --- a/proto/anki/deck_config.proto +++ b/proto/anki/deck_config.proto @@ -56,6 +56,8 @@ message DeckConfig { NEW_CARD_GATHER_PRIORITY_RANDOM_NOTES = 3; // Siblings are neither grouped nor ordered. NEW_CARD_GATHER_PRIORITY_RANDOM_CARDS = 4; + // Pick one card from random deck. + NEW_CARD_GATHER_PRIORITY_EACH_FROM_RANDOM_DECK = 6; } enum NewCardSortOrder { // Ascending card template ordinal. diff --git a/rslib/src/scheduler/queue/builder/gathering.rs b/rslib/src/scheduler/queue/builder/gathering.rs index fb6274de5..102bdff0b 100644 --- a/rslib/src/scheduler/queue/builder/gathering.rs +++ b/rslib/src/scheduler/queue/builder/gathering.rs @@ -1,6 +1,9 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use rand::thread_rng; +use rand::Rng; +use std::collections::HashSet; use super::DueCard; use super::NewCard; use super::QueueBuilder; @@ -65,6 +68,10 @@ impl QueueBuilder { NewCardGatherPriority::Deck => { self.gather_new_cards_by_deck(col, NewCardSorting::LowestPosition) } + NewCardGatherPriority::EachFromRandomDeck => { + self.gather_new_cards_by_each_from_random_deck(col, NewCardSorting::LowestPosition) + } + NewCardGatherPriority::DeckThenRandomNotes => self.gather_new_cards_by_deck( col, NewCardSorting::RandomNotes(self.context.timing.days_elapsed), @@ -112,6 +119,55 @@ impl QueueBuilder { Ok(()) } + fn gather_new_cards_by_each_from_random_deck( + &mut self, + col: &mut Collection, + sort: NewCardSorting, + ) -> Result<()> { + let mut deck_ids = col.storage.get_active_deck_ids_sorted()?; + let mut rng = thread_rng(); + let mut cards_added = HashSet::::new(); + + // Continue until global limit is reached or no more decks with cards. + while !self.limits.root_limit_reached(LimitKind::New) && !deck_ids.is_empty() { + let selected_index = rng.gen_range(0..deck_ids.len()); + let selected_deck = deck_ids[selected_index]; + + if self.limits.limit_reached(selected_deck, LimitKind::New)? { + // Remove the deck from the list since it's at its limit. + deck_ids.swap_remove(selected_index); + continue; + } + + let mut found_card = false; + col.storage + .for_each_new_card_in_deck(selected_deck, sort, |card| { + let limit_reached = self.limits.limit_reached(selected_deck, LimitKind::New)?; + if limit_reached { + Ok(false) + } + else if !cards_added.contains(&card.id) && self.add_new_card(card) { + cards_added.insert(card.id); + self.limits + .decrement_deck_and_parent_limits(selected_deck, LimitKind::New)?; + found_card = true; + // Stop iterating this deck after getting one card. + return Ok(false); + } + else { + Ok(true) + } + })?; + + // If we couldn't find any card from this deck, remove it from consideration + if !found_card { + deck_ids.swap_remove(selected_index); + } + } + + Ok(()) + } + fn gather_new_cards_sorted( &mut self, col: &mut Collection, diff --git a/rslib/src/scheduler/queue/builder/mod.rs b/rslib/src/scheduler/queue/builder/mod.rs index c707f8df1..e418026fa 100644 --- a/rslib/src/scheduler/queue/builder/mod.rs +++ b/rslib/src/scheduler/queue/builder/mod.rs @@ -420,6 +420,21 @@ mod test { ]; assert_eq!(col.queue_as_deck_and_template(parent.id), cards); + // each from random deck - test that we get expected count with variety across decks + col.set_deck_gather_order(&mut parent, NewCardGatherPriority::EachFromRandomDeck); + let cards = col.queue_as_deck_and_template(parent.id); + + // Verify we get cards from multiple decks + let unique_decks: std::collections::HashSet<_> = cards.iter().map(|(deck_id, _)| *deck_id).collect(); + assert!(unique_decks.len() > 1, "EachFromRandomDeck should select from multiple decks"); + + // Verify the child limit is respected (child + grandchild <= 3) + let child_family_count = cards.iter().filter(|(deck_id, _)| *deck_id == child.id || *deck_id == grandchild.id).count(); + assert!(child_family_count <= 3, "Child limit should be respected"); + + // Should get 7 (out of 8) cards total (respects child limit of 3) + assert_eq!(cards.len(), 7); + Ok(()) } diff --git a/ts/routes/deck-options/choices.ts b/ts/routes/deck-options/choices.ts index 6f34eae0e..1ddc87672 100644 --- a/ts/routes/deck-options/choices.ts +++ b/ts/routes/deck-options/choices.ts @@ -25,6 +25,10 @@ export function newGatherPriorityChoices(): Choice