The Core Loop: How an Answer Becomes Progress
In yczz/oc-english, the important event is not the moment a learner taps an option; it is the state change that follows. This chapter traces that change from a practice card, across the frontend/backend boundary, into a saved profile, and then into the conditions for an exam and a new reward. The result is a concrete way to understand what the application records, what it leaves unchanged, and why its points loop is more than a number printed on a screen.
The distinction between a selection and a committed result is easy to miss because both happen quickly. A learner sees a card, chooses an option, and receives feedback. Underneath, the application has to decide whether the answer was correct, whether the associated word is new to the unit, whether points should be added, and whether the unit has reached the exam gate. Those decisions belong to different layers, but they form one user experience.
A click has two lives
The first life of an answer is local to the interface. The practice screen renders a prompt, optional listening controls, and four buttons. When the learner clicks one, the frontend compares the chosen position with the question’s correct-answer position. It also locks the card so repeated clicks cannot submit several answers while the first request is being handled. At this point, the interface knows what the learner selected, but it has not yet established the durable result.
The second life crosses into the application’s command layer. The frontend sends the book, unit, card identifier, and a Boolean result—correct or incorrect—to the Tauri API wrapper. That wrapper is deliberately thin: it passes the request to the Rust command with the expected names and waits for a result. It does not decide how many points an answer deserves or edit the stored list of practiced words on its own.
Rust receives the request while holding the current profile behind the application state’s mutex. A mutex is a way to ensure that only one operation at a time mutates that shared profile. The command locates the requested book and unit, then applies the rules. If the answer is wrong, the result is simple: no points are awarded, and no word is added to the unit’s practiced-word list. The learner still gets feedback in the interface, but the profile does not claim progress that the rule did not authorize.
If the answer is correct, the command awards two points and increases the profile’s point balance. For a word card or a listening card, the card identifier contains the word. The command checks that the word belongs to the selected unit before recording it. It then adds the word only if it is not already present in that unit’s progress. This uniqueness rule matters because the practice set contains two cards for each word: one asks for meaning and one asks the learner to identify a spoken word. Both can be correct and both can earn the small practice reward, but the unit should not count the same word twice toward completion.
That gives the result three distinct meanings:
- Correctness says whether this particular selection matched the card’s answer.
- Progress says whether the word has ever been recorded as successfully practiced for this unit.
- Reward says how many points this answer is allowed to add.
They are related, but they are not interchangeable. A repeated correct card can be a successful interaction and still add no new word to the progress list. Conversely, a wrong answer can be useful feedback for the learner while producing no stored progress and no points. Keeping these outcomes separate prevents the application from treating every click as equivalent.
This separation also defines what partial progress means. The practice screen has temporary state: which card is on screen, whether the current card is locked, and how many cards the learner has moved through. The profile has durable state: the words recorded for a particular book and unit, the point balance, and later the exam history. A learner can therefore make meaningful progress without the application pretending that moving a progress bar is the same as learning a word. The persistent unit record is updated when the command accepts a correct word card, not when the learner merely opens the unit or reaches its final card.
The book-and-unit key matters for the same reason. Progress is attached to the learning context in which it was earned. If the same English word appears in two different units, the source model does not collapse those contexts into one global checkbox; each unit can evaluate its own word list. That keeps the exam gate local to the selected unit. It also makes the unit view interpretable: the displayed count answers “how many words in this unit have been recorded?” rather than “how many times has this profile clicked a word anywhere?”
The boundary protects the economy as well. If the browser-facing code could award points by changing its own displayed total, a learner could see a reward without the profile having accepted a corresponding event. In the current design, the command calculates the award, changes the profile, and returns the amount that the interface mirrors. The same authority pattern is applied to later actions such as purchases. A button can be enabled because the page believes an item is affordable, but the command still checks the price and inventory before committing it. The visible interface is a guide to possible actions; the durable state is governed at the boundary.
There is a cost to this arrangement. A local command must receive enough information to identify the book, unit, and card, and the frontend must keep its request shape aligned with the Rust side. A mismatch can break an otherwise sensible interaction. But that cost buys a clean distinction between an interaction that was attempted and a state change the application is willing to remember. For a learning product, that distinction is the difference between animation and progression.
The full transaction looks like this:
The diagram’s most important boundary is the direction of authority. The interface decides which button the learner pressed and displays the response. The command decides what that response means for the profile. After the mutation, the profile is serialized to the local JSON save. This arrangement also explains why the frontend can update its point display immediately without becoming the ultimate owner of the rule: it is reflecting the result returned by the command, whose mutation has already been saved.
There is a practical failure path here as well. A learner can answer incorrectly, see the correct option identified, and continue through the practice cards. The system will not quietly convert that mistake into completion. If the word has not been recorded by a later correct card, the unit remains short of the condition required for an exam. The product therefore gives immediate feedback without weakening its durable definition of progress.
From a word record to an exam gate
The unit gate is built from the unique word records, not from the number of cards a learner has clicked. The application compares the unit’s word list with the practiced-word list stored in the profile. Only when every word is present does the unit become complete. The frontend then renders the exam action as available; before that, it keeps the action disabled and explains that practice must be finished first.
Suppose an early unit contains the word pencil. A correct meaning card for pencil can add the word to the unit’s progress and award two points. A later listening card for the same word can award another two points if answered correctly, but it does not create a second pencil entry. If the learner answers both cards incorrectly, neither the points balance nor the practiced-word list changes. The difference is small at the level of one word, but it is what keeps a unit with repeated question forms from appearing complete merely because the learner moved through a long screen.
The gate has two useful consequences. First, it makes preparation visible: a unit view can show how many distinct words have been practiced. Second, it gives the exam a defined prerequisite. The exam is not available simply because a learner opened a unit or accumulated enough points elsewhere. It becomes available because the selected unit’s own word coverage has reached the required state.
The exam then introduces a second kind of proof. Its paper contains 15 questions, divided into eight word questions, four listening questions, and three grammar questions. A score of at least 12 out of 15 passes. Passing changes the unit’s stored status and can unlock a larger point reward, but the exam does not erase the practice record. The application retains the best score and the number of examinations taken, so a later attempt can be interpreted against earlier performance.
The reward rules make the difference between a first milestone and a repeated attempt explicit. The first passed exam awards 100 points. A later passed exam awards 10 points for each question by which the learner improves the stored best score. If the learner repeats the same best result, the retake awards nothing. A learner who moves from 12 correct answers to 14 can therefore receive a smaller improvement reward; a learner who repeats 14 receives the satisfaction of a stable record but no additional points; a failed attempt changes neither the pass flag nor the reward balance, although the attempt is still counted in the exam history.
This makes the exam a state transition with several possible outcomes rather than a single win-or-lose label. Before the first pass, the unit is incomplete or unpassed. A passing score changes the pass flag and establishes a best score. A later score can leave the pass flag unchanged while improving the best score, or leave both unchanged while increasing the number of attempts. A failing retake can likewise add history without adding points. The application can tell these cases apart because it stores more than the latest number.
This is not just an economy table. It changes the next decision available to the learner. Practice gives a reason to answer the next card. Completing the word set gives a reason to attempt the exam. Passing gives a reason to inspect the shop or continue to another unit. Improving a best score gives a reason to retry, but the no-reward rule for an unchanged result discourages endless repetition as a way to farm points. The system is using rewards to distinguish new progress from repeated activity.
The rules also avoid a common ambiguity in small learning applications: whether the score shown on the page is the score the system will remember. When an exam is submitted, Rust regenerates the paper for the current attempt using the same deterministic inputs used to start it, counts the answers against that paper, computes the pass status, updates the best score, increments the attempt count, and saves the profile. The response sent back to the interface includes the correct count, total, pass flag, points awarded, first-pass flag, and best score. The result page can therefore explain why the learner received a reward instead of merely displaying a new total.
After the command returns, the frontend updates the visible point chips and renders the result. This is a synchronization step, not a second scoring pass. The interface can show “first pass,” “improvement,” or “no reward” because the backend has already classified the outcome. The same pattern appears throughout the application: the screen supplies an action, the command applies a rule to durable state, and the returned result tells the screen what changed.
That separation is especially important for failure conditions. The exam cannot be started with incomplete practice; the command checks the practiced-word count even if a caller tries to bypass the disabled button. A submission with the wrong number of answers is rejected rather than partially scored. A purchase later in the loop is also checked on the backend, which follows the same principle: a frontend affordance is not by itself permission to mutate the profile.
The profile saved after these operations contains more than a point balance. It includes progress keyed by book and unit, the words practiced for each unit, the best exam result, the pass flag, and the number of attempts. It also contains the character and inventory state that will eventually make the reward visible. Because the mutation is saved after the command completes, logging out and returning later can restore the same learning and customization state. The answer has outlived the card that produced it.
There is a limit to what this durable loop means. The application can reliably say that a learner selected a correct option, recorded a word, became eligible for an exam, or improved a stored score. It cannot, from those fields alone, say that the learner can produce the word in conversation, remember it weeks later, or transfer the grammar rule to an unfamiliar sentence. The state is precise about application activity, not comprehensive about language ability.
That distinction is the foundation for reading the question engine that sits beneath the loop. The gate depends on the content of the cards, the exam depends on how questions and distractors are generated, and the meaning of a passing score depends on the abilities those questions actually sample. Once the answer transaction is clear, the harder question is no longer “did the system record progress?” but “what kind of learning signal did it choose to record?”