switch to 4 buttons when previewing in test scheduler

- Currently we just use 1.5x and 2x the normal preview delay; we could
change this in the future.
- Don't try to capture the current state; just use a flag to denote
exit status.
- Show (end) when exiting
This commit is contained in:
Damien Elmes 2021-03-01 23:47:00 +10:00
parent c74a71a6d7
commit de7baa80bd
13 changed files with 132 additions and 80 deletions

View file

@ -239,9 +239,6 @@ class Scheduler:
return card.queue
def answerButtons(self, card: Card) -> int:
conf = self._cardConf(card)
if card.odid and not conf["resched"]:
return 2
return 4
def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str:

View file

@ -857,9 +857,16 @@ def test_preview():
col.reset()
# grab the first card
c = col.sched.getCard()
assert col.sched.answerButtons(c) == 2
if is_2021():
passing_grade = 4
else:
passing_grade = 2
assert col.sched.answerButtons(c) == passing_grade
assert col.sched.nextIvl(c, 1) == 600
assert col.sched.nextIvl(c, 2) == 0
assert col.sched.nextIvl(c, passing_grade) == 0
# failing it will push its due time back
due = c.due
col.sched.answerCard(c, 1)
@ -870,7 +877,7 @@ def test_preview():
assert c2.id != c.id
# passing it will remove it
col.sched.answerCard(c2, 2)
col.sched.answerCard(c2, passing_grade)
assert c2.queue == QUEUE_TYPE_NEW
assert c2.reps == 0
assert c2.type == CARD_TYPE_NEW

View file

@ -1319,7 +1319,7 @@ message SchedulingState {
}
message Preview {
uint32 scheduled_secs = 1;
Normal original_state = 2;
bool finished = 2;
}
message ReschedulingFilter {
Normal original_state = 1;

View file

@ -7,7 +7,7 @@ impl From<pb::scheduling_state::Preview> for PreviewState {
fn from(state: pb::scheduling_state::Preview) -> Self {
PreviewState {
scheduled_secs: state.scheduled_secs,
original_state: state.original_state.unwrap_or_default().into(),
finished: state.finished,
}
}
}
@ -16,7 +16,7 @@ impl From<PreviewState> for pb::scheduling_state::Preview {
fn from(state: PreviewState) -> Self {
pb::scheduling_state::Preview {
scheduled_secs: state.scheduled_secs,
original_state: Some(state.original_state.into()),
finished: state.finished,
}
}
}

View file

@ -49,7 +49,7 @@ impl CardStateUpdater {
} else {
PreviewState {
scheduled_secs: filtered.preview_delay * 60,
original_state: normal_state,
finished: false,
}
.into()
}

View file

@ -5,7 +5,6 @@ mod current;
mod learning;
mod preview;
mod relearning;
mod rescheduling_filter;
mod review;
mod revlog;
@ -44,7 +43,6 @@ pub struct CardAnswer {
pub milliseconds_taken: u32,
}
// fixme: 4 buttons for previewing
// fixme: log preview review
// fixme: undo
@ -107,25 +105,52 @@ impl CardStateUpdater {
current: CardState,
next: CardState,
) -> Result<Option<RevlogEntryPartial>> {
// any non-preview answer resets card.odue and increases reps
if !matches!(current, CardState::Filtered(FilteredState::Preview(_))) {
self.card.reps += 1;
self.card.original_due = 0;
let revlog = match next {
CardState::Normal(normal) => {
// transitioning from filtered state?
if let CardState::Filtered(filtered) = &current {
match filtered {
FilteredState::Preview(_) => {
return Err(AnkiError::invalid_input(
"should set finished=true, not return different state",
));
}
FilteredState::Rescheduling(_) => {
// card needs to be removed from normal filtered deck, then scheduled normally
self.card.remove_from_filtered_deck_before_reschedule();
}
}
}
// apply normal scheduling
self.apply_normal_study_state(current, normal)
}
CardState::Filtered(filtered) => {
self.ensure_filtered()?;
match filtered {
FilteredState::Preview(next) => self.apply_preview_state(current, next),
FilteredState::Rescheduling(next) => {
self.apply_normal_study_state(current, next.original_state)
}
}
}
}?;
Ok(revlog)
}
fn apply_normal_study_state(
&mut self,
current: CardState,
next: NormalState,
) -> Result<Option<RevlogEntryPartial>> {
self.card.reps += 1;
self.card.original_due = 0;
let revlog = match next {
CardState::Normal(normal) => match normal {
NormalState::New(next) => self.apply_new_state(current, next),
NormalState::Learning(next) => self.apply_learning_state(current, next),
NormalState::Review(next) => self.apply_review_state(current, next),
NormalState::Relearning(next) => self.apply_relearning_state(current, next),
},
CardState::Filtered(filtered) => match filtered {
FilteredState::Preview(next) => self.apply_preview_state(current, next),
FilteredState::Rescheduling(next) => {
self.apply_rescheduling_filter_state(current, next)
}
},
}?;
if next.leeched() && self.config.inner.leech_action() == LeechAction::Suspend {
@ -173,6 +198,7 @@ impl Collection {
let now = TimestampSecs::now();
let timing = self.timing_for_timestamp(now)?;
let secs_until_rollover = (timing.next_day_at - now.0).max(0) as u32;
Ok(vec![
answer_button_time_collapsible(
choices

View file

@ -3,6 +3,7 @@
use crate::{
card::CardQueue,
config::SchedulerVersion,
prelude::*,
scheduler::states::{CardState, IntervalKind, PreviewState},
};
@ -17,7 +18,12 @@ impl CardStateUpdater {
current: CardState,
next: PreviewState,
) -> Result<Option<RevlogEntryPartial>> {
self.ensure_filtered()?;
if next.finished {
self.card
.remove_from_filtered_deck_restoring_queue(SchedulerVersion::V2);
return Ok(None);
}
self.card.queue = CardQueue::PreviewRepeat;
let interval = next.interval_kind();
@ -48,7 +54,7 @@ mod test {
card::CardType,
scheduler::{
answering::{CardAnswer, Rating},
states::{CardState, FilteredState, LearnState, NormalState},
states::{CardState, FilteredState},
},
timestamp::TimestampMillis,
};
@ -56,42 +62,35 @@ mod test {
#[test]
fn preview() -> Result<()> {
let mut col = open_test_collection();
dbg!(col.scheduler_version());
let mut c = Card {
deck_id: DeckID(1),
ctype: CardType::Learn,
queue: CardQueue::Learn,
queue: CardQueue::DayLearn,
remaining_steps: 2,
due: 123,
..Default::default()
};
col.add_card(&mut c)?;
// set the first (current) step to a day
let deck = col.storage.get_deck(DeckID(1))?.unwrap();
let mut conf = col
.get_deck_config(DeckConfID(deck.normal()?.config_id), false)?
.unwrap();
*conf.inner.learn_steps.get_mut(0).unwrap() = 24.0 * 60.0;
col.add_or_update_deck_config(&mut conf, false)?;
// pull the card into a preview deck
let mut filtered_deck = Deck::new_filtered();
filtered_deck.filtered_mut()?.reschedule = false;
col.add_or_update_deck(&mut filtered_deck)?;
assert_eq!(col.rebuild_filtered_deck(filtered_deck.id)?, 1);
// the original state reflects the learning steps, not the card properties
let next = col.get_next_card_states(c.id)?;
assert_eq!(
assert!(matches!(
next.current,
CardState::Filtered(FilteredState::Preview(_))
));
// the exit state should have a 0 second interval, which will show up as (end)
assert!(matches!(
next.easy,
CardState::Filtered(FilteredState::Preview(PreviewState {
scheduled_secs: 600,
original_state: NormalState::Learning(LearnState {
remaining_steps: 2,
scheduled_secs: 86_400,
}),
scheduled_secs: 0,
finished: true
}))
);
));
// use Again on the preview
col.answer_card(&CardAnswer {
@ -106,8 +105,7 @@ mod test {
c = col.storage.get_card(c.id)?.unwrap();
assert_eq!(c.queue, CardQueue::PreviewRepeat);
// and then it should return to its old state once passed
// (based on learning steps)
// hard
let next = col.get_next_card_states(c.id)?;
col.answer_card(&CardAnswer {
card_id: c.id,
@ -118,8 +116,34 @@ mod test {
milliseconds_taken: 0,
})?;
c = col.storage.get_card(c.id)?.unwrap();
assert_eq!(c.queue, CardQueue::PreviewRepeat);
// good
let next = col.get_next_card_states(c.id)?;
col.answer_card(&CardAnswer {
card_id: c.id,
current_state: next.current,
new_state: next.good,
rating: Rating::Good,
answered_at: TimestampMillis::now(),
milliseconds_taken: 0,
})?;
c = col.storage.get_card(c.id)?.unwrap();
assert_eq!(c.queue, CardQueue::PreviewRepeat);
// and then it should return to its old state once easy selected
let next = col.get_next_card_states(c.id)?;
col.answer_card(&CardAnswer {
card_id: c.id,
current_state: next.current,
new_state: next.easy,
rating: Rating::Easy,
answered_at: TimestampMillis::now(),
milliseconds_taken: 0,
})?;
c = col.storage.get_card(c.id)?.unwrap();
assert_eq!(c.queue, CardQueue::DayLearn);
assert_eq!(c.due, 1);
assert_eq!(c.due, 123);
Ok(())
}

View file

@ -1,20 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::{
prelude::*,
scheduler::states::{CardState, ReschedulingFilterState},
};
use super::{CardStateUpdater, RevlogEntryPartial};
impl CardStateUpdater {
pub(super) fn apply_rescheduling_filter_state(
&mut self,
current: CardState,
next: ReschedulingFilterState,
) -> Result<Option<RevlogEntryPartial>> {
self.ensure_filtered()?;
self.apply_study_state(current, next.original_state.into())
}
}

View file

@ -15,7 +15,6 @@ impl CardStateUpdater {
current: CardState,
next: ReviewState,
) -> Result<Option<RevlogEntryPartial>> {
self.card.remove_from_filtered_deck_before_reschedule();
self.card.queue = CardQueue::Review;
self.card.ctype = CardType::Review;
self.card.interval = next.scheduled_days;

View file

@ -37,7 +37,7 @@ impl FilteredState {
pub(crate) fn review_state(self) -> Option<ReviewState> {
match self {
FilteredState::Preview(state) => state.original_state.review_state(),
FilteredState::Preview(_) => None,
FilteredState::Rescheduling(state) => state.original_state.review_state(),
}
}

View file

@ -64,6 +64,10 @@ impl NormalState {
NormalState::Relearning(RelearnState { review, .. }) => Some(review),
}
}
pub(crate) fn leeched(self) -> bool {
self.review_state().map(|r| r.leeched).unwrap_or_default()
}
}
impl From<NewState> for NormalState {

View file

@ -1,12 +1,12 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::{IntervalKind, NextCardStates, NormalState, StateContext};
use super::{IntervalKind, NextCardStates, StateContext};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PreviewState {
pub scheduled_secs: u32,
pub original_state: NormalState,
pub finished: bool,
}
impl PreviewState {
@ -22,9 +22,22 @@ impl PreviewState {
..self
}
.into(),
hard: self.original_state.into(),
good: self.original_state.into(),
easy: self.original_state.into(),
hard: PreviewState {
// ~15 minutes with the default setting
scheduled_secs: ctx.with_learning_fuzz(ctx.preview_step * 90),
..self
}
.into(),
good: PreviewState {
scheduled_secs: ctx.with_learning_fuzz(ctx.preview_step * 120),
..self
}
.into(),
easy: PreviewState {
scheduled_secs: 0,
finished: true,
}
.into(),
}
}
}

View file

@ -22,7 +22,9 @@ pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String {
/// Times within the collapse time are represented like '<10m'
pub fn answer_button_time_collapsible(seconds: u32, collapse_secs: u32, i18n: &I18n) -> String {
let string = answer_button_time(seconds as f32, i18n);
if seconds < collapse_secs {
if seconds == 0 {
i18n.tr(TR::SchedulingEnd).into()
} else if seconds < collapse_secs {
format!("<{}", string)
} else {
string