experiment with simple resched=off case to 'preview mode'

the previous approach meant we weren't able to preserve the card state
exactly when cards were in learning, since we didn't record the step
position prior to cards being moved into the filtered deck.

it also meant the answer buttons needed to change depending on state - 4
for cards in learning/review, but 2 when the card is on the final step
or is a review.

instead, in preview mode cards always have 2 buttons: again will repeat
again after a delay, and good immediately removes the card and restores
it to its previous state.

to accomplish this, we use a separate queue #, as the learn count
always needs to have a 1:1 correspondence to the number of cards
This commit is contained in:
Damien Elmes 2017-12-26 21:46:49 +10:00
parent 575f61c384
commit ba87fc7736
2 changed files with 110 additions and 222 deletions

View file

@ -57,11 +57,10 @@ class Scheduler:
def answerCard(self, card, ease):
self.col.log()
assert 1 <= ease <= 4
assert 0 <= card.queue <= 3
assert 0 <= card.queue <= 4
self.col.markReview(card)
if self._burySiblingsOnAnswer:
self._burySiblings(card)
card.reps += 1
self._answerCard(card, ease)
@ -71,6 +70,11 @@ class Scheduler:
card.flushSched()
def _answerCard(self, card, ease):
if self._previewingCard(card):
self._answerCardPreview(card, ease)
return
card.reps += 1
card.wasNew = card.type == 0
if card.queue == 0:
@ -88,6 +92,22 @@ class Scheduler:
# update daily limit
self._updateStats(card, 'rev')
# hard-coded for now
_previewDelay = 600
def _answerCardPreview(self, card, ease):
assert 1 <= ease <= 2
if ease == 1:
# repeat after delay
card.queue = 4
card.due = intTime() + self._previewDelay
self.lrnCount += 1
else:
# restore original card state and remove from filtered deck
self._restoreFromFiltered(card)
self._removeFromFiltered(card)
def counts(self, card=None):
counts = [self.newCount, self.lrnCount, self.revCount]
if card:
@ -117,23 +137,14 @@ order by due""" % self._deckLimit(),
return ret
def countIdx(self, card):
if card.queue == 3:
if card.queue in (3,4):
return 1
return card.queue
def answerButtons(self, card):
conf = self._cardConf(card)
# fixme: resched=off case
# if card.odid:
# if not conf['resched']:
# if card.queue == 2:
# return 4
# conf = self._lrnConf(card)
# if card.type in (0,1) or len(conf['delays']) > 1:
# return 3
# return 2
if card.odid and not conf['resched']:
return 2
return 4
def unburyCards(self):
@ -449,6 +460,10 @@ did in %s and queue = 1 and due < ? limit %d)""" % (
select count() from cards where did in %s and queue = 3
and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit),
self.today)
# previews
self.lrnCount += self.col.db.scalar("""
select count() from cards where did in %s and queue = 4
limit %d""" % (self._deckLimit(), self.reportLimit))
def _resetLrn(self):
self._resetLrnCount()
@ -464,7 +479,7 @@ and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit),
return True
self._lrnQueue = self.col.db.all("""
select due, id from cards where
did in %s and queue = 1 and due < :lim
did in %s and queue in (1,4) and due < :lim
limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
# as it arrives sorted by did first, we need to sort it
self._lrnQueue.sort()
@ -478,7 +493,10 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
if self._lrnQueue[0][0] < cutoff:
id = heappop(self._lrnQueue)[1]
card = self.col.getCard(id)
self.lrnCount -= card.left // 1000
if self._previewingCard(card):
self.lrnCount -= 1
else:
self.lrnCount -= card.left // 1000
return card
# daily learning
@ -542,10 +560,9 @@ did = ? and queue = 3 and due <= ? limit ?""",
def _moveToFirstStep(self, card, conf):
card.left = self._startingLeft(card)
resched = self._resched(card)
card.lastIvl = card.ivl
if card.type == 2 and resched:
if card.type == 2:
# review card that will move to relearning
card.ivl = self._lapseIvl(card, self._lrnConf(card))
@ -624,27 +641,15 @@ did = ? and queue = 3 and due <= ? limit ?""",
def _rescheduleAsRev(self, card, conf, early):
lapse = card.type == 2
resched = self._resched(card)
if resched:
if lapse:
self._rescheduleGraduatingLapse(card)
else:
self._rescheduleNew(card, conf, early)
if lapse:
self._rescheduleGraduatingLapse(card)
else:
self._rescheduleNew(card, conf, early)
# if we were dynamic, graduating means moving back to the old deck
if card.odid:
# and leaving learning if scheduling off
if not resched:
card.due = card.odue
if card.type == 2:
card.queue = 2
else:
card.queue = card.type = 0
card.did = card.odid
card.odue = 0
card.odid = 0
self._removeFromFiltered(card)
def _rescheduleGraduatingLapse(self, card):
card.due = self.today+card.ivl
@ -860,40 +865,28 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# update interval
card.lastIvl = card.ivl
if self._resched(card):
self._updateRevIvl(card, ease)
# then the rest
card.factor = max(1300, card.factor+[-150, 0, 150][ease-2])
card.due = self.today + card.ivl
else:
card.due = card.odue
self._updateRevIvl(card, ease)
# then the rest
card.factor = max(1300, card.factor+[-150, 0, 150][ease-2])
card.due = self.today + card.ivl
# card leaves filtered deck
if card.odid:
card.did = card.odid
card.odid = 0
card.odue = 0
self._removeFromFiltered(card)
def _rescheduleEarlyRev(self, card, ease):
# update interval
card.lastIvl = card.ivl
if self._resched(card):
self._updateEarlyRevIvl(card, ease)
# then the rest
card.factor = max(1300, card.factor+[-150, 0, 150][ease-2])
card.due = self.today + card.ivl
else:
card.due = card.odue
self._updateEarlyRevIvl(card, ease)
# then the rest
card.factor = max(1300, card.factor+[-150, 0, 150][ease-2])
card.due = self.today + card.ivl
# move from 0->2
card.queue = 2
# card leaves filtered deck
if card.odid:
card.did = card.odid
card.odid = 0
card.odue = 0
self._removeFromFiltered(card)
def _logRev(self, card, ease, delay, type):
def log():
@ -1111,6 +1104,25 @@ did = ?, due = ?, usn = ? where id = ?
"""
self.col.db.executemany(query, data)
def _removeFromFiltered(self, card):
if card.odid:
card.did = card.odid
card.odue = 0
card.odid = 0
def _restoreFromFiltered(self, card):
assert card.odid
card.due = card.odue
if card.type in (0, 2):
card.queue = card.type
else:
if card.odue > 1000000000:
card.queue = 1
else:
card.queue = 3
# Leeches
##########################################################################
@ -1129,12 +1141,6 @@ did = ?, due = ?, usn = ? where id = ?
# handle
a = conf['leechAction']
if a == 0:
# if it has an old due, remove it from cram/relearning
if card.odue:
card.due = card.odue
if card.odid:
card.did = card.odid
card.odue = card.odid = 0
card.queue = -1
# notify UI
runHook("leech", card)
@ -1194,11 +1200,9 @@ did = ?, due = ?, usn = ? where id = ?
def _deckLimit(self):
return ids2str(self.col.decks.active())
def _resched(self, card):
def _previewingCard(self, card):
conf = self._cardConf(card)
if not conf['dyn']:
return True
return conf['resched']
return conf['dyn'] and not conf['resched']
# Daily cutoff
##########################################################################
@ -1299,6 +1303,12 @@ To study outside of the normal schedule, click the Custom Study button below."""
def nextIvl(self, card, ease):
"Return the next interval for CARD, in seconds."
# preview mode?
if self._previewingCard(card):
if ease == 1:
return self._previewDelay
return 0
# (re)learning?
if card.queue in (0,1,3):
return self._nextLrnIvl(card, ease)
@ -1312,10 +1322,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
# review
early = card.odid and (card.odue > self.today)
if early:
if self._resched(card):
return self._earlyReviewIvl(card, ease)*86400
else:
return 0
return self._earlyReviewIvl(card, ease)*86400
else:
return self._nextRevIvl(card, ease, fuzz=False)*86400
@ -1330,16 +1337,11 @@ To study outside of the normal schedule, click the Custom Study button below."""
elif ease == 2:
return self._delayForRepeatingGrade(conf, card.left)
elif ease == 4:
# early removal
if not self._resched(card):
return 0
return self._graduatingIvl(card, conf, True, fuzz=False) * 86400
else: # ease == 3
left = card.left%1000 - 1
if left <= 0:
# graduate
if not self._resched(card):
return 0
return self._graduatingIvl(card, conf, False, fuzz=False) * 86400
else:
return self._delayForGrade(conf, left)

View file

@ -627,112 +627,51 @@ def test_filt_keep_lrn_state():
c.load()
assert c.type == c.queue == 1
def test_filt_reschedoff():
# add card
def test_preview():
# add cards
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
d.addNote(f)
c = f.cards()[0]
orig = copy.copy(c)
f2 = d.newNote()
f2['Front'] = "two"
d.addNote(f2)
# cram deck
did = d.decks.newDyn("Cram")
cram = d.decks.get(did)
cram['resched'] = False
d.sched.rebuildDyn(did)
d.reset()
# graduate should return it to new
# grab the first card
c = d.sched.getCard()
ni = d.sched.nextIvl
assert ni(c, 1) == 60
assert ni(c, 2) == (60+600)//2
assert ni(c, 3) == 600
assert ni(c, 4) == 0
assert d.sched.nextIvlStr(c, 4) == "(end)"
d.sched.answerCard(c, 4)
assert c.queue == c.type == 0
# undue reviews should also be unaffected
c.ivl = 100
c.type = 2
c.queue = 2
c.due = d.sched.today + 25
c.factor = STARTING_FACTOR
c.flush()
cardcopy = copy.copy(c)
d.sched.rebuildDyn(did)
d.reset()
assert d.sched.answerButtons(c) == 2
assert d.sched.nextIvl(c, 1) == d.sched._previewDelay
assert d.sched.nextIvl(c, 2) == 0
# failing it will push its due time back
due = c.due
d.sched.answerCard(c, 1)
assert c.due != due
# the other card should come next
c2 = d.sched.getCard()
assert c2.id != c.id
# passing it will remove it
d.sched.answerCard(c2, 2)
# the other card should appear again
c = d.sched.getCard()
assert ni(c, 1) == 600
assert ni(c, 2) == 0
assert ni(c, 3) == 0
assert c.id == orig.id
# remove it
d.sched.answerCard(c, 2)
assert c.ivl == 100
assert c.due == d.sched.today + 25
# check failure too
c = cardcopy
c.flush()
d.sched.rebuildDyn(did)
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
d.sched.emptyDyn(did)
c.load()
assert c.ivl == 100
assert c.due == d.sched.today + 25
# fail+grad early
c = cardcopy
c.flush()
d.sched.rebuildDyn(did)
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
d.sched.answerCard(c, 4)
d.sched.emptyDyn(did)
c.load()
assert c.ivl == 100
assert c.due == d.sched.today + 25
# due cards - pass
c = cardcopy
c.due = -25
c.flush()
d.sched.rebuildDyn(did)
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 3)
d.sched.emptyDyn(did)
c.load()
assert c.ivl == 100
assert c.due == -25
# fail
c = cardcopy
c.due = -25
c.flush()
d.sched.rebuildDyn(did)
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
d.sched.emptyDyn(did)
c.load()
assert c.ivl == 100
assert c.due == -25
# fail with normal grad
c = cardcopy
c.due = -25
c.flush()
d.sched.rebuildDyn(did)
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
d.sched.answerCard(c, 4)
c.load()
assert c.ivl == 100
assert c.due == -25
# lapsed card pulled into cram
# d.sched._cardConf(c)['lapse']['mult']=0.5
# d.sched.answerCard(c, 1)
# d.sched.rebuildDyn(did)
# d.reset()
# c = d.sched.getCard()
# d.sched.answerCard(c, 2)
# print c.__dict__
# ensure it's in the same state as it started
assert c.queue == 0
assert c.reps == 0
assert c.type == 0
def test_ordcycle():
d = getEmptyCol()
@ -1067,56 +1006,3 @@ def test_failmult():
# so the card is reset to new
d.sched.answerCard(c, 1)
assert c.ivl == 1
# answering a new card with scheduling off should not change
# the original position
def test_preview_order():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "oneone"
d.addNote(f)
f = d.newNote()
f['Front'] = "twotwo"
d.addNote(f)
assert d.getCard(d.findCards("oneone")[0]).due == 1
assert d.getCard(d.findCards("twotwo")[0]).due == 2
did = d.decks.newDyn("Cram")
cram = d.decks.get(did)
cram['resched'] = False
d.sched.rebuildDyn(did)
d.reset()
c = d.sched.getCard()
assert "oneone" in c.q()
d.sched.answerCard(c, 3)
d.sched.answerCard(c, 3)
assert c.due == 1
# answering a due review with scheduling off should not change scheduling
def test_reviews_reschedoff():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
d.addNote(f)
c = f.cards()[0]
c.ivl = 100
c.queue = c.type = 2
c.due = d.sched.today
c.factor = 2500
c.flush()
did = d.decks.newDyn("Cram")
cram = d.decks.get(did)
cram['resched'] = False
d.sched.rebuildDyn(did)
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 4)
assert c.ivl == 100
assert c.due == d.sched.today
assert c.factor == 2500