mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 07:22:23 -04:00
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:
parent
575f61c384
commit
ba87fc7736
2 changed files with 110 additions and 222 deletions
154
anki/sched.py
154
anki/sched.py
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue