diff --git a/PLAN.md b/PLAN.md index b8600d7..b77f054 100644 --- a/PLAN.md +++ b/PLAN.md @@ -260,3 +260,11 @@ Abgabe: **9. Dezember 2026, 03:00 MEZ**. Eingereicht wird in „Best Daily Game Build gab es zweimal doppelte Versionsnummern und Abbruch. Bei Änderungen an `devvit.json` Playtest stoppen und neu starten. - Folge der 30-Tage-Regel: Freischaltungen gehen nach 30 Tagen ohne Spiel verloren. + +### 2026-11-13 – Nummer je Subreddit + +- „Ninelands #N“ zählt **je Subreddit ab dem ersten Tag dort** (`firstDay` in Redis, beim ersten Aufruf per `nx` + gesetzt, enthält keine Nutzerdaten und läuft nicht ab). So beginnt `r/Ninelands` mit #1, auch wenn es erst im + Dezember startet. Die Karte hängt weiter nur am Datum (alle spielen am selben Tag dieselbe). `FIRST_DAY` entfällt. +- Plan Besitzer/Claude: `r/ninelands_game_dev` bleibt Test-Subreddit; `r/Ninelands` (Name frei) etwa eine Woche vor + der Abgabe anlegen (≈ 1. Dez.), öffentlich oder „Restricted“, < 200 Mitglieder bis zur Bewertung. diff --git a/src/server/db.ts b/src/server/db.ts index 7aa5b2c..cf23ab7 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -7,6 +7,7 @@ // player:{userId} JSON PlayerStats + lastDay, Aussehen, Kronen-Tag // comments:{day} Hash userId → 1 (Ergebnis schon als Kommentar gepostet) // posts Hash day → postId (verhindert doppelte Tagesposts) +// firstDay erster Tag in diesem Subreddit (Nummer „#N“ zählt ab hier; enthält keine Nutzerdaten) // // Devvit-Regel „Enable and respect user deletions“: Für Kontolöschungen gibt es keinen Trigger, deshalb laufen alle // Schlüssel mit User-ID oder Namen nach RETENTION_S ab (Reddit empfiehlt ≤ 30 Tage). Jede Abgabe verlängert die @@ -255,3 +256,9 @@ export async function dbGetPostId(day: string): Promise { export async function dbSetPostId(day: string, postId: string): Promise { await redis.hSet('posts', {[day]: postId}) } + +/** Erster Tag in diesem Subreddit; beim ersten Aufruf auf heute gesetzt (nx: nie überschreiben) */ +export async function dbFirstDay(today: string): Promise { + await redis.set('firstDay', today, {nx: true}) + return (await redis.get('firstDay')) ?? today +} diff --git a/src/server/server.test.ts b/src/server/server.test.ts index 916d8c4..5838750 100644 --- a/src/server/server.test.ts +++ b/src/server/server.test.ts @@ -50,7 +50,8 @@ const fake: Partial = { expiries.set(k, seconds) }, get: async k => strings.get(k), - set: async (k, v) => { + set: async (k, v, opts) => { + if (opts?.nx && strings.has(k)) return '' strings.set(k, v) return 'OK' }, @@ -362,6 +363,12 @@ test('Flair: Streak ab 2 Tagen', async () => { assert.deepEqual(flairs.at(-1)?.text, '🔥 5-day streak') }) +test('Nummer zählt ab dem ersten Tag im Subreddit', async () => { + assert.equal((await getDay()).number, 1) + strings.set('firstDay', addDays(today, -9)) + assert.equal((await getDay()).number, 10) +}) + test('404', async () => { const rsp = await fetch(serverURL) assert.equal(rsp.status, 404) diff --git a/src/server/server.ts b/src/server/server.ts index 4be3a25..84eaec9 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -23,12 +23,14 @@ import { type DailyMap, dailyMap, dayLabel, + dayNumber, isDay, } from '../shared/daily.ts' import {flairText, shareLine} from '../shared/format.ts' import {DEFAULT_LOOK} from '../shared/look.ts' import { dbClaimComment, + dbFirstDay, dbGetGoals, dbGetLook, dbGetPlayer, @@ -176,7 +178,7 @@ async function routeGetDay(): Promise { ) return { day: map.day, - number: map.number, + number: dayNumber(await dbFirstDay(today), map.day), mapId: map.mapId, trait: map.trait, goals, @@ -252,7 +254,7 @@ async function routeComment(): Promise { if (!(await dbClaimComment(map.day, userId))) return {posted: false} await reddit.submitComment({ id: postId, - text: `${shareLine(map.number, mine)} · #${mine.rank} today`, + text: `${shareLine(dayNumber(await dbFirstDay(map.day), map.day), mine)} · #${mine.rank} today`, runAs: 'USER', }) return {posted: true} @@ -304,7 +306,7 @@ async function ensureDailyPost(): Promise<{id: string; created: boolean}> { const existing = await dbGetPostId(map.day) if (existing) return {id: existing, created: false} const post = await reddit.submitCustomPost({ - title: `Ninelands #${map.number} · ${dayLabel(map.day)}`, + title: `Ninelands #${dayNumber(await dbFirstDay(map.day), map.day)} · ${dayLabel(map.day)}`, entry: 'default', postData: {day: map.day}, }) diff --git a/src/shared/daily.test.ts b/src/shared/daily.test.ts index 72e1eea..e9173fc 100644 --- a/src/shared/daily.test.ts +++ b/src/shared/daily.test.ts @@ -5,7 +5,7 @@ import {Game} from '../engine/core/Game.ts' import {computeGoals} from '../engine/core/goals.ts' import {applyMoves, encodeMoves} from '../engine/core/replay.ts' import {dailySeed, mulberry32} from '../engine/core/rng.ts' -import {dailyBoard, dailyMap} from './daily.ts' +import {dailyBoard, dailyMap, dayNumber} from './daily.ts' test('Tageskarte ist deterministisch und je Tag verschieden', () => { const a = dailyMap(new Date('2026-11-20T00:00:00Z')) @@ -13,8 +13,13 @@ test('Tageskarte ist deterministisch und je Tag verschieden', () => { const c = dailyMap(new Date('2026-11-21T12:00:00Z')) assert.deepEqual(a, b) assert.notEqual(a.seed, c.seed) - assert.equal(c.number, a.number + 1) - assert.equal(dailyMap(new Date('2026-11-13T05:00:00Z')).number, 1) +}) + +test('Nummer zählt je Subreddit ab dem ersten Tag dort', () => { + assert.equal(dayNumber('2026-12-01', '2026-12-01'), 1) + assert.equal(dayNumber('2026-12-01', '2026-12-09'), 9) + assert.equal(dayNumber('2026-11-13', '2027-01-01'), 50) + assert.equal(dayNumber('2026-12-01', '2026-11-20'), 1) // alte Karte vor dem Start: nie 0 oder negativ }) test('Reddit-Seed unterscheidet sich von der Website-Tageskarte', () => { diff --git a/src/shared/daily.ts b/src/shared/daily.ts index 238c5a0..94f0697 100644 --- a/src/shared/daily.ts +++ b/src/shared/daily.ts @@ -8,14 +8,9 @@ import type {WorldTrait} from '../engine/core/WorldRules.ts' /** Präfix des Tages-Seeds. Nie ändern – sonst ändern sich alle Tageskarten (und Bestenlisten) rückwirkend. */ export const DAILY_SEED_PREFIX = 'ninelands-reddit-daily-' -/** Erster Tag des Reddit-Spiels (UTC); daraus die laufende Nummer „Ninelands #N“. */ -export const FIRST_DAY = '2026-11-13' - export interface DailyMap { /** UTC-Tag „JJJJ-MM-TT“ */ day: string - /** laufende Nummer ab FIRST_DAY (1 = erster Tag) */ - number: number seed: number mapId: string trait: WorldTrait @@ -24,11 +19,8 @@ export interface DailyMap { export function dailyMap(date: Date): DailyMap { const day = dailyKey(date) const seed = fnv1a(`${DAILY_SEED_PREFIX}${day}`) - const number = - Math.round((Date.parse(day) - Date.parse(FIRST_DAY)) / 86_400_000) + 1 return { day, - number, seed, mapId: mapCodeFromSeed(seed), trait: rolledTrait(seed), @@ -40,6 +32,17 @@ export function dailyBoard(map: DailyMap): Board { return generateBoard(map.seed, {trait: map.trait}) } +/** + * Laufende Nummer „Ninelands #N“: zählt je Subreddit ab dem ersten Tag dort (1 = erster Tag), damit z. B. + * r/Ninelands mit #1 beginnt. Die Karte selbst hängt nur am Datum – alle spielen am selben Tag dieselbe. + */ +export function dayNumber(firstDay: string, day: string): number { + return Math.max( + 1, + Math.round((Date.parse(day) - Date.parse(firstDay)) / 86_400_000) + 1, + ) +} + /** Gültiger UTC-Tag „JJJJ-MM-TT“ (aus Post-Daten oder Anfragen) */ export function isDay(value: unknown): value is string { return (