I Finally Built the Exam Mode I Kept Talking About
I had been talking about exam mode for a while.
Probably too long.
In one of my earlier build logs, I wrote about the difference between learning and testing.
Normal practice should let me think.
Vocabulary should not have a countdown screaming at me.
A relaxed practice session should not suddenly submit itself because I spent too long understanding an explanation.
But an actual mock test is different.
If the real exam gives me a fixed amount of time, negative marking, a specific structure and no second chances, then the app should reproduce that pressure properly.
For a while, that distinction existed mostly in my roadmap.
Now it exists in the product.
After finishing the Peak Vocabulary Engine, I moved straight into the next major milestone:
the Peak Exam Engine.
Practice mode was never supposed to become exam mode
This distinction ended up being more important than I expected.
The app already had timers before the exam engine.
I could practise questions.
I could choose different session sizes.
The system could record response time.
There were explanations, review queues, bookmarks and progress tracking.
It would have been very easy to take that existing practice screen, add a countdown at the top and call it:
Mock Test Mode
I did not want to do that.
Because practice and examination have different rules.
When I am learning, the app should help me.
When I am sitting a mock, the app should judge what I can do under constraints.
Those are almost opposite behaviours.
So the exam engine became a separate system rather than a slightly scarier version of normal practice.
Five exams meant five different contexts
The project had already expanded beyond treating everything as one generic "IPMAT verbal" bucket.
So the exam engine could not just contain one universal mock preset.
I built presets for:
IPMAT Indore
IPMAT Rohtak
JIPMAT
IIM Bangalore UG
IIM Kozhikode BMS
The important part is not simply having five buttons.
Each preset represents a versioned exam configuration.
That means the engine can associate the selected exam with things like its structure, timing and marking behaviour instead of scattering those rules randomly throughout the interface.
That matters because exams change.
If a pattern changes later, I do not want old attempts becoming impossible to interpret because the app silently started using new rules.
The rules that produced an attempt should remain understandable.
That pushed me toward treating exam configurations as actual data rather than UI decoration.
Marking became part of the engine
Normal practice is forgiving.
You answer a question.
You learn from it.
The app records what happened.
A mock test has to be much stricter.
Correct answers may earn marks.
Incorrect answers may lose marks depending on the selected exam's rules.
Unattempted questions need to remain unattempted rather than being treated as ordinary wrong answers.
The final score has to come from the correct marking configuration for that mock.
That sounds obvious.
But it creates a very different system from ordinary question practice.
Now an answer is not just:
correct or incorrect
It can affect:
Raw score.
Accuracy.
Attempt rate.
Negative marks.
Topic analysis.
Timing analysis.
The meaning of the entire attempt.
Once scoring becomes serious, getting those rules wrong is much worse than displaying the wrong animation.
The timer finally became strict
Earlier, I deliberately avoided making normal practice auto-submit.
I still think that was the right decision.
If I am learning and the timer reaches zero, forcing the question away from me is often counterproductive.
The exam engine is different.
When the clock ends, the attempt ends.
That is part of what makes it an exam simulation instead of timed practice.
So the engine now has strict countdown behaviour and automatic submission when time expires.
That created a new set of questions.
What happens if I am currently answering something?
What happens if I never manually press submit?
What does the interface show as time disappears?
What state needs to be preserved?
What exactly gets submitted?
These details are boring until one of them fails halfway through a mock.
Then they become the only thing that matters.
I finally built the question palette
This is one of those features that instantly makes something feel more like a real exam interface.
A proper mock cannot behave like an endless feed where I just press Next repeatedly.
I need to understand the entire attempt.
Which questions have I visited?
Which ones have I answered?
Which ones remain untouched?
Which ones do I want to return to?
So the exam engine now has a proper question palette and navigation system.
I can move between questions instead of being trapped inside a linear sequence.
That changes strategy.
In practice mode, the app decides much more of the flow.
In an exam, I should be able to decide:
Skip this.
Come back later.
Do this one now.
Leave that one.
Review the difficult ones before submission.
That freedom is part of exam performance too.
Mark for review finally means something
I also added proper review flags.
This sounds almost embarrassingly basic for an exam system.
But that is exactly why it matters.
During a mock, there are questions where I think:
I answered this, but I do not trust it.
Or:
I do not want to waste two minutes on this right now.
The system needs to preserve that intention.
So I can mark questions for review and return to them later through the palette.
Again, the app is not trying to teach me during this stage.
It is giving me tools to manage the attempt.
I also needed Clear Response
A surprisingly small feature that matters a lot:
Clear Response.
If I select an option and later decide I would rather leave the question unattempted, I need to be able to remove the answer completely.
Changing from option B to option C is not the same thing.
Unattempted is its own state.
That becomes especially important when negative marking exists.
Sometimes deciding not to answer is itself a strategic decision.
The engine needs to respect that.
Submitting should feel serious
In normal practice, completing a session is not a dramatic event.
A mock is different.
Once I submit, the attempt is over.
So I did not want a stray click instantly ending everything.
The engine includes final submission confirmation before a manual submit.
That gives me one last opportunity to realise:
I still have unanswered questions.
I marked something for review.
I clicked the wrong button.
Or I am genuinely finished.
Time expiry is the exception.
If the exam clock reaches zero, there is nothing to confirm.
The attempt auto-submits.
That distinction is exactly the kind of thing that separates a real exam flow from a normal practice session with a timer added on top.
The result screen had to do more than show a score
Once the engine could run a proper attempt, the next obvious question was:
What happens after submission?
Showing:
Score: 87
is not enough.
A mock is useful because it produces evidence.
So the post-exam side started analysing much more than the final number.
Accuracy.
Attempts.
Timing.
Topic performance.
Difficulty.
Errors.
How the session unfolded.
The score tells me what happened overall.
The analysis should start helping me understand why.
This is also where the exam engine begins connecting with the larger intelligence system I want the app to have.
A mock should not disappear after I submit it.
It should become evidence about my preparation.
Timing became much more useful here
I had already been interested in silently recording answer time during ordinary practice.
Exam mode makes that information far more meaningful.
Under strict timing, speed becomes part of performance.
Two students can get the same number of questions correct while having completely different problems.
One may be fast but careless.
Another may be accurate but far too slow.
A third may spend huge amounts of time on a particular topic.
A raw score hides all of that.
So the engine records timing information that can later be connected to deeper analysis.
This was one of the first moments where I could clearly see the next milestone waiting for me.
The app was collecting enough evidence to start saying something intelligent about how I prepare.
Attempts needed to survive too
By this point, I had already spent a lot of time thinking about local-first behaviour and cloud sync.
Exam attempts made that even more important.
Imagine spending a full mock under strict timing and then losing the entire attempt because the network disappeared during submission.
That would be unforgivable.
So exam state and completed attempts needed to work with the same broader persistence philosophy as the rest of the app.
Local persistence first.
Cloud synchronization where appropriate.
The student should not have to care which layer saved the attempt.
It should just be there.
I refused to fake full mocks
There was still one major limitation.
Content.
The engine could now support serious exam behaviour.
The question bank was still growing.
That created a tempting shortcut.
I could make every mock look full-sized by recycling content aggressively or pretending the bank was larger than it actually was.
That would make screenshots look better.
It would also make the product worse.
So I chose transparency instead.
If the available bank for a particular exam cannot yet support the full ideal attempt, the engine scales the attempt to the content that is actually available.
No pretending.
No magically inventing breadth the bank does not have yet.
The engine is ready to grow.
The content has to catch up.
I am much more comfortable with that than building fake completeness into the product.
This changed the role of the question bank
Earlier in the project, the question bank was basically the product.
If I had 32 questions, I had 32 questions worth of experience.
If I had 64, the app became slightly bigger.
Now the relationship feels different.
The bank is becoming fuel for multiple engines.
The vocabulary engine can interpret vocabulary interactions.
The exam engine can assemble and run structured attempts.
The intelligence layer can analyse the resulting evidence.
That means adding a good question creates more value than it used to.
The same piece of content can participate in a much richer system.
This is one reason I decided to finish the major engines before spending weeks generating thousands of questions.
I wanted to know what those questions were entering.
Now I do.
The exam engine finally closes an old promise
This is probably the most satisfying part.
Earlier in the project, I wrote about the modes I wanted:
Learn.
Paced.
Exam.
At the time, a lot of that was product thinking.
I knew normal learning should remain calm.
I knew optional timing should exist.
I knew strict exam simulation needed different rules.
But knowing how something should work and actually building it are very different things.
Now the exam side has:
Exam-specific presets.
Versioned configurations.
Real marking behaviour.
Negative marking where applicable.
Strict timers.
Auto-submission.
A question palette.
Navigation.
Mark for review.
Clear response.
Submission confirmation.
Post-mock analysis.
Timing data.
Persistent attempts.
That is a long way from:
put a timer above six questions.
And then the app had too much information to ignore
Finishing the exam engine created another problem.
A good problem.
The app was now collecting a lot of evidence.
Practice answers.
Vocabulary behaviour.
Repeated mistakes.
Response times.
Mock attempts.
Topic performance.
Accuracy.
Review history.
Retention signals.
At some point, simply displaying all of that becomes lazy.
I do not want a dashboard that dumps twenty graphs on me and expects me to diagnose myself.
The whole reason I am building a learning system is that the system should eventually help interpret the evidence.
Not make me become my own data analyst.
So once the exam engine was working, the next milestone became obvious.
The app already knew what I had done.
Now I wanted it to start telling me what that meant.