#!/usr/local/bin/perl -w # qz - generic quiz script # # Copyright (C) 1996 by John J. Chew, III # All Rights Reserved. ## table of contents # libraries # naming convention # global constants # global variables # parameters that a configuration file can safely override # subroutines # main code ## libraries unshift (@INC, "$ENV{'HOME'}/lib/perl") if defined $ENV{'HOME'}; require 'getopts.pl'; ## naming convention # # gkName global constant # gName global variable # lName local variable # tName temporary variable (not explicitly scoped, assumed to be volatile) ## global constants # field names in quiz files, number of fields ($gkFQuestion, $gkFAnswer, $gkFRating, $gkFAge, $gkFFlags, $gkFNote, $gkFields) = 0..6; # version number for this script $gkVersion = '2.0.3a3'; ## global variables # true if a prompt needs redisplaying after a TSTP $gAtPrompt = 0; # environment: unknown, unix, dos $gEnvironment = 'unknown'; $_ = $ENV{'PATH'}; if ((!defined $_) && $INC[$#INC] eq ':') { $gEnvironment = 'mac'; $gDBMSuffix = ''; } elsif (/^\//) { $gEnvironment = 'unix'; $gDBMSuffix = '.dir'; } elsif (/^[A-Z]:\\/) { $gEnvironment = 'dos'; $gDBMSuffix = '.db'; } else { die "Unknown environment, please contact jjchew\@math.utoronto.ca\n"; } # chronological list of numbers of questions answered incorrectly @gErrors = (); # map file number to question count @gFileCount = (); # map file number to dirty flag @gFileDirty = (); # map file number to file name @gFileName = (); # number of times we've tried picking a question $gGenTries = 0; # last question asked $gLastQ = undef; # store data for question $n as follows: # $gQData[$gkFields*$n+$gkFQuestion] = question text # $gQData[$gkFields*$n+$gkFAnswer ] = answer text # $gQData[$gkFields*$n+$gkFRating ] = moving mean response time or # '+' . $gMaxRating+1 if new # $gQData[$gkFields*$n+$gkFAge ] = time() of last correct answer # $gQData[$gkFields*$n+$gkFFlags ] = flags: # 'C' if case of answer doesn't matter # 'O' if order of words in answer doesn't matter # $gQData[$gkFields*$n+$gkFNote ] = note to display after answer @gQData = (); # list of question numbers sorted chronologically by last correct answer @gQByAge = (); # map difficulty rating to list of space-separated question numbers @gQByRating = (); # number of questions $gQCount = 0; # number of questions, rounded up to a power of two $gQCount2 = 0; # number of questions answered correctly $gQCorrect = 0; # 1-based ordinal question number of question currently being asked $gQOrdinal = 0; # tricky data structure defined as follows: # # gRT(x) := $gRatingTree[$x]; r(x) := rating of question $x # gRT(0)=r(0); gRT(1)=r(0)+r(1); gRT(2)=r(2); gRT(3)=r(0)+r(1)+r(2)+r(3) # gRT(4)=r(4); gRT(5)=r(4)+r(5); gRT(6)=r(6); gRT(7)=r(0)+...+r(7)... # i.e. gRT(2k)=r(2k); gRT(4k+1)=r(4k)+r(4k+1); gRT(8k+3)=r(8k)+...r(8k+3) # and in general, gRT((2k+1)*2**n-1)=sum i (2k*2**n..(2k+1)*2**n-1) r(i) # # This gives us linear data structure initialization and log n searches # and data structure updates; a big improvement over the old linear search. # # Thanks to my brain for thinking of this. @gRatingTree = (); # set whenever we return from a TSTP $gResumed = 0; # total of all difficulty ratings $gTotalRating = 0; # total time spent answering questions $gTotalTime = 0; # question number to when question was asked in this session @gWhenAsked = (); ## parameters that a configuration file can safely override # maximum difficulty rating in seconds $gMaxRating = 100; # default program $gProgram = 'jjc'; # minimum interval between repeats $gRepeatInterval = 15; # user typing speed in characters per second $gTypingSpeed = 9; %gProgramDef = ( 'hardest', '100;0;0;0', 'oldest', '0;100;0;0', 'random', '0;0;100;0', 'jjc', '30;20;40;10', ); %gProgramHelp = ( 'hardest', 'do all questions from hardest to easiest', 'oldest', 'do all questions from oldest to newest', 'random', 'pick at random, biased toward hard questions', 'jjc', '20% oldest, 20% hardest, 50% random, 10% review', ); ## subroutines # $eof = &AskQ($qNumber) - ask a question # &DoOptL - list file stats # $string = &FormatRating($rating) - format either 'new' or numeric # 0 = &GiveUp($qNumber) - record that user gave up on this question # 0 = &GotIt($qNumber, $time) - record that user correctly answered question # &HandleTSTP() - TSTP signal handler # &LoadData() - load quiz data # &MungeData() - precalculate information useful for running a quiz # &ParseCommandLine() - parse command line # $answer = &ReadLine($prompt, *time, $makelc) - prompt for a line and # update *time, convert to lower case if desired # &SaveData() - save quiz data # &SetRating($qNumber, $newRating) - set question's difficulty rating # &TouchQ($qNumber) - mark a question as dirty # &Usage() - display usage message and die # $eof = &AskQ($qNumber) - ask a question sub AskQ { local($lQ) = @_; die "Oops ($lQ >= $gQCount)\nAborting" if $lQ >= $gQCount; local($lQIndex) = $gkFields*$lQ; local($lQuestion, $lAnswer, $lFlags, $lNote) = @gQData[$lQIndex+$gkFQuestion, $lQIndex+$gkFAnswer, $lQIndex+$gkFFlags, $lQIndex+$gkFNote]; local($_); local($lT) = 0; local($lPrompt) = sprintf("[%d] %s: ", ++$gQOrdinal, $lQuestion); $lNote =~ s/
|

