ADR: Support Ranged Correct Answers and Revised Scoring Model
Status
- [x] proposed - 2026-03-02
- [ ] accepted - this is what we do
- [ ] rejected - state the reason
- [ ] deprecated - we dont need this decision anymore
- [ ] superseded - we have made a new decision, reference
Context
The current game engine models every question with a single scalar correctValue.
Players guess a range [userMin, userMax]; if the correct value falls inside their
range they score 1 point, and the narrowest range earns a 5-point bonus.
Issue #85 introduces a new category of questions where the correct answer is itself
a range (e.g. "When was the Classicism period?" with correct answer 1750-1820).
Along with this come several related requirements:
- Questions may carry manual facts and a reference URL shown in the fasit
to build credibility ("we checked this"). - Questions carry illustration metadata (author, license, source URL) for legal
compliance and proper attribution. - A test mode is needed so developers and content authors can play a specific
question set solo, without the lobby timer. - A personal review page lets each player browse questions, answers, and
reference material at their own pace after the game finishes. - Time is no longer a scoring factor in any game mode (global change).
- Relative scales (bathtubs, tankers) are explicitly out of scope.
The game flow, state machine, and player input mechanism (touchSlider setting
min/max) remain unchanged.
Forces
- The domain model must stay backward-compatible with existing single-point questions.
- The scoring change (removing time relevance) is global, not per-question-type.
- The state machine (
Game.sm) is stable and must not be modified for this feature. - Hexagonal architecture requires that all new behaviour stays inside the domain
core and application services; adapters translate, they do not decide. - The "Recognisable Play" design spec demands predictable rhythm and transparent
scoring. Changing the scoring model must not break player trust.
Decision
D1 -- Ranged correct answers as the universal question format
Model every question with correctMin and correctMax.
- Single-point questions:
correctMin == correctMax(backward-compatible degenerate case). - Range questions:
correctMin < correctMax. - A convenience method
correctValue()returns the midpoint for display purposes. - A predicate
isRangeAnswer()returnstruewhencorrectMax - correctMin > 0.
The Question record becomes:
public record Question(
UUID id, String questionText, String isoUnit,
double defaultMin, double defaultMax,
double correctMin, double correctMax,
String illustrationUrl, String language, List<String> tags,
List<Fact> facts, String refUrl,
IllustrationMeta illustrationMeta
) {
public double correctValue() {
return (correctMin + correctMax) / 2.0;
}
public boolean isRangeAnswer() {
return correctMax - correctMin > 1e-9;
}
}
The Answer.isWithin(double) method is preserved for backward compatibility and a
new method encompasses(double min, double max) is added:
public boolean encompasses(double correctMin, double correctMax) {
return userMin <= correctMin && userMax >= correctMax;
}
D2 -- Time-irrelevant scoring (global)
Time is removed as a scoring factor entirely. This is a global change, not
per-question-type.
- 1 point: player's range encompasses the correct range
(userMin <= correctMin && userMax >= correctMax). - 5 bonus points: to the player(s) with the smallest
delta(userMax - userMin)
among correct answers only. Split equally on ties. - Tiebreaker for final ranking: total score only. Equal scores share the same rank.
submittedAtis still recorded for analytics and display but has no effect on
scoring or ranking.
Implemented via a ScoringStrategy interface in the domain:
public interface ScoringStrategy {
ScoreResult calculate(Game game, List<Question> questions);
}
The current ScoringService is refactored into an implementation of this interface.
The new time-irrelevant scoring becomes the default (and, for now, only) strategy.
The interface exists so that future scoring models can be introduced without
modifying existing code (Open-Closed Principle).
D3 -- Question metadata: facts, reference URL, illustration licensing
Each question gains:
List<Fact> facts-- manually authored facts shown during fasit review.String refUrl-- a link to the verified source (e.g. Wikipedia).IllustrationMeta illustrationMeta-- a value object containing:String authorString licenseType(e.g. "CC BY-SA 4.0", "Public Domain")String sourceUrl
These are domain value objects, not adapter concerns, because they affect
what the game engine serves to players. The adapter layer decides how to render
them (tiny credit line below illustrations, clickable reference links in fasit).
The JSON import format (import-data-structure.md) is extended accordingly.
D4 -- Test mode via known word-combo code
Test mode is triggered by navigating to a game using a known word-combo code
(e.g. "Anna-Beth-Charlie"). This is not a separate engine or state machine.
Implementation:
- A new adapter route (e.g.
GET /test/{word-combo}) creates a game with
deterministic configuration: - Lobby timer disabled (immediate start on first player join).
- Min players = 1 (solo play allowed).
- Question set fixed to a known test set.
- The game uses the same domain model, same state machine, same scoring.
- A
GameModemarker (enum:STANDARD,TEST) is stored on theGameaggregate
so that test games can be excluded from analytics and auto-lobby rotation.
D5 -- Personal post-game review page
After a game finishes, each player can access a personal review page that lets
them browse all questions at their own pace.
- This is a new read-only adapter route, not a state machine change.
- Route:
GET /play/{code}/review(requires valid player session). - Content: for each question, show the question text, the player's answer, the
correct answer (range or point), reference URL, illustration with credits,
and facts. - Data source: existing
Gameaggregate (answers, question IDs) and
QuestionRepository(question content). No new ports needed.
D6 -- State machine: no changes
The state machine (Game.sm) is not modified. All states and transitions remain:
AUTO_LOBBY -> COUNTDOWN -> PLAYING -> REVIEWING -> RESULTS -> FASIT -> FINISHED -> COOLDOWN -> AUTO_LOBBY
The ranged-answer logic, scoring changes, and new metadata are all handled in
domain value objects and application services, not in state transitions or guards.
D7 -- Relative scales: out of scope
Human-relatable unit scales (bathtubs, tankers) are deferred to a separate issue.
When implemented, they belong in the adapter/presentation layer, not the domain.
Consequences
Positive
- Backward compatible: existing single-point questions work unchanged
(correctMin == correctMax). - Clean architecture: all changes are in the domain layer and services.
No hexagonal boundary violations. No state machine modifications. - Extensible scoring: the Strategy pattern allows future scoring models without
modifying existing code. - Legal compliance: illustration metadata enables proper attribution and
takedown capability. - Player trust: reference URLs and manually curated facts reinforce the
"Recognisable Play" credibility model. - Testability: test mode uses the same engine, so test games exercise the
real code paths.
Negative
- Scoring model is a global change: there is no gradual rollout. All games
switch to time-irrelevant scoring at once. This simplifies the codebase but
removes a competitive dimension that some players may value. - Question data migration: existing question JSON must be converted from
correctValuetocorrectMin/correctMax. This is mechanical but must be
done before deployment. - Increased Question complexity: each question now carries more fields (facts,
refUrl, illustrationMeta). Content authoring becomes more involved.
Risks
- Tie frequency increases: without time as a tiebreaker, more players may
share the same rank. This is acceptable per the interview decision but should
be monitored after deployment. - Test mode abuse: if the word-combo is guessable, non-developers could
access test games. Mitigated by using a non-obvious combo and excluding test
games from public display.
Implementation Phases
Phase 1 -- Domain model and scoring (Issue #85 core)
- Extend
Questionrecord withcorrectMin,correctMax. - Add
Answer.encompasses(min, max)method. - Introduce
ScoringStrategyinterface; refactorScoringServiceto implement it. - Implement time-irrelevant scoring as the default strategy.
- Update
ReviewServiceandAnswerGraphSvgBuilderto render range fasit
(band instead of single line). - Update
import-data-structure.mdand migrate existing question JSON. - Update existing tests; add tests for range questions and new scoring.
Phase 2 -- Question metadata
- Add
facts,refUrl,IllustrationMetatoQuestionrecord. - Add
IllustrationMetavalue object to domain. - Update fasit templates to display facts, reference links, and image credits.
- Update question JSON import format.
Phase 3 -- Test mode
- Add
GameModeenum to domain. - Add
GameModefield toGameaggregate. - Create adapter route for test-mode game creation.
- Exclude test games from auto-lobby rotation.
Phase 4 -- Personal review page
- Create
GET /play/{code}/reviewadapter route. - Build JTE template for per-question review with all metadata.
- Require valid player session for access.
References
- Player-reported feature proposal (ranged answers, test mode, facts)
- the Recognisable Play design principles spec -- UX trust model
- the public common-screen flow spec -- game flow spec
- the question JSON import schema -- question JSON schema
- the domain data types ADR -- domain data type policy