Merge branch 'feat/nummer-pro-subreddit'
All checks were successful
CI / test (push) Successful in 23s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lukas Müllner 2026-11-13 18:30:59 +01:00
commit e09dc0eaa4
6 changed files with 47 additions and 15 deletions

View file

@ -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.

View file

@ -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<string | undefined> {
export async function dbSetPostId(day: string, postId: string): Promise<void> {
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<string> {
await redis.set('firstDay', today, {nx: true})
return (await redis.get('firstDay')) ?? today
}

View file

@ -50,7 +50,8 @@ const fake: Partial<typeof redis> = {
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)

View file

@ -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<GetDayRsp> {
)
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<CommentRsp> {
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},
})

View file

@ -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', () => {

View file

@ -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 (