# Daily challenges

Games do not have to have dailies. This is a capability, not an obligation — but if your game has
something a player could finish today and finish again tomorrow, the platform can hold that state,
count the streak, and put a marker on your game's card in the arcade so a player scanning the
catalog sees there is something waiting in it.

That marker is the point. A daily challenge is not primarily a reward mechanism — it does not pay,
and [section 5](#5-a-challenge-does-not-pay) is emphatic about that — it is a reason to come back.

**A daily challenge is a progression.** Not a new kind of object, not its own config section:
a progression that repeats on a known period and asks to be shown. Everything you already know
about progressions applies, and [inventory-and-catalogs.md](inventory-and-catalogs.md) is still
where progressions are explained.

## 1. Is this the mechanism you want

Two things look alike and are not. Pick with the table, because the choice decides **where the
state lives**, and a row in the wrong place is not recoverable by reading it back.

| You want                                              | Use                              | Because                                                   |
| ----------------------------------------------------- | -------------------------------- | --------------------------------------------------------- |
| "Clear 10 piles today", shown in the arcade, streaked | a period schedule + `challenge`  | yesterday survives; a missed day is not a zero             |
| A repeatable reward that pays out                     | a `cron` reset                    | a challenge cannot pay; a `cron` progression can           |
| A season track, a battle pass                         | an ordinary progression           | it accumulates; it does not reset                          |
| An internal counter you do not want shown             | a period schedule, no `challenge` | the platform stores it and displays nothing                |

The difference between the first two is not cosmetic. A `cron` reset is **destructive**: it zeroes
one lifetime row in place, so yesterday is overwritten. There is no calendar to compute a streak
from, no way to tell "missed it" from "scored zero", and no completion history. A period schedule
keeps one row per period and never overwrites.

**So `cron` is the right answer for a repeatable reward and the wrong one for a challenge.** If you
find yourself wanting both — a challenge that pays — read section 5 before you design around it.

## 2. Declare it in your game config

Two fields on an ordinary progression:

```yaml
progressions:
  - id: 4f2a9c71-3e8b-4d15-9a62-7c0e5b1d8f43
    slug: clear-ten-piles
    display_name: "Clear ten piles"
    description: "Clear ten piles in a single run."
    config:
      completion_threshold: 10
      reset_schedule: { type: daily } # daily | weekly | monthly
      platform_display: challenge # none (the default) | challenge
```

`reset_schedule` is the whole of the scheduling — there is no cron expression to write and no
timezone to choose. `platform_display: challenge` is what makes it the arcade's business rather
than only yours; the default is `none`, and a progression on a daily schedule that says nothing
appears on no platform surface at all.

`completion_threshold` is the target. `client_writable` decides who may report it, and
[section 4](#4-who-may-report-it) is where that choice actually matters.

**Upload the config the way you upload any other.** A challenge is not promoted differently and
does not need a new build to reach players.

## 3. Report progress from your game

The write is the ordinary progression write. Your game already knows how to make it:

```js
await gem.me.mutations.addProgression('clear-ten-piles', 1); // a counter
await gem.me.mutations.maxProgression('best-depth-today', 7); // a record
```

`addProgression` accumulates and `maxProgression` keeps the best, exactly as they do for any other
progression. The platform routes the write to the current period on its own — your game does not
name a period, compute one, or pass a date.

**A progression on a period schedule writes a period row and NO lifetime row.** This is the one
runtime surprise, and it is worth stating plainly because the symptom is silence:

```js
await gem.me.mutations.addProgression('clear-ten-piles', 10);
const row = await gem.me.progressions.get('clear-ten-piles'); // null
```

The read is not broken and the write did not fail. Period state lives apart from lifetime
progression state, and the ordinary progression read only ever sees the latter. **Do not add a
retry, and do not call `progressionSet` to "fix" the missing row** — a set writes the lifetime row
you were not supposed to have, and now the player has state in two places that mean different
things.

## 4. Who may report it

`client_writable` decides whether the browser may make that write or whether it must come from your
game's server.

A browser credential is public. A `client_writable` challenge is therefore **forgeable**: a player
can complete today's challenge without playing. That is allowed, and the platform accepts it,
because a challenge cannot pay anything — forging one moves a marker on a card and nothing else.

**Choose server-reported when the challenge is a claim about skill you intend to stand behind** — a
leaderboard-adjacent "beat this in under a minute" — and client-reported when it is a nudge to play.
If your game has no server, client-reported is the only option, and for a challenge that is a
reasonable place to be.

## 5. A challenge does not pay

**A progression on a daily, weekly or monthly schedule cannot grant anything, and config that asks
it to is refused at upload — not accepted and quietly ignored.**

Four fields are rejected on a period schedule:

| Field                 | Why it is refused                                                              |
| --------------------- | ------------------------------------------------------------------------------ |
| `rewards_on_complete` | the period path emits no inventory changes, so the operations would never run   |
| `platform_grants`     | a grant is derived from those same changes, so nothing would ever be granted    |
| `tracks`              | track claim state lives on the lifetime row a period-scoped progression has not |
| `max_completions`     | it is a lifetime cap, and period rows do not accumulate into one                |

The refusal is deliberate and the alternative is worse: a silent accept would let you ship a
challenge that uploads clean, looks correct in your config, and pays nobody for months before
anyone notices.

**So grant from your own game server.** Your game reports the completion to the platform for the
marker and the streak, and pays the player through whatever it already uses to pay them — a game
currency, an item, an unlock. If the *progression itself* must pay through the platform, it is not
a challenge: use a `cron` reset and accept that you lose the calendar with it.

## 6. Periods are UTC, and your game must not compute the boundary

**A day ends at 00:00 UTC for every player, everywhere.** A player at UTC+13 sees the day roll over
at 11:00 their time; a player at UTC-8 at 16:00 theirs.

Two consequences, and the second is the one that breaks code:

- **Do not word a challenge as "today".** "Today's board" is a fine title. A mechanic that assumes
  the player's local midnight is the boundary is wrong for most of the planet.
- **Never derive the boundary from a clock.** A period is identified by an instant the platform
  supplies, not by a date your game computes. A game that asks the platform when the current period
  ends keeps working if the platform's periods ever change shape; a game that computed one does not.

## 7. An id is permanent, and means one thing forever

A challenge's id is carried on every player's history for a long time, with nothing pointing back at
your config to explain it. **Reusing an id for a different challenge silently merges two challenges
into one history** and makes every number computed from it unexplainable — not wrong in a way that
shows, just quietly meaningless.

- **A new challenge gets a new id.** Always.
- **To rename what the player sees, change `display_name`.** That is free and is not part of the
  challenge's identity.
- Upload refuses a config that changes an id's period, checked against your recent versions.

## 8. Retuning the target mid-period is safe

`completion_threshold` is snapshotted onto each player's row at their first progress in a period.
Raising it later cannot un-complete a period a player already finished, and cannot rewrite a
historical percentage.

**Lowering it mid-period is safe too, and has a consequence worth knowing:** a player who already
started that period keeps the target they started with. Two players in the same period can be
working toward different numbers, and every platform surface shows each player their own. That is
deliberate — the alternative rewrites history — but it will generate a support question the first
time two friends compare.

## 9. Streaks

The platform counts consecutive periods in which a player completed what your game offered, and
keeps a longest-ever alongside the current one. Your game declares nothing to get this; it follows
from the challenge.

Two things to know before you build a mechanic on it:

- **A streak is the platform's, and it is shown on platform surfaces.** Your game reports progress
  and does not read the streak back — see below.
- **Whether a player's streak is shown at all can depend on who they are.** Some players do not see
  streak surfaces. Do not design a mechanic whose fairness depends on every player seeing one.

## 10. What your game can read back

**Your game reports daily progress; it does not read it back.** The ordinary progression read
returns nothing for a period-scoped progression (section 3), and the SDK surfaces no period or
streak read of its own.

That is workable because the arcade is what displays the challenge state — the marker on your
game's card, and the player's own progress and streak on platform surfaces. Your game does not have
to render any of it.

**If your game wants its own in-game daily UI, render what it just reported.** Your game knows what
it sent this session. Treat that as a view of this session's play, not as the authoritative daily
record — the platform holds that, and it is the one that survives the player switching devices.

## 11. When an upload is refused

| What you get                                 | What happened                                        | What to do                                                        |
| -------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------- |
| `rewards_on_complete` cannot be combined      | you gave a challenge a reward                        | grant from your game server; see section 5                        |
| `platform_grants` cannot be combined          | you mapped a challenge onto a platform currency      | same; or use a `cron` reset if the progression must pay           |
| `tracks` cannot be combined                   | you gave a challenge tiers                           | one target per period, or use a `cron` reset                      |
| `max_completions` cannot be combined          | you capped a challenge's lifetime completions        | remove it; periods are not capped this way                        |
| `platform_display: challenge` needs a period  | you asked to display a progression that never resets | add a `daily`, `weekly` or `monthly` `reset_schedule`             |
| an id's period changed                        | you edited an existing challenge's schedule          | leave it and add a new challenge with a new id                    |

Every refusal names the field and the reason. **Read the error rather than a list written down
here** — the platform is what decides these, and this table is only as current as the day it was
written.