/\n/g; $gGenTries = 0; $gWhenAsked[$lQ] = $gQOrdinal; $lAnswer = "\L$lAnswer" if $lFlags =~ /C/; if ($lFlags =~ /O/) { # don't care about order local(%lAnswer, $lCount, $lFirstQ, %lFound, $lWord); $lCount = 0; for $lWord (split(/\s+/, $lAnswer)) { $lAnswer{$lWord} = 1; $lCount++; } for ($lFirstQ = 1; $lCount > 0; ) { unless ($lFirstQ) { print %lFound ? "Enter another answer, or press return to see them all.\n" : "Press return to see the correct answer, or try again.\n"; } $_ = &ReadLine($lPrompt, *lT, $lFlags =~ /C/); defined $_ ? chop : return 1; $lFirstQ = 0; /\S/ || return &GiveUp($lQ); for $lWord (split(/\s+/)) { $lAnswer{$lWord} ? $lFound{$lWord}++ ? print("You have already entered `$lWord'.\n") : $lCount-- : print("`$lWord' is not correct.\n"); } } } else { # not in unordered mode $_ = &ReadLine($lPrompt, *lT, $lFlags =~ /C/); length($_) ? chop : return 1; $_ eq $lAnswer || return &GiveUp($lQ); } &GotIt($lQ, $lT); } # &DoOptL - list file stats sub DoOptL { $tSolved = $tTotal = $tUnseen = $tUnsolved = 0; for ($tQIndex=$gkFRating; $tQIndex <= $#gQData; $tQIndex += $gkFields) { $tRating = $gQData[$tQIndex]; if ($tRating =~ /^\+/) { $tUnseen++; } elsif ($tRating == $gMaxRating) { $tUnsolved++; } else { $tSolved++; $tTotal += $tRating; } } $tSeen = $tSolved + $tUnsolved; printf "Total: %d\n", $tUnseen+$tSeen; printf "Unseen: %d\n", $tUnseen if $tUnseen; printf "Solved: $tSolved"; printf " (%d%%)", 0.5+100*$tSolved/$tSeen if $tSeen; print "\n"; printf "Unsolved: $tUnsolved"; printf " (%d%%)", 0.5+100*$tUnsolved/$tSeen if $tSeen; print "\n"; printf "Mean solution time: %.1f s\n", $tTotal/$tSolved if $tSolved; printf "Mean difficulty: %.1f s\n", (100*($tUnsolved+$tUnseen)+$tTotal)/($tSeen+$tUnseen) if $tUnsolved || $tUnseen; $t = $gQData[$gQByAge[0]*$gkFields+$gkFAge]; print "Oldest solution: "; if ($t) { $t=time-$t; printf "%d %s\n", $t<60?($t,'s'):($t/=60)<60? ($t,'m'):($t/=60)<24?($t,'h'):($t/24,'d'); } else { print "never\n"; } } # $string = &FormatRating($rating) - format either 'new' or numeric sub FormatRating { local($lR) = @_; $lR =~ s/^\+.*/new/; $lR; } # 0 = &GiveUp($qNumber) - record that user gave up on this question sub GiveUp { local($lQ) = @_; print "$lNote\n" if length($lNote); print "The correct answer is '$gQData[$lQ*$gkFields+$gkFAnswer]'" . " (" . &FormatRating($gQData[$lQ*$gkFields+$gkFRating]) . "-$gMaxRating)\n"; push(@gErrors, $lQ); &SetRating($lQ, $gMaxRating); $gLastQ = $lQ; 0; } # 0 = &GotIt($qNumber, $time) - record that user correctly answered question sub GotIt { local($lQ, $lTime) = @_; local($lOldRating) = $gQData[$gkFields*$lQ+$gkFRating]; local($lNewRating) = int($lOldRating =~ /^\+/ ? $lTime : (1+2*$lOldRating+$lTime)/3); $gTotalTime += $lTime; $lNewRating < 1 ? ($lNewRating = 1) : $lNewRating > $gMaxRating && ($lNewRating = $gMaxRating); print "$lNote\n" if length($lNote); print "Correct." . " (" . &FormatRating($lOldRating) . "-$lNewRating)\n"; $gQCorrect++; # update question fields &SetRating($lQ, $lNewRating); $tQIndex = $lQ * $gkFields + $gkFAge; $tOldAge = $gQData[$tQIndex]; $gQData[$tQIndex] = time(); # update chronological list $tLow = 0; $tHigh = $gQCount; while ($tHigh - $tLow > 1) { $tMid = int(($tLow + $tHigh) / 2); $tComp = $gQData[$gQByAge[$tMid]*$gkFields+$gkFAge] <=> $tOldAge; if ($tComp < 0) { $tLow = $tMid; } elsif ($tComp > 0) { $tHigh = $tMid; } else { $tLow = $tHigh = $tMid; last; } } while ($gQData[$gQByAge[$tLow]*$gkFields+$gkFAge] == $tOldAge && $tLow > 0) { $tLow--; } while ($gQData[$gQByAge[$tHigh]*$gkFields+$gkFAge] == $tOldAge && $tHigh < $gQCount-1) { $tHigh++; } for ($tQ=$tLow; $tQ<=$tHigh; $tQ++) { if ($gQByAge[$tQ] == $lQ) { push(@gQByAge, splice(@gQByAge, $tQ, 1)); last; } } &TouchQ($lQ); $gLastQ = $lQ; 0; } # &HandleTSTP() - TSTP signal handler sub HandleTSTP { $SIG{'TSTP'} = $] >= 5 ? '::HandleTSTP' : 'HandleTSTP'; $stopped_at = time; kill 'STOP', $$; if ($gAtPrompt) { $lStart += time - $stopped_at; print $lPrompt; } $gResumed = 1; } # &LoadData() - load quiz data sub LoadData { $gQCount = 0; $tLast = 0; while (<>) { chop; @tFields = split(/\t/, $_, 100); die "Can't parse line:\n$_\n" ."Expected $gkFields fields, got $#tFields+1.\nAborting" unless $#tFields == $gkFields-1; push(@gQData, @tFields); $gQCount++; if (eof(ARGV)) { push(@gFileCount, $. - $tLast); $tLast = $.; push(@gFileDirty, 0); push(@gFileName, $ARGV); } } close(ARGV); } # &MungeData() - precalculate information useful for running a quiz sub MungeData { # round $gQCount up to a power of two for ($gQCount2 = 1; $gQCount2 < $gQCount; $gQCount2 <<= 1) { } # calculate rating information @gQByRating = ('') x ($gMaxRating + 2); # ($u0,$s0) = times; # DEBUG $gTotalRating = 0; $tQIndex = $gkFRating; for ($tQ=0; $tQ<$gQCount;$tQ++,$tQIndex += $gkFields) { # a fun loop $gTotalRating += $gRatingTree[$tQ] = $tR = $gQData[$tQIndex]; $gQByRating[$tR] .= "$tQ "; next unless $tQ % 2; $t1 = $tQ-1; $gRatingTree[$tQ] += $gRatingTree[$t1]; $t2=1; $t3 = $tQ & (1 + $tQ); while (($t1-=$t2) > $t3) { $t2 += $t2; $gRatingTree[$tQ] += $gRatingTree[$t1]; } } # ($u1,$s1) = times; printf "time: %5.2f %5.2f\n", $u1-$u0, $s1-$s0; # DEBUG # build chronological list $#gQByAge = $gQCount - 1; $tQ = $gQCount; $tQIndex = $tQ * $gkFields + $gkFAge; while ($tQ-- > 0) { $gQByAge[$tQ] = $tQIndex -= $gkFields; } @gQByAge = sort { $gQData[$a] <=> $gQData[$b]; } @gQByAge; for $tQ (@gQByAge) { $tQ = int($tQ/$gkFields); } # build last-asked list @gWhenAsked = (-$gRepeatInterval) x $gQCount; } # &ParseCommandLine() - parse command line sub ParseCommandLine { # initialize variables to help avoid -w warnings $opt_a = $opt_d = $opt_h = $opt_i = $opt_l = $opt_n = $opt_r = $opt_s = $opt_v = $opt_A = $opt_L = 0; $opt_f = $opt_n = $opt_p = $opt_r = undef; &Getopts('adf:hiln:p:r:svAL-:') || &Usage; # -: avoids warning in getopts.pl # check for incompatible arguments ($opt_a+$opt_d+$opt_l+$opt_A+$opt_L > 1) && &Usage; # check argument count ( $opt_a ? $#ARGV != 4 : $opt_d ? $#ARGV != 3 : $opt_l ? $#ARGV < 0 : $opt_A ? ($#ARGV < 0 || $#ARGV > 1) : $opt_L ? $#ARGV < 0 : 0 ) && &Usage; # -a: handled in main code # -d: handled in main code # -f file: obsolete if (defined $opt_f) { warn "The `-f' flag is no longer supported; please just list quiz files on the command line instead.\n"; push(@ARGV, $opt_f); } # -h: display help $opt_h && &Usage; # -i: obsolete if ($opt_i) { warn "The `-i' flag is no longer supported; please use the 'O' question flag instead.\n"; &Usage; } # -l: handled in main code # -n noq: obsolete if (defined $opt_n) { warn "The `-n noq' flag is no longer supported; please press control-D to exit qz instead.\n"; &Usage; } # -p: specify question selection program $gProgram = $opt_p if defined $opt_p; # -r mri: obsolete if (defined $opt_r) { warn "The `-r mri' flag is no longer supported; please override $gRepeatInterval in .qzrc instead.\n"; &Usage; } # -s: obsolete if ($opt_s) { warn "The `-s' flag is no longer supported; please use `-m hardest' instead.\n"; &Usage; } # -v: display version if ($opt_v) { print "qz version $gkVersion\n"; exit 0; } # -A: handled in main code # -L: handled in main code # read configuration file @tFiles = (); push(@tFiles, "$ENV{'HOME'}/.qzrc") if defined $ENV{'HOME'}; push(@tFiles, '.qzrc'); for $tFile (@tFiles) { if (-f $tFile && -r _) { do $tFile; last; } } # make sure -p operand (as possibly overriden by .qzrc) is acceptable unless (defined $gProgramDef{$gProgram}) { print STDERR "Undefined program: `$gProgram'\n\n"; &Usage; } } # $answer = &ReadLine($prompt, *time, $makelc) - prompt for a line and # update *time, convert to lower case if desired sub ReadLine { local($lPrompt, *lTotalTime, $lMakeLC) = @_; local($_, $lDuration, $lNote, $lStart, $lTime); $gResumed = 0; $gAtPrompt = 1; print $lPrompt; $lTime = 0; $lStart = time; while (1) { $_ = ; $gAtPrompt = 0; if ($gResumed) { $gResumed = 0; next unless defined $_; } defined $_ || return $_; $lDuration = time - $lStart - (length)/$gTypingSpeed; last if $_ eq ''; if (s/^;exit//i) { return undef; } elsif (s/^;//) { print "(", join(',',eval($_)), ")\n";} elsif ($gEnvironment ne 'mac' && s/^!//) { system $_; } elsif ((defined $gLastQ) && s/^\+//) { chop; ($lNote = ($gQData[$gLastQ*$gkFields+$gkFNote].=$_)) =~ s/
|

/\n/g; print "$lNote\n"; } else { $lTime += $lDuration; last; } $lStart = time; $gAtPrompt = 1; print $lPrompt; } $lTotalTime += $lTime; s/^[ \t]*//; s/[ \t]+$//; $lMakeLC ? "\L$_" : $_; } # &SaveData() - save quiz data sub SaveData { $tQIndex = 0; for $tFile (0..$#gFileDirty) { if ($gFileDirty[$tFile]) { $tName = $gFileName[$tFile]; $tNewName = $tName; $tNewName =~ s/\.[^.]*$//; $tNewName .= '.new'; open(FILE, ">$tNewName") || die "Can't create $tNewName\nAborting"; $tCount = $gFileCount[$tFile]; $tFormat = join("\t", ('%s') x $gkFields) . "\n"; while ($tCount-- > 0) { (printf FILE $tFormat, @gQData[$tQIndex..$tQIndex+$gkFields-1]) || die "Error writing to $tNewName ($!)\nAborting"; $tQIndex += $gkFields; } close(FILE) || die "Error closing $tNewName\nAborting"; rename($tNewName, $tName) || die "Error renaming $tNewName to $tName\nAborting"; $gFileDirty[$tFile] = 0; } else { $tQIndex += $gkFields*$gFileCount[$tFile]; } } } # &SetRating($qNumber, $newRating) - set question's difficulty rating sub SetRating { local($lQ, $lR) = @_; $tQIndex = $gkFields*$lQ + $gkFRating; $tR = $gQData[$tQIndex]; if ($lR ne $tR) { $gQByRating[$tR] =~ s/\b$lQ // || die "Panic (tR=$tR;g[]=$gQByRating[$tR])" ; $gQByRating[$lR] .= "$lQ "; $gQData[$tQIndex] = $lR; &TouchQ($lQ); $tDiff = $lR - $tR; $gTotalRating += $tDiff; while ($lQ < $gQCount) { # ELFS: prove that this works $gRatingTree[$lQ] += $tDiff; $lQ |= $lQ+1; } } } # &TouchQ($qNumber) - mark a question as dirty sub TouchQ { for ($tFile = $tQ = 0; $tQ <= $_[0]; $tQ += $gFileCount[$tFile++]) { die "Internal error: &TouchQ($_[0])\nAborting" if $tFile > $#gFileCount; } $gFileDirty[--$tFile] = 1;; } # &Usage() - display usage message and die sub Usage { print STDERR "Usage:\n". " To see what version of qz this is: qz -v\n". " To see this message again: qz -h\n". " To run a quiz: qz [-p program] [file.qz...]\n"; print STDERR " (Quiz files default to all the ones in the current directory.)\n"; printf STDERR " %-10s %s\n", 'Program', 'Description'; for $tP (sort keys %gProgramHelp) { printf STDERR " %-10s %s\n", $tP, $gProgramHelp{$tP}; } printf STDERR " To add a question or create a file:\n". " qz -a 'question' 'answer' flags 'note' file.qz\n". " (flags: C=case-insensitive O=word-order-insensitive CO=both)\n". " To add a file full of questions: qz -A [question-file] file.qz\n". " (Line format: question tab answer tab flags tab note)\n". " To delete a question: qz -d 'question' file.qz\n". " To list all questions: qz -l file1.qz [file2.qz...]\n". " To list stats about questions: qz -L file1.qz [file2.qz...]\n"; exit 1; } ## main code if ($gEnvironment eq 'mac') { print "\n\245\245\245 qz $gkVersion \245\245\245\n"; @argv = split(/\s+/, $0); shift @argv; unshift(@ARGV, @argv); print join(' ', 'qz', @ARGV), "\n"; } srand; &ParseCommandLine; # To add a question or create a file: # qz -a 'question' 'answer' flags 'note' file.qz if ($opt_a) { local(@lFields) = (); $lFields[$gkFQuestion] = shift @ARGV; $lFields[$gkFAnswer] = shift @ARGV; $lFields[$gkFRating] = '+' . ($gMaxRating+1); $lFields[$gkFAge] = 0; $lFields[$gkFFlags] = shift @ARGV; $lFields[$gkFNote] = shift @ARGV; &LoadData; for ($tQIndex = $gkFQuestion; $tQIndex <= $#gQData; $tQIndex += $gkFields) { last if $lFields[$gkFQuestion] eq $gQData[$tQIndex]; } $gFileCount[0]++ if $tQIndex > $#gQData; $tQIndex -= $gkFQuestion; @gQData[$tQIndex..$tQIndex+$gkFields-1] = @lFields; &TouchQ(0); &SaveData; } # To delete a question: qz -d 'question' file.qz\n". elsif ($opt_d) { local($lQuestion) = shift @ARGV; &LoadData; for ($tQIndex = $gkFQuestion; $tQIndex <= $#gQData; $tQIndex += $gkFields) { last if $lQuestion eq $gQData[$tQIndex]; } die "No such question: '$lQuestion'\n" unless $tQIndex <= $#gQData; $gFileCount[0]--; $tQIndex -= $gkFQuestion; splice(@gQData, $tQIndex, $gkFields); &TouchQ(0); &SaveData; } # To add a file full of questions: qz -A [question-file] file.qz # (Line format: question tab answer tab flags tab note) elsif ($opt_A) { local($lQFile) = $#ARGV > 0 ? shift @ARGV : '-'; local(@tFields) = (); $tFields[$gkFRating] = '+' . ($gMaxRating+1); $tFields[$gkFAge] = 0; &LoadData; @ARGV = $lQFile; while () { chop; @_ = split(/\t/); die "Wrong number of fields in this line:\n$_\nAborting" unless $#_ == $gkFields - 1; $tFields[$gkFQuestion] = $_[0]; $tFields[$gkFAnswer] = $_[1]; $tFields[$gkFFlags] = $_[2]; $tFields[$gkFNote] = $_[3]; for ($tQIndex = $gkFQuestion; $tQIndex <= $#gQData; $tQIndex += $gkFields) { last if $_[0] eq $gQData[$tQIndex]; } $gFileCount[0]++ if $tQIndex > $#gQData; $tQIndex -= $gkFQuestion; @gQData[$tQIndex..$tQIndex+$gkFields-1] = @tFields; } &TouchQ(0); &SaveData; } # To list all questions: qz -l file1.qz [file2.qz...]\n". elsif ($opt_l) { &LoadData; @tWidths = (0) x $gkFields; $tFieldIndex = 0; for $tField (@gQData) { $tLength = ($tFieldIndex == $gkFRating && $tField =~ /^\+/) ? 3 : length($tField); $tWidths[$tFieldIndex] = $tLength if $tWidths[$tFieldIndex] < $tLength; $tFieldIndex = 0 if ++$tFieldIndex == $gkFields; } $tFormat = '%' . $tWidths[$gkFRating ] .'s '. '%-' . $tWidths[$gkFQuestion] .'.'. $tWidths[$gkFQuestion] . 's '. '%-' . $tWidths[$gkFAnswer ] .'.'. $tWidths[$gkFAnswer ] . 's '; $tSpace2 = ' ' x ($tWidths[$gkFRating] + $tWidths[$gkFQuestion] + 13); for $tQ (sort { $gQData[$gkFRating+$gkFields*$b] <=> $gQData[$gkFRating+$gkFields*$a] || $gQData[$gkFAge+$gkFields*$b] <=> $gQData[$gkFAge+$gkFields*$a] } 0..$gQCount-1) { $tQIndex = $tQ * $gkFields; if ($gQData[$_ = $tQIndex + $gkFAge]) { @tData = localtime($gQData[$_]); printf "%04d-%02d-%02d ", $tData[5]+1900, $tData[4]+1, $tData[3]; } else { print "-not-seen- "; } printf $tFormat, &FormatRating($gQData[$tQIndex + $gkFRating]), $gQData[$tQIndex + $gkFQuestion], $gQData[$tQIndex + $gkFAnswer]; print "\n"; $_ = $gQData[$tQIndex + $gkFNote]; print "$tSpace2$_\n" if length($_); } } # To list stats about questions: qz -L file1.qz [file2.qz...]\n". elsif ($opt_L) { &LoadData; &MungeData; &DoOptL; } # actually run a quiz else { # open(PIPE, "|fmt"); select(PIPE); for $t (sort keys %main::) { print "$t\n"; } exit 0; # DEBUG $SIG{'TSTP'} = $] >= 5 ? '::HandleTSTP' : 'HandleTSTP' unless $gEnvironment eq 'mac'; local(@lErrors, $lAlg, @lAlgProbs, $gQOrdinal, $gQCorrect, $lQ, $lRating, @lRepeats, %lRepeats, $lSessionStart); $lSessionStart = time; @ARGV = <*.qz> if $#ARGV == -1; &LoadData; &MungeData; if (time - $lSessionStart > 2) { print "Press return to begin.\n"; $_ = ; } # normalise algorithm distribution @lAlgProbs = split(/;/, $gProgramDef{$gProgram}, 4); $tS = 0; for $t (@lAlgProbs) { $tS += $t; } for $t (@lAlgProbs) { $t /= $tS; } @lErrors = @lRepeats = %lRepeats = (); $gQCorrect = $gQOrdinal = 0; $gTotalTime = 0; $lSessionStart = time; ask:while (1) { # pick an algorithm $t=rand; for ($lAlg=0; $t>0; $t-=$lAlgProbs[$lAlg++]) { } $lAlg--; # print "(" . ('hard', 'old', 'random', 'review')[$lAlg] . ")\n"; # DEBUG # pick a question if ($lAlg == 0) { # hardest for ($lRating = $#gQByRating; $lRating >= 0; $lRating--) { next unless length($gQByRating[$lRating]); @tQ = split(/ /, $gQByRating[$lRating]); for $tQ (@tQ) { if ($gQOrdinal - $gWhenAsked[$tQ] >= $gRepeatInterval) { &AskQ($tQ) ? last ask : next ask; } } } print "No more questions available.\n"; last ask; } elsif ($lAlg == 1) { # oldest for $lQ (@gQByAge) { if ($gQOrdinal - $gWhenAsked[$lQ] >= $gRepeatInterval) { &AskQ($lQ) ? last ask : next ask; } } print "No more questions available.\n"; last ask; } elsif ($lAlg == 2) { # random $lQ = 0; $tWidth = $gQCount2; $tR = rand($gTotalRating); while ($tWidth > 1) { $tWidth >>= 1; $tMid = $lQ + $tWidth; if ($tMid <= $gQCount && $tR >= $gRatingTree[$tMid-1]) { $tR -= $gRatingTree[$tMid-1]; $lQ = $tMid; } } if ($gQOrdinal - $gWhenAsked[$lQ] >= $gRepeatInterval) { &AskQ($lQ) ? last ask : next ask; } else { $gGenTries++; next ask; } } elsif ($lAlg == 3) { # review if ($#gErrors >= 0 && $gQOrdinal - $gWhenAsked[$gErrors[0]] >= $gRepeatInterval) { &AskQ(shift @gErrors) ? last ask : next ask; } else { $gGenTries++; next ask; } } } continue { if ($gGenTries >= 20) { print "Giving up after $gGenTries tries at picking a question.\n"; last; } } &SaveData; $gQOrdinal--; printf "\nYou answered %d question%s correctly of %d", $gQCorrect, $gQCorrect == 1 ? '' : 's', $gQOrdinal; printf " (%.1f%%)", 100 * $gQCorrect/$gQOrdinal if $gQOrdinal; print ".\n"; printf "You took on average %.1f seconds to answer correctly.\n", $gTotalTime/$gQCorrect if $gQCorrect; print "Congratulations!\n" if $gQOrdinal && $gQCorrect/$gQOrdinal > 0.9; $elapsed = time - $lSessionStart; printf "Elapsed time: %d:%02d:%02d\n", int($elapsed/3600), int($elapsed/60) % 60, $elapsed % 60; print "\nCurrent statistics for this question set:\n"; &DoOptL; } 0;