Looks like the Great Firewall or something like it is preventing you from completely loading www.skritter.com because it is hosted on Google App Engine, which is periodically blocked. Try instead our mirror:
This might also be caused by an internet filter, such as SafeEyes. If you have such a filter installed, try adding appspot.com to the list of allowed domains.
This document gives an overview of the many things that need to be done to implement a fully functional Skritter client.
Note: If you're interested in embarking on such an adventure, you should probably contact us. We're still building all the systems required, and the information presented here is probably incomplete.
First thing to do is fetch Items using the Items Endpoint. These Items represent what the User is studying. You've got basically two options here:
Fetch a small (10-100) batch of them using the 'next' ordering and periodically get more as needed. This method is relatively straightforward since you don't have to worry about synchronization. However, this system does not allow for fully offline study.
Use the Batch System to fetch all Items at once. Occasionally check the server for changes made from other clients. This is much more complicated to get exactly right, but in the end you get a more powerful client.
Only required if you do 'Full Account Sync'. Feel free to roll your own algorithm to order them for study. We suggest these features:
Here's some example code from the iOS app for calculating 'readiness', which is then used to order prompts from lowest to highest.
double ageWithLongShotDeprioritization( BOOL deprioritizeLongShots, double t, double last, double next, BOOL singleCharacter, NSString *key) { if(!last && next - t > 600) return SK_USER_ITEM_AGE_SPACED; if(!last || next - last == 1) return SK_USER_ITEM_AGE_NEW; // Makes equally due words come up before the characters contained within them double lengthPenalty = singleCharacter ? -0.02 : 0; // sanity check if (t < last) return 0.0; // Calculate rtd here to take into account spacing, not to use given rtd. double seenAgo = t - last; double rtd = next - last; double readiness = seenAgo / rtd; // If readiness is negative, it's an error, unless rtd is <= 1, in which case // it's been new and has been spaced, so we can just let it have a // negative readiness and never come up. if(readiness < 0 && rtd > 1) { DLog(@"Warning: last %.2f later than next %.2f for %@ (now is %.2f)", last, next, key, now()); readiness = 0.7; } // Tweak readiness to favor items that haven't been seen in a while at really // low readinesses, so that new items don't crowd them out and you see everything // when studying a small set of words. // I want it to grow logarithmically such that it'll give a very small but non- // negligible change to items that are older than a few minutes, while giving // about .50 boost after a year to something maxed out (10 years). // The boost should drop off quickly for items that have some readiness themselves. if(readiness > 0.0 && seenAgo > 9000.0) { double dayBonus = 1.0; double ageBonus = 0.1 * log( dayBonus + (dayBonus * dayBonus * seenAgo) * DAYS_IN_A_SECOND); double readiness2 = readiness > 1.0 ? 0.0 : 1.0 - readiness; ageBonus *= readiness2 * readiness2; // Less bonus if ready readiness += ageBonus; } // Don't let anything long-term be more ready than 250%; deprioritize the really // overdue long shots so that the more doable stuff comes first (down to 150%). if(deprioritizeLongShots) { if(readiness > 2.5 && rtd > 600.0) { if(readiness > 20.0) readiness = 1.5; // Deprioritize only down to 150% else readiness = 3.5 - pow(readiness * 0.4, 0.33333); } if(lengthPenalty && readiness > 1) { readiness = pow(readiness, 1 + lengthPenalty); } } return readiness; }
There are many things to remember to control what is shown when for a Skritter prompt. This details what to show for the four prompt types (writing, tone, reading, definition); the two languages (Chinese and Japanese); single characters vs. multi-character words; various settings (hidden reading, hidden definition, raw squigs, colored tones, eccentric flavor, parts studied, styles studied, Heisig keywords, grading buttons, and tone buttons); and prompt states (whether there are sentences, mnemonics, custom definitions, audio files, uncommon characters, kanji-less writings, simp/trad differences, new words, and not-yet-correct words). It gets complicated.
Rather than discuss what happens when you can combine prompt types as on the Skritter web client (writing + tone together or reading + definition together when both related Items are due), we suggest that you do not attempt it, as although it is more efficient for the learner, it is probably not worth the complexity for the developer. If you do want to try it, just pay attention to the Skritter web client's behavior when deciding what to show/hide/obscure and when.
If you notice inconsistencies or mistakes below, let us know. This is as much to document how it should work so we ourselves remember as it is for guiding you in building clients.
When the prompt is first displayed, show:
Do not show, but make quickly accessible without having to show the answer:
Show when show button is pressed:
When erase button is pressed, try to reset:
When the character is finished, show:
Important differences from writing prompts:
When not doing typing for reading prompts, reading and definition prompts are very similar to each other. Some notes:
Tone, non-typing reading, and definition prompts will normally only be scored 1 ("forgot", or "don't know" for a word which has never yet been answered correctly) or 3 ("got it"). The User may use grading buttons to mark scores of 2 ("so-so") and 4 ("too easy"), but these won't be auto-assigned except for in writing prompts.
For writing prompts, characters should automatically mark themselves wrong after more than totalStrokes / 4 + 3 strokes are rejected, where totalStrokes is the maximum number of strokes in the longest stroke order for that character. Optionally, characters should mark themselves so-so 2 strokes before that threshold. More than three rejected strokes in a row should trigger a hint. Explicitly asking for a hint (like by a single tap) should increment the number of wrong strokes by 1/4 of the allowed wrong stroke threshold, rounded down. You can play around with these numbers, but they're hard to tune so that everyone is pleased.
When doing a multiple-character prompt (writing or tone), you generate scores for both the word-level item and each character-level item. Here's an example of how you determine the word-level score based on each character-level score:
wrongCount = len([prompt for prompt in characterPrompts if prompt.score == 1]) // If there are just 2 chars, and you get one wrong, you got it wrong. if wrongCount == 1 and len(characterPrompts) == 2 wordScore = 1 // If you get 2+ chars wrong, you got it wrong. elif wrongCount >= 2 wordScore = 1 // Round it down; decided this was better than rounding up since // most of the time it will turn into a 2, which is good. else total = sum([prompt.score for prompt in characterPrompts]) average = total / len(characterPrompts) wordScore = math.floor(scoreFloat)
Note that if any of the wrong characters are not also added for study on their own, then the whole word should get marked wrong. An Item is being studied on its own if it contains any values in its vocabIds property.
For the iOS app, we implemented a more complex mechanism whereby character-level items that are less than 70% due are not submitted when marked wrong when the word is marked wrong, on the hypothesis that the user forgot only which character it was in that word, not how to write that character on its own. This turns out to do the right thing most of the time and prevent you from needing to provide manual char- vs. word-score override buttons. Example implementation:
- (void)decideWhetherToSubmitSubUI:(SKUserItem *)subUI { // Add a flag saying whether to actually submit this review. // (If we get the prompt wrong, but we know the character much better than the word, // then we'll say that we didn't really fairly test the character, // so we'll just ignore the review instead of submitting it.) // If the char is at least 70% as ready as the word is, then we'll count it wrong. // If the sub-UI is not active for study by itself // (most common in Japanese, still common in Chinese), then the user doesn't // really care about whether it comes up for review on its own, so we'll not skip it. subUI.skipThisReview = (subUI.scoreThisReview == 1 && self.ui.scoreThisReview == 1 && subUI.style != SK_NO_STYLE && [subUI ageWithLongShotDeprioritization:NO] < 0.7 * [self.ui ageWithLongShotDeprioritization:NO]); if( subUI.scoreThisReview == 1 && self.lang == SK_ZH && self.part == SK_TONE && ([subUI.base isEqualToString:@"一"] || [subUI.base isEqualToString:@"不"])) { // These two have tone sandhi; everyone knows their real tone. subUI.skipThisReview = YES; }
It is also helpful to skip submission of tone prompt reviews when the answered tone is correct for the character itself, but wrong in the context of the word.
See the Scheduling page for details on how to calculate the new interval for an Item after it has been studied.
You will need SRS Config Entity data to do the scheduling properly. Every hundred reviews or so, you should update these entities from the server to get the latest values.
Items should also be spaced. If you study an Item for a given Vocab, all other Items that are for that share the same 'base' should have their 'next' property pushed back so there's some minimum distance. An Item's 'base' is the second value in the Item's id if you divide by hyphens. So if an Item's id is "ja-日本-0", then the base is "日本". The server spaces Items by one fifth of their current interval (minimum ten minutes), or twelve hours if it's a new Item. If the Item is already scheduled after the calculated spacing distance, the Item's next value does not change. Spacing Items ensures Items for the same Vocab don't overlap one another too much, and parts of a Vocab a User is having trouble with get priority. Clients should try to match the spacing the server does, though it does not need to be exact.
First, create the Review objects which will be sent to the server to report what the User has studied. There should be one for every Item involved (ie if you study a multi-character word for any part other than the definition, Items for each Vocab listed in the word's containedVocabIds should also have their own separate Review objects). Most properties should be self explanatory, but here are some other things you should know:
Then, update the Item objects:
Note: Be wary of system time changes. These happen every so often and will throw off your time tracking. Make sure your time tracking values are not made negative because of these changes.
Use the Reviews Endpoint to save your Reviews to the server. Aside from the Review objects themselves, you'll also need to provide the date the Reviews you are submitting happened. Skritter gets the date for the User's current timezone, shifting time by four hours so that the hours between midnight and 4AM still counts toward the previous day. Get the current local time from your client and use that to determine the date, doing the same four hour time shift calculation.
You don't necessarily have to save these Reviews to the server immediately. Batches of a dozen or more are fine, and the server can optimize updating things such as SRS values and the day's progress stats the larger the batches are.
If your client supports offline studying, and you have more than one day's Reviews built up, save them one day at a time with at least ten seconds between each batch submission. The Review processing system is built for the Flash client, which saves prompts shortly after they are generated. This is set to be changed in the future to be more flexible and not requiring such careful measures, but for the time being be careful in this scenario.
Right now, you cannot save overall progress data to days before the last update to a person's account. So if you have reviews from Monday, but the user has already saved reviews to her account on Tuesday, you cannot save those overall progress stats to Monday. You will need to set it to a more recent date. Individual Items will still retain their proper values (saying exactly when they were last studied) but overall progress stats will be applied to the wrong date.
Invariably, data gets out of sync. When you try to save Reviews based on old data, the server will recognize this and revert or recalculate the interval on the fly. But it's up to the client to realize when data gets out of sync, and handle them correctly.
First, occasionally poll the Review Errors Endpoint to know which Reviews you submitted didn't go through. If something did go wrong, fetch the Item again from the server and continue on based on what the server decided.
Second, your client should guard itself when other clients update the User's data. There are multiple strategies for doing this:
In progress.