Support Ranged Answers And Scoring Model

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:

  1. Questions may carry manual facts and a reference URL shown in the fasit
    to build credibility ("we checked this").
  2. Questions carry illustration metadata (author, license, source URL) for legal
    compliance and proper attribution.
  3. A test mode is needed so developers and content authors can play a specific
    question set solo, without the lobby timer.
  4. A personal review page lets each player browse questions, answers, and
    reference material at their own pace after the game finishes.
  5. Time is no longer a scoring factor in any game mode (global change).
  6. 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() returns true when correctMax - 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.
  • submittedAt is 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 author
  • String 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 GameMode marker (enum: STANDARD, TEST) is stored on the Game aggregate
    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 Game aggregate (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
    correctValue to correctMin/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)

  1. Extend Question record with correctMin, correctMax.
  2. Add Answer.encompasses(min, max) method.
  3. Introduce ScoringStrategy interface; refactor ScoringService to implement it.
  4. Implement time-irrelevant scoring as the default strategy.
  5. Update ReviewService and AnswerGraphSvgBuilder to render range fasit
    (band instead of single line).
  6. Update import-data-structure.md and migrate existing question JSON.
  7. Update existing tests; add tests for range questions and new scoring.

Phase 2 -- Question metadata

  1. Add facts, refUrl, IllustrationMeta to Question record.
  2. Add IllustrationMeta value object to domain.
  3. Update fasit templates to display facts, reference links, and image credits.
  4. Update question JSON import format.

Phase 3 -- Test mode

  1. Add GameMode enum to domain.
  2. Add GameMode field to Game aggregate.
  3. Create adapter route for test-mode game creation.
  4. Exclude test games from auto-lobby rotation.

Phase 4 -- Personal review page

  1. Create GET /play/{code}/review adapter route.
  2. Build JTE template for per-question review with all metadata.
  3. 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