Imagine a guessing game where one participant selects a set of images and another selects candidates one at a time. A correct selection adds progress. An incorrect selection ends the attempt. A round finishes only when every selected image has been found.
This article examines a local R45 snapshot of an application I have been building with AI assistance. It focuses on the API contract and state transitions, without claiming that a source review establishes complete concurrency safety. The article was prepared with AI from project records and that source snapshot; the full release test suite was not rerun for this article.
Start with the input contract
The controller accepts the following Zod schema:
export const answerSchema = z.object({
attemptGroupId: z.string().uuid(),
selectedImageIds: z.array(z.string().uuid()).length(1),
}).strict();
The array contains exactly one image identifier. The client reports its selection; it does not supply a trusted verdict. Fields such as correct, result, and viaKey are outside this contract. The associated test file includes rejection cases for those extra fields.
Input shape validation is only the first stage. The server also checks whether the selected image belongs to the current candidate set before comparing it with the stored correct answers. A well-formed identifier is not necessarily a valid move.
Separate a correct move from a completed round
The completion condition in the reviewed handler is:
const groupCompleted =
isCorrect && sameSet(nextProgress, group.correct_image_ids);
The helper removes duplicates, sorts the arrays, and compares their lengths and elements. For a hypothetical correct set of A and B, choosing A is progress, not completion. Choosing B afterwards completes the round. Reversing the order should lead to the same set.
This distinction mattered when specifying changes to the coding assistant. “Show success after a correct answer” leaves several possible meanings. “Finish the round only when the accumulated correct set matches the target set” defines a narrower behavior that can be inspected and tested.
A retry needs a defined result
The answer() method performs its work inside a transaction, verifies ownership of the attempt, obtains a lock for the player/target pair, and reads the attempt with FOR UPDATE.
There are two separate repeat paths. If a correct image is already in intermediate progress, the handler returns idempotent: true, isCorrect: true, and groupCompleted: false without adding progress again.
If the group answer has already been recorded, the handler checks whether the repeated choice is among the stored selections. A different choice is rejected with ANSWER_ALREADY_RECORDED. An out-of-order group produces ATTEMPT_GROUP_OUT_OF_ORDER.
These branches describe the reviewed implementation. They do not establish that every simultaneous request sequence is safe. A concurrency claim requires controlled requests and assertions about the resulting rows and side effects, not simply observing a lock in the source.
The target can change during an attempt
The author of the image set may edit it while another participant is playing. The implementation stores target_visual_version when an attempt starts and compares it with the profile's current visual_version when processing an answer.
If an active attempt is stale, it becomes abandoned with a skipped result; the internal handler outcome is questionnaire-changed. This implementation therefore terminates an outdated active attempt. Continuing against an immutable snapshot would be another possible product contract, but it is not the behavior described here.
Availability is checked separately. A profile becoming unavailable or a block between participants may also terminate an active attempt. Content consistency and access authorization are different conditions.
Connect completion to its side effect
After all groups have been completed successfully, the transaction calls grantAndOpenDialog, carrying the game_success source and the attempt identifier. The response includes dialogId.
A client-side success message alone cannot represent this entire transition. The useful question is whether the server has performed the operation that success is supposed to unlock.
A focused verification matrix
These are proposed checks, not newly executed test results:
| Scenario | Property to verify |
|---|---|
| Repeat an accepted intermediate choice | Progress does not grow twice |
| Submit a different choice to a recorded group | The request is rejected |
| Answer a later group early | The attempt does not advance |
| Edit the target's image set mid-attempt | The stale active attempt terminates |
| Make the target unavailable | Old content does not grant new access |
| Submit concurrent requests | Stored progress and side effects remain consistent |
The design lesson I take from this code is to name the transitions before implementing them: accepted input, valid candidate, correct choice, completed group, repeated request, stale target, and completed attempt. An AI coding assistant can help implement those rules, but it still needs an unambiguous definition of the expected behavior.
Source scope: the local R45 files apps/api/src/game/game.controller.ts, game.service.ts, and game-pure.spec.ts. The repository is not presented as public evidence; the quoted code is version-specific. This article makes no claim about current production behavior or newly executed tests.
Top comments (0)