Skip to content

WI-015: Teaching cost

WI-015: Teaching cost

Narrowed at spec review. This item originally proposed personal mechanic reactions as well. Writing the design showed the ordering was wrong — see What was deferred, and why below. Scope is now teaching cost only.

Problem

Nothing records what a game cost to teach. That is a real tabletop concern with no home in the schema and no equivalent on BoardGameGeek: complexity ratings describe the game, not the experience of explaining it to four people on a Tuesday.

It is also the part of the original proposal that needs no controlled vocabulary, so it can ship now.

In scope

  • sessions.teachMinutes (integer, nullable) — how long the teach took this time. It varies by group, so it belongs on the play, not the game.
  • sessions.wasTaught (boolean, nullable) — whether anyone was taught at all. Distinguishes “nobody needed teaching” from “not recorded”, which a null teachMinutes alone cannot.
  • user_game_teaching — userId, gameId, canTeach, typicalTeachMinutes. A standing capability: “I can teach Brass.”
  • Regenerated packages/shared schemas.

Out of scope

  • Mechanic reactions of any kind — deferred, see below.
  • Deriving canTeach from session history. Asking is cheaper and more honest than inferring from a handful of plays.
  • Any UI or recommendation logic.

What was deferred, and why

The original proposal conflated three things with different lifetimes:

LifetimePowersStatus
Play observation — “trading confused everyone”one sessionhistory, feedDeferred
Standing preference — “I like engine-building”evolvesdiscoveryDeferred
Teaching capability — “I can teach Brass”per user-gamegroup planningThis item

Two reasons for the split.

Teaching capability is not a mechanic reaction. It is a user↔game capability that merely correlates with complexity. Modelling it alongside mechanic opinions would be a category error, and awkward to unpick later.

The other two depend on a vocabulary that does not exist. Reactions only help discovery if they reference a controlled mechanic vocabulary, which WI-020/WI-028 produce (ADR-0017). Building them first means keying opinions to free-text strings that later need remapping — and that migration is unverifiable, because nobody can tell whether “engine building” and “engine-building” were the same person’s opinion.

They should be revisited once the catalogue exists. The proposed shape is preserved in this item’s history.

Definition of done

  • sessions.teachMinutes and sessions.wasTaught exist, both nullable — one-tap logging is unaffected (ADR-0007).
  • user_game_teaching exists with a unique (userId, gameId), cascading on both.
  • teachMinutes rejects zero and negatives at the contract level.
  • A session records wasTaught = false distinctly from not recording it — a test asserting the three states (true, false, null) round-trip, since collapsing false into null is the easy mistake.
  • “Which games can this user teach” is answerable in one query, and indexed.
  • packages/shared exports derived schemas for the new table; a test asserts they track the columns.
  • Gate green.

Verification

Terminal window
pnpm --filter db test
pnpm --filter @tabletop/shared test
pnpm gate

Notes

teachMinutes sits on the session rather than the game because teaching time is a property of the group, not the box. typicalTeachMinutes on user_game_teaching is the user’s own estimate — deliberately not an average of their sessions, which would be a derived value and would need the same trigger discipline as the shelf projection for no real gain.