@joystyk/daily 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +154 -2
- package/dist/browser/clock.d.ts +42 -0
- package/dist/browser/clock.js +56 -0
- package/dist/browser/clock.js.map +1 -0
- package/dist/browser/content.d.ts +73 -0
- package/dist/browser/content.js +170 -0
- package/dist/browser/content.js.map +1 -0
- package/dist/browser/errors.d.ts +23 -0
- package/dist/browser/errors.js +20 -0
- package/dist/browser/errors.js.map +1 -0
- package/dist/browser/hash.d.ts +6 -0
- package/dist/browser/hash.js +16 -0
- package/dist/browser/hash.js.map +1 -0
- package/dist/browser/index.d.ts +5 -0
- package/dist/browser/index.js +6 -0
- package/dist/browser/results.d.ts +37 -0
- package/dist/browser/results.js +102 -0
- package/dist/browser/results.js.map +1 -0
- package/dist/dates.d.ts +20 -0
- package/dist/dates.js +30 -0
- package/dist/dates.js.map +1 -0
- package/dist/ids.d.ts +15 -0
- package/dist/ids.js +25 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/local-time.d.ts +17 -0
- package/dist/local-time.js +29 -0
- package/dist/local-time.js.map +1 -0
- package/dist/release.d.ts +23 -0
- package/dist/release.js +29 -0
- package/dist/release.js.map +1 -0
- package/dist/schemas.d.ts +99 -0
- package/dist/schemas.js +121 -0
- package/dist/schemas.js.map +1 -0
- package/dist/share.d.ts +55 -0
- package/dist/share.js +73 -0
- package/dist/share.js.map +1 -0
- package/dist/streaks.d.ts +31 -0
- package/dist/streaks.js +69 -0
- package/dist/streaks.js.map +1 -0
- package/dist/word-stats.d.ts +12 -0
- package/dist/word-stats.js +46 -0
- package/dist/word-stats.js.map +1 -0
- package/package.json +47 -11
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { parsePuzzleId } from "./ids.js";
|
|
2
|
+
import { isIsoDate } from "@joystyk/kit";
|
|
3
|
+
import { err, isGameId, isJson, ok } from "@joystyk/contracts";
|
|
4
|
+
import * as z from "zod/mini";
|
|
5
|
+
//#region src/schemas.ts
|
|
6
|
+
var describe = (issue, prefix = []) => {
|
|
7
|
+
const path = [...prefix, ...(issue.path ?? []).map((segment) => typeof segment === "object" ? segment.key : segment)].map(String).join(".");
|
|
8
|
+
return path === "" ? issue.message : `${path}: ${issue.message}`;
|
|
9
|
+
};
|
|
10
|
+
/** Runs any Standard Schema, sync or async, and returns its output or its issues. */
|
|
11
|
+
var decode = async (schema, value) => {
|
|
12
|
+
const result = await schema["~standard"].validate(value);
|
|
13
|
+
return result.issues === void 0 ? ok(result.value) : err(result.issues.map((issue) => describe(issue)));
|
|
14
|
+
};
|
|
15
|
+
/** Any JSON value, checked with the contracts' `isJson`. */
|
|
16
|
+
var json = z.custom(isJson, { error: "Expected a JSON value" });
|
|
17
|
+
var isoDate = z.string().check(z.refine(isIsoDate, "Expected a real date written yyyy-mm-dd"));
|
|
18
|
+
/** A content hash: the first 12 hex digits of SHA-256 over the canonical JSON of the puzzle. */
|
|
19
|
+
var CONTENT_HASH = /^[0-9a-f]{12}$/;
|
|
20
|
+
var contentHash = z.string().check(z.regex(CONTENT_HASH, "Expected 12 lowercase hex digits"));
|
|
21
|
+
/** A variant name such as "quick" or "standard": lowercase kebab-case, at most 32 characters. */
|
|
22
|
+
var VARIANT = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
23
|
+
var variant = z.string().check(z.maxLength(32), z.regex(VARIANT, "Expected a kebab-case variant name"));
|
|
24
|
+
/** The current version of the puzzle file and index formats. */
|
|
25
|
+
var DAILY_SCHEMA_VERSION = 1;
|
|
26
|
+
/** Decodes the puzzle file's wrapper. The id's date must match `date`. */
|
|
27
|
+
var dailyFile = z.object({
|
|
28
|
+
schemaVersion: z.literal(1),
|
|
29
|
+
puzzleId: z.string().check(z.refine((id) => parsePuzzleId(id) !== null, "Expected an id like daily-word:2026-10-08")),
|
|
30
|
+
date: isoDate,
|
|
31
|
+
contentHash,
|
|
32
|
+
config: json
|
|
33
|
+
}).check(z.refine((file) => parsePuzzleId(file.puzzleId)?.date === file.date, {
|
|
34
|
+
message: "The puzzle id's date doesn't match the file's date",
|
|
35
|
+
path: ["puzzleId"]
|
|
36
|
+
}));
|
|
37
|
+
var mismatch = (field, expected, actual) => ({
|
|
38
|
+
message: `Expected ${expected}, got ${actual}`,
|
|
39
|
+
path: [field]
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* A Standard Schema for one game's puzzle file: the wrapper, then `config` through the game's
|
|
43
|
+
* own `schemas.config` (any Standard Schema, sync or async). With `expected`, it also refuses
|
|
44
|
+
* a file for another game, date or hash than the one requested. Issue paths inside the config
|
|
45
|
+
* start with `config`.
|
|
46
|
+
*/
|
|
47
|
+
var dailyEnvelope = (config, expected = {}) => ({ "~standard": {
|
|
48
|
+
version: 1,
|
|
49
|
+
vendor: "joystyk",
|
|
50
|
+
validate: async (value) => {
|
|
51
|
+
const file = await dailyFile["~standard"].validate(value);
|
|
52
|
+
if (file.issues !== void 0) return { issues: file.issues };
|
|
53
|
+
const { puzzleId: id, date, contentHash: hash } = file.value;
|
|
54
|
+
const issues = [];
|
|
55
|
+
const ref = parsePuzzleId(id);
|
|
56
|
+
if (expected.gameId !== void 0 && ref?.gameId !== expected.gameId) issues.push(mismatch("puzzleId", `a puzzle for ${expected.gameId}`, id));
|
|
57
|
+
if (expected.date !== void 0 && date !== expected.date) issues.push(mismatch("date", expected.date, date));
|
|
58
|
+
if (expected.contentHash !== void 0 && hash !== expected.contentHash) issues.push(mismatch("contentHash", expected.contentHash, hash));
|
|
59
|
+
if (issues.length > 0) return { issues };
|
|
60
|
+
const decoded = await config["~standard"].validate(file.value.config);
|
|
61
|
+
if (decoded.issues !== void 0) return { issues: decoded.issues.map((issue) => ({
|
|
62
|
+
message: issue.message,
|
|
63
|
+
path: ["config", ...issue.path ?? []]
|
|
64
|
+
})) };
|
|
65
|
+
return { value: {
|
|
66
|
+
...file.value,
|
|
67
|
+
config: decoded.value
|
|
68
|
+
} };
|
|
69
|
+
}
|
|
70
|
+
} });
|
|
71
|
+
/** Decodes a game index. Each date appears once. */
|
|
72
|
+
var dailyIndex = z.object({
|
|
73
|
+
schemaVersion: z.literal(1),
|
|
74
|
+
gameId: z.string().check(z.refine(isGameId, "Expected a kebab-case game id")),
|
|
75
|
+
dates: z.array(z.object({
|
|
76
|
+
date: isoDate,
|
|
77
|
+
hash: contentHash,
|
|
78
|
+
size: z.int().check(z.nonnegative())
|
|
79
|
+
})).check(z.refine((dates) => new Set(dates.map((d) => d.date)).size === dates.length, "Each date may appear only once"))
|
|
80
|
+
});
|
|
81
|
+
/** Decodes a `DailyResult`. */
|
|
82
|
+
var dailyResult = z.object({
|
|
83
|
+
puzzleId: z.string().check(z.refine((id) => parsePuzzleId(id) !== null, "Expected an id like daily-word:2026-10-08")),
|
|
84
|
+
variant: z.nullable(variant),
|
|
85
|
+
contentHash,
|
|
86
|
+
outcome: z.enum([
|
|
87
|
+
"won",
|
|
88
|
+
"lost",
|
|
89
|
+
"completed"
|
|
90
|
+
]),
|
|
91
|
+
assisted: z.boolean(),
|
|
92
|
+
summary: z.string().check(z.maxLength(200)),
|
|
93
|
+
stats: z.record(z.string(), z.number().check(z.refine(Number.isFinite, "Expected a finite number"))),
|
|
94
|
+
shareRows: z.array(z.string().check(z.maxLength(200))).check(z.maxLength(50)),
|
|
95
|
+
shareDetails: z.optional(z.array(z.nullable(z.union([z.int().check(z.minimum(0), z.maximum(100)), z.literal("found")]))).check(z.maxLength(50))),
|
|
96
|
+
shareWords: z.optional(z.array(z.string().check(z.maxLength(1e3))).check(z.maxLength(50))),
|
|
97
|
+
finishedOn: isoDate
|
|
98
|
+
});
|
|
99
|
+
/**
|
|
100
|
+
* The date a result counts for: its puzzle's own date, even when it was finished after the
|
|
101
|
+
* next puzzle came out (a puzzle started is a puzzle kept).
|
|
102
|
+
*/
|
|
103
|
+
var creditDate = (result) => {
|
|
104
|
+
const ref = parsePuzzleId(result.puzzleId);
|
|
105
|
+
if (ref === null) throw new RangeError(`Not a daily puzzle id: ${JSON.stringify(result.puzzleId)}.`);
|
|
106
|
+
return ref.date;
|
|
107
|
+
};
|
|
108
|
+
var compareCodeUnits = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
109
|
+
/**
|
|
110
|
+
* JSON with object keys sorted by UTF-16 code unit and no whitespace. The content hash is the
|
|
111
|
+
* first 12 hex digits of SHA-256 over the canonical JSON of a puzzle file's `config`.
|
|
112
|
+
*/
|
|
113
|
+
var canonicalJson = (value) => {
|
|
114
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
115
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
116
|
+
return `{${Object.keys(value).sort(compareCodeUnits).map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
117
|
+
};
|
|
118
|
+
//#endregion
|
|
119
|
+
export { CONTENT_HASH, DAILY_SCHEMA_VERSION, VARIANT, canonicalJson, creditDate, dailyEnvelope, dailyFile, dailyIndex, dailyResult, decode, json };
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=schemas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.js","names":[],"sources":["../src/schemas.ts"],"sourcesContent":["// Standard Schemas (Zod 4 mini, D-22) for everything daily that crosses a boundary: the\n// puzzle file and the game index the site serves, and the result a player keeps\n// (content-pipeline.md, \"Delivery\" and \"Results and streaks\").\n\nimport {\n err,\n isGameId,\n isJson,\n ok,\n type Json,\n type Result,\n type StandardSchemaV1,\n type Validator,\n} from '@joystyk/contracts';\nimport { isIsoDate } from '@joystyk/kit';\nimport * as z from 'zod/mini';\nimport { parsePuzzleId } from './ids.ts';\n\n/** One validation problem, flattened for logs: `path: message`. */\nexport type Issue = string;\n\nconst describe = (issue: StandardSchemaV1.Issue, prefix: readonly PropertyKey[] = []): Issue => {\n const path = [\n ...prefix,\n ...(issue.path ?? []).map((segment) => (typeof segment === 'object' ? segment.key : segment)),\n ]\n .map(String)\n .join('.');\n return path === '' ? issue.message : `${path}: ${issue.message}`;\n};\n\n/** Runs any Standard Schema, sync or async, and returns its output or its issues. */\nexport const decode = async <Output>(\n schema: StandardSchemaV1<unknown, Output>,\n value: unknown,\n): Promise<Result<Output, readonly Issue[]>> => {\n const result = await schema['~standard'].validate(value);\n return result.issues === undefined ? ok(result.value) : err(result.issues.map((issue) => describe(issue)));\n};\n\n/** Any JSON value, checked with the contracts' `isJson`. */\nexport const json = z.custom<Json>(isJson, { error: 'Expected a JSON value' });\n\nconst isoDate = z.string().check(z.refine(isIsoDate, 'Expected a real date written yyyy-mm-dd'));\n\n/** A content hash: the first 12 hex digits of SHA-256 over the canonical JSON of the puzzle. */\nexport const CONTENT_HASH = /^[0-9a-f]{12}$/;\nconst contentHash = z.string().check(z.regex(CONTENT_HASH, 'Expected 12 lowercase hex digits'));\n\n/** A variant name such as \"quick\" or \"standard\": lowercase kebab-case, at most 32 characters. */\nexport const VARIANT = /^[a-z0-9]+(-[a-z0-9]+)*$/;\nconst variant = z.string().check(z.maxLength(32), z.regex(VARIANT, 'Expected a kebab-case variant name'));\n\n/** The current version of the puzzle file and index formats. */\nexport const DAILY_SCHEMA_VERSION = 1;\n\n// ---------------------------------------------------------------------------------------------\n// The puzzle file: /daily/<game>/<date>.<hash>.json\n\n/**\n * One day's puzzle file as the site serves it: the game's `Config` for the day (every variant)\n * wrapped with its id, date, hash and format version. `config` is still untrusted JSON here;\n * `dailyEnvelope` decodes it with the game's own schema.\n */\nexport type DailyFile = {\n readonly schemaVersion: typeof DAILY_SCHEMA_VERSION;\n readonly puzzleId: string;\n readonly date: string;\n readonly contentHash: string;\n readonly config: Json;\n};\n\n/** A puzzle file whose `config` has passed the game's `schemas.config`. */\nexport type DailyEnvelope<Config> = Omit<DailyFile, 'config'> & { readonly config: Config };\n\n/** Decodes the puzzle file's wrapper. The id's date must match `date`. */\nexport const dailyFile: z.ZodMiniType<DailyFile> = z\n .object({\n schemaVersion: z.literal(DAILY_SCHEMA_VERSION),\n puzzleId: z\n .string()\n .check(z.refine((id) => parsePuzzleId(id) !== null, 'Expected an id like daily-word:2026-10-08')),\n date: isoDate,\n contentHash,\n config: json,\n })\n .check(\n z.refine((file) => parsePuzzleId(file.puzzleId)?.date === file.date, {\n message: \"The puzzle id's date doesn't match the file's date\",\n path: ['puzzleId'],\n }),\n );\n\n/** What the caller knows before decoding a puzzle file, from the URL it asked for. */\nexport type EnvelopeExpectations = {\n readonly gameId?: string;\n readonly date?: string;\n readonly contentHash?: string;\n};\n\nconst mismatch = (field: keyof DailyFile, expected: string, actual: string): StandardSchemaV1.Issue => ({\n message: `Expected ${expected}, got ${actual}`,\n path: [field],\n});\n\n/**\n * A Standard Schema for one game's puzzle file: the wrapper, then `config` through the game's\n * own `schemas.config` (any Standard Schema, sync or async). With `expected`, it also refuses\n * a file for another game, date or hash than the one requested. Issue paths inside the config\n * start with `config`.\n */\nexport const dailyEnvelope = <Config>(\n config: Validator<Config>,\n expected: EnvelopeExpectations = {},\n): StandardSchemaV1<unknown, DailyEnvelope<Config>> => ({\n '~standard': {\n version: 1,\n vendor: 'joystyk',\n validate: async (value) => {\n const file = await dailyFile['~standard'].validate(value);\n if (file.issues !== undefined) return { issues: file.issues };\n const { puzzleId: id, date, contentHash: hash } = file.value;\n const issues: StandardSchemaV1.Issue[] = [];\n const ref = parsePuzzleId(id);\n if (expected.gameId !== undefined && ref?.gameId !== expected.gameId) {\n issues.push(mismatch('puzzleId', `a puzzle for ${expected.gameId}`, id));\n }\n if (expected.date !== undefined && date !== expected.date) issues.push(mismatch('date', expected.date, date));\n if (expected.contentHash !== undefined && hash !== expected.contentHash) {\n issues.push(mismatch('contentHash', expected.contentHash, hash));\n }\n if (issues.length > 0) return { issues };\n const decoded = await config['~standard'].validate(file.value.config);\n if (decoded.issues !== undefined) {\n return {\n issues: decoded.issues.map((issue) => ({ message: issue.message, path: ['config', ...(issue.path ?? [])] })),\n };\n }\n return { value: { ...file.value, config: decoded.value } };\n },\n },\n});\n\n// ---------------------------------------------------------------------------------------------\n// The game index: /daily/<game>/index.json\n\n/** One published date in a game's index. */\nexport type DailyIndexEntry = {\n readonly date: string;\n readonly hash: string;\n /** The file's size in bytes. */\n readonly size: number;\n};\n\n/**\n * The dates a game has published, served `no-cache`. It lists dates up to tomorrow's UTC date\n * (`latestListedDate`), each with its content hash, which names the file.\n */\nexport type DailyIndex = {\n readonly schemaVersion: typeof DAILY_SCHEMA_VERSION;\n readonly gameId: string;\n readonly dates: readonly DailyIndexEntry[];\n};\n\n/** Decodes a game index. Each date appears once. */\nexport const dailyIndex: z.ZodMiniType<DailyIndex> = z.object({\n schemaVersion: z.literal(DAILY_SCHEMA_VERSION),\n gameId: z.string().check(z.refine(isGameId, 'Expected a kebab-case game id')),\n dates: z\n .array(z.object({ date: isoDate, hash: contentHash, size: z.int().check(z.nonnegative()) }))\n .check(\n z.refine((dates) => new Set(dates.map((d) => d.date)).size === dates.length, 'Each date may appear only once'),\n ),\n});\n\n// ---------------------------------------------------------------------------------------------\n// Results\n\n/** How a daily puzzle ended. Finishing means the engine reached an outcome. */\nexport type DailyOutcome = 'won' | 'lost' | 'completed';\n\n/** One finished daily puzzle, kept per puzzle id and variant (content-pipeline.md). */\nexport type DailyResult = {\n /** \"daily-word:2026-10-08\". Its date is the date the result counts for. */\n readonly puzzleId: string;\n /** \"quick\", \"standard\", … or null for a game without variants. */\n readonly variant: string | null;\n readonly contentHash: string;\n readonly outcome: DailyOutcome;\n /** Hints, checks or reveals were used, as each game defines them. */\n readonly assisted: boolean;\n /** The outcome's summary, e.g. \"4/6\". */\n readonly summary: string;\n readonly stats: Readonly<Record<string, number>>;\n /** Spoiler-free symbol rows from the game, e.g. \"⬜🟨⬜⬜⬜\". */\n readonly shareRows: readonly string[];\n /** The same rows in words, for *Copy as text*: \"Guess 1: 1 wrong place\". Absent in older results. */\n readonly shareWords?: readonly string[];\n /** Separate validated annotations; absent in legacy records. */\n readonly shareDetails?: readonly (number | 'found' | null)[];\n /** The UTC date when it finished, `yyyy-mm-dd`. May be after the puzzle's date. */\n readonly finishedOn: string;\n};\n\n/** Decodes a `DailyResult`. */\nexport const dailyResult: z.ZodMiniType<DailyResult> = z.object({\n puzzleId: z.string().check(z.refine((id) => parsePuzzleId(id) !== null, 'Expected an id like daily-word:2026-10-08')),\n variant: z.nullable(variant),\n contentHash,\n outcome: z.enum(['won', 'lost', 'completed']),\n assisted: z.boolean(),\n summary: z.string().check(z.maxLength(200)),\n stats: z.record(z.string(), z.number().check(z.refine(Number.isFinite, 'Expected a finite number'))),\n shareRows: z.array(z.string().check(z.maxLength(200))).check(z.maxLength(50)),\n shareDetails: z.optional(\n z\n .array(z.nullable(z.union([z.int().check(z.minimum(0), z.maximum(100)), z.literal('found')])))\n .check(z.maxLength(50)),\n ),\n // Spoken move histories may describe 25 swaps or nine golf holes in one row.\n shareWords: z.optional(z.array(z.string().check(z.maxLength(1000))).check(z.maxLength(50))),\n finishedOn: isoDate,\n});\n\n/**\n * The date a result counts for: its puzzle's own date, even when it was finished after the\n * next puzzle came out (a puzzle started is a puzzle kept).\n */\nexport const creditDate = (result: Pick<DailyResult, 'puzzleId'>): string => {\n const ref = parsePuzzleId(result.puzzleId);\n if (ref === null) throw new RangeError(`Not a daily puzzle id: ${JSON.stringify(result.puzzleId)}.`);\n return ref.date;\n};\n\n// ---------------------------------------------------------------------------------------------\n// Canonical JSON for content hashes\n\nconst compareCodeUnits = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);\n\n/**\n * JSON with object keys sorted by UTF-16 code unit and no whitespace. The content hash is the\n * first 12 hex digits of SHA-256 over the canonical JSON of a puzzle file's `config`.\n */\nexport const canonicalJson = (value: Json): string => {\n if (value === null || typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;\n const entries = Object.keys(value)\n .sort(compareCodeUnits)\n .map((key) => `${JSON.stringify(key)}:${canonicalJson((value as Readonly<Record<string, Json>>)[key] as Json)}`);\n return `{${entries.join(',')}}`;\n};\n"],"mappings":"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"}
|
package/dist/share.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One row of a result grid, in two forms: emoji symbols for *Share*, and the same row in
|
|
3
|
+
* plain words for *Copy as text*, which screen readers read well.
|
|
4
|
+
*/
|
|
5
|
+
export type ShareRow = {
|
|
6
|
+
/** Symbols only, e.g. "⬜🟨⬜⬜⬜". Letters and bare digits are refused; numeric keycap emoji are symbols. */
|
|
7
|
+
readonly symbols: string;
|
|
8
|
+
/** The row in words, e.g. "Guess 1: 1 wrong place". */
|
|
9
|
+
readonly words: string;
|
|
10
|
+
/** Optional spoiler-free suffix: a whole percentage (0–100) or the fixed word found. */
|
|
11
|
+
readonly detail?: number | 'found';
|
|
12
|
+
};
|
|
13
|
+
export type ShareInput = {
|
|
14
|
+
/** The game's display name, e.g. "Daily Word". */
|
|
15
|
+
readonly title: string;
|
|
16
|
+
/** The puzzle's date, `yyyy-mm-dd`. Shown in the header unless `number` is given. */
|
|
17
|
+
readonly date: string;
|
|
18
|
+
/** A puzzle number from `puzzleNumber`, shown as "#412" in place of the date. */
|
|
19
|
+
readonly number?: number;
|
|
20
|
+
/** The variant's display name, shown in brackets: "Quick". */
|
|
21
|
+
readonly variant?: string | null;
|
|
22
|
+
/** A short result that ends the header line, e.g. "4/6" or "4/6 (hard)". */
|
|
23
|
+
readonly headline?: string;
|
|
24
|
+
/** A result line of its own, e.g. "Solved in 1:42, no help". */
|
|
25
|
+
readonly summary?: string;
|
|
26
|
+
readonly rows?: readonly ShareRow[];
|
|
27
|
+
/** The game page's absolute URL, with no query string or fragment. */
|
|
28
|
+
readonly url?: string;
|
|
29
|
+
};
|
|
30
|
+
/** `symbols`: the emoji form for *Share*. `words`: the plain form for *Copy as text*. */
|
|
31
|
+
export type ShareForm = 'symbols' | 'words';
|
|
32
|
+
/**
|
|
33
|
+
* The text to share, in either form. Lines: a header (`<title> <date or #number>`, the
|
|
34
|
+
* variant in brackets, then the headline), the summary line, the rows and the URL; absent
|
|
35
|
+
* parts are left out. In the `words` form each row is a sentence on its own line.
|
|
36
|
+
*
|
|
37
|
+
* Throws a `RangeError` for a symbol row containing a letter or bare digit, a field with a line
|
|
38
|
+
* break, or a URL with a query string or fragment: each is a bug that could leak a spoiler or
|
|
39
|
+
* a tracking parameter.
|
|
40
|
+
*/
|
|
41
|
+
export declare const shareText: (input: ShareInput, form?: ShareForm) => string;
|
|
42
|
+
/** How one kind of cell appears: its symbol, and its name in words. */
|
|
43
|
+
export type ShareLegendEntry = {
|
|
44
|
+
readonly symbol: string;
|
|
45
|
+
/** E.g. "right place". Leave it out for cells the words form shouldn't count ("not in word"). */
|
|
46
|
+
readonly words?: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Turns a grid of cell kinds into share rows. Each row's words form counts the cells of each
|
|
50
|
+
* kind that has `words`, in the legend's order: "Guess 2: 1 right place, 2 wrong place". A row
|
|
51
|
+
* with nothing to count reads `<label>: <empty>`.
|
|
52
|
+
*/
|
|
53
|
+
export declare const gridRows: <Kind extends string>(grid: readonly (readonly Kind[])[], legend: Readonly<Record<Kind, ShareLegendEntry>>, label: (index: number) => string, empty?: string) => ShareRow[];
|
|
54
|
+
/** A play time for share text and results: "0:07", "1:42", "1:02:05". Whole seconds, rounded down. */
|
|
55
|
+
export declare const formatClock: (ms: number) => string;
|
package/dist/share.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { isIsoDate } from "./dates.js";
|
|
2
|
+
//#region src/share.ts
|
|
3
|
+
var NUMERIC_KEYCAP = /[0-9]\uFE0F?\u20E3/gu;
|
|
4
|
+
var LETTER_OR_DIGIT = /[\p{L}\p{N}]/u;
|
|
5
|
+
var LINE_BREAK = /[\r\n\p{Zl}\p{Zp}]/u;
|
|
6
|
+
var PAGE_URL = /^https?:\/\/[^\s?#]+$/;
|
|
7
|
+
var singleLine = (field, value) => {
|
|
8
|
+
if (LINE_BREAK.test(value)) throw new RangeError(`Share text ${field} must be a single line.`);
|
|
9
|
+
return value.trim();
|
|
10
|
+
};
|
|
11
|
+
var sentence = (text) => /[.!?…]$/.test(text) ? text : `${text}.`;
|
|
12
|
+
/**
|
|
13
|
+
* The text to share, in either form. Lines: a header (`<title> <date or #number>`, the
|
|
14
|
+
* variant in brackets, then the headline), the summary line, the rows and the URL; absent
|
|
15
|
+
* parts are left out. In the `words` form each row is a sentence on its own line.
|
|
16
|
+
*
|
|
17
|
+
* Throws a `RangeError` for a symbol row containing a letter or bare digit, a field with a line
|
|
18
|
+
* break, or a URL with a query string or fragment: each is a bug that could leak a spoiler or
|
|
19
|
+
* a tracking parameter.
|
|
20
|
+
*/
|
|
21
|
+
var shareText = (input, form = "symbols") => {
|
|
22
|
+
if (!isIsoDate(input.date)) throw new RangeError(`Expected a "yyyy-mm-dd" date, got ${JSON.stringify(input.date)}.`);
|
|
23
|
+
const label = input.number === void 0 ? input.date : `#${input.number}`;
|
|
24
|
+
if (input.number !== void 0 && !Number.isSafeInteger(input.number)) throw new RangeError(`Expected a whole puzzle number, got ${input.number}.`);
|
|
25
|
+
const lines = [[
|
|
26
|
+
singleLine("title", input.title),
|
|
27
|
+
label,
|
|
28
|
+
...input.variant == null || input.variant === "" ? [] : [`(${singleLine("variant", input.variant)})`],
|
|
29
|
+
...input.headline === void 0 || input.headline === "" ? [] : [singleLine("headline", input.headline)]
|
|
30
|
+
].join(" ")];
|
|
31
|
+
if (input.summary !== void 0 && input.summary !== "") lines.push(singleLine("summary", input.summary));
|
|
32
|
+
for (const [index, row] of (input.rows ?? []).entries()) {
|
|
33
|
+
const symbols = singleLine(`row ${index + 1}`, row.symbols);
|
|
34
|
+
if (LETTER_OR_DIGIT.test(symbols.replace(NUMERIC_KEYCAP, ""))) throw new RangeError(`Share row ${index + 1} must hold symbols only, never letters or digits.`);
|
|
35
|
+
if (row.detail !== void 0 && row.detail !== "found" && (!Number.isInteger(row.detail) || row.detail < 0 || row.detail > 100)) throw new RangeError("Share detail must be a whole percentage from 0 to 100 or found.");
|
|
36
|
+
const detail = row.detail === void 0 ? "" : row.detail === "found" ? " found" : ` ${row.detail}%`;
|
|
37
|
+
lines.push(form === "symbols" ? symbols + detail : sentence(singleLine(`row ${index + 1} words`, row.words)));
|
|
38
|
+
}
|
|
39
|
+
if (input.url !== void 0) {
|
|
40
|
+
if (!PAGE_URL.test(input.url)) throw new RangeError("The share URL must be an absolute http(s) URL with no query string or fragment.");
|
|
41
|
+
lines.push(input.url);
|
|
42
|
+
}
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Turns a grid of cell kinds into share rows. Each row's words form counts the cells of each
|
|
47
|
+
* kind that has `words`, in the legend's order: "Guess 2: 1 right place, 2 wrong place". A row
|
|
48
|
+
* with nothing to count reads `<label>: <empty>`.
|
|
49
|
+
*/
|
|
50
|
+
var gridRows = (grid, legend, label, empty = "none") => grid.map((cells, index) => {
|
|
51
|
+
const counts = Object.keys(legend).flatMap((kind) => {
|
|
52
|
+
const words = legend[kind].words;
|
|
53
|
+
const count = cells.filter((cell) => cell === kind).length;
|
|
54
|
+
return words === void 0 || count === 0 ? [] : [`${count} ${words}`];
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
symbols: cells.map((cell) => legend[cell].symbol).join(""),
|
|
58
|
+
words: `${label(index)}: ${counts.length === 0 ? empty : counts.join(", ")}`
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
/** A play time for share text and results: "0:07", "1:42", "1:02:05". Whole seconds, rounded down. */
|
|
62
|
+
var formatClock = (ms) => {
|
|
63
|
+
if (!Number.isFinite(ms) || ms < 0) throw new RangeError(`Expected a duration in milliseconds, got ${ms}.`);
|
|
64
|
+
const total = Math.floor(ms / 1e3);
|
|
65
|
+
const hours = Math.floor(total / 3600);
|
|
66
|
+
const minutes = Math.floor(total % 3600 / 60);
|
|
67
|
+
const seconds = String(total % 60).padStart(2, "0");
|
|
68
|
+
return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
export { formatClock, gridRows, shareText };
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=share.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share.js","names":[],"sources":["../src/share.ts"],"sourcesContent":["// Spoiler-free share text (content-pipeline.md, \"Share text\"). The shape is shared; each game\n// supplies its result line and symbol rows. Share text never carries answer letters, clue\n// text, theme words, the player's name or a tracking parameter.\n\nimport { isIsoDate } from './dates.ts';\n\n/**\n * One row of a result grid, in two forms: emoji symbols for *Share*, and the same row in\n * plain words for *Copy as text*, which screen readers read well.\n */\nexport type ShareRow = {\n /** Symbols only, e.g. \"⬜🟨⬜⬜⬜\". Letters and bare digits are refused; numeric keycap emoji are symbols. */\n readonly symbols: string;\n /** The row in words, e.g. \"Guess 1: 1 wrong place\". */\n readonly words: string;\n /** Optional spoiler-free suffix: a whole percentage (0–100) or the fixed word found. */\n readonly detail?: number | 'found';\n};\n\nexport type ShareInput = {\n /** The game's display name, e.g. \"Daily Word\". */\n readonly title: string;\n /** The puzzle's date, `yyyy-mm-dd`. Shown in the header unless `number` is given. */\n readonly date: string;\n /** A puzzle number from `puzzleNumber`, shown as \"#412\" in place of the date. */\n readonly number?: number;\n /** The variant's display name, shown in brackets: \"Quick\". */\n readonly variant?: string | null;\n /** A short result that ends the header line, e.g. \"4/6\" or \"4/6 (hard)\". */\n readonly headline?: string;\n /** A result line of its own, e.g. \"Solved in 1:42, no help\". */\n readonly summary?: string;\n readonly rows?: readonly ShareRow[];\n /** The game page's absolute URL, with no query string or fragment. */\n readonly url?: string;\n};\n\n/** `symbols`: the emoji form for *Share*. `words`: the plain form for *Copy as text*. */\nexport type ShareForm = 'symbols' | 'words';\n\nconst NUMERIC_KEYCAP = /[0-9]\\uFE0F?\\u20E3/gu;\nconst LETTER_OR_DIGIT = /[\\p{L}\\p{N}]/u;\nconst LINE_BREAK = /[\\r\\n\\p{Zl}\\p{Zp}]/u;\nconst PAGE_URL = /^https?:\\/\\/[^\\s?#]+$/;\n\nconst singleLine = (field: string, value: string): string => {\n if (LINE_BREAK.test(value)) throw new RangeError(`Share text ${field} must be a single line.`);\n return value.trim();\n};\n\nconst sentence = (text: string): string => (/[.!?…]$/.test(text) ? text : `${text}.`);\n\n/**\n * The text to share, in either form. Lines: a header (`<title> <date or #number>`, the\n * variant in brackets, then the headline), the summary line, the rows and the URL; absent\n * parts are left out. In the `words` form each row is a sentence on its own line.\n *\n * Throws a `RangeError` for a symbol row containing a letter or bare digit, a field with a line\n * break, or a URL with a query string or fragment: each is a bug that could leak a spoiler or\n * a tracking parameter.\n */\nexport const shareText = (input: ShareInput, form: ShareForm = 'symbols'): string => {\n if (!isIsoDate(input.date)) throw new RangeError(`Expected a \"yyyy-mm-dd\" date, got ${JSON.stringify(input.date)}.`);\n const label = input.number === undefined ? input.date : `#${input.number}`;\n if (input.number !== undefined && !Number.isSafeInteger(input.number)) {\n throw new RangeError(`Expected a whole puzzle number, got ${input.number}.`);\n }\n const header = [\n singleLine('title', input.title),\n label,\n ...(input.variant == null || input.variant === '' ? [] : [`(${singleLine('variant', input.variant)})`]),\n ...(input.headline === undefined || input.headline === '' ? [] : [singleLine('headline', input.headline)]),\n ].join(' ');\n\n const lines = [header];\n if (input.summary !== undefined && input.summary !== '') lines.push(singleLine('summary', input.summary));\n for (const [index, row] of (input.rows ?? []).entries()) {\n const symbols = singleLine(`row ${index + 1}`, row.symbols);\n if (LETTER_OR_DIGIT.test(symbols.replace(NUMERIC_KEYCAP, ''))) {\n throw new RangeError(`Share row ${index + 1} must hold symbols only, never letters or digits.`);\n }\n if (\n row.detail !== undefined &&\n row.detail !== 'found' &&\n (!Number.isInteger(row.detail) || row.detail < 0 || row.detail > 100)\n )\n throw new RangeError('Share detail must be a whole percentage from 0 to 100 or found.');\n const detail = row.detail === undefined ? '' : row.detail === 'found' ? ' found' : ` ${row.detail}%`;\n lines.push(form === 'symbols' ? symbols + detail : sentence(singleLine(`row ${index + 1} words`, row.words)));\n }\n if (input.url !== undefined) {\n if (!PAGE_URL.test(input.url)) {\n throw new RangeError('The share URL must be an absolute http(s) URL with no query string or fragment.');\n }\n lines.push(input.url);\n }\n return lines.join('\\n');\n};\n\n/** How one kind of cell appears: its symbol, and its name in words. */\nexport type ShareLegendEntry = {\n readonly symbol: string;\n /** E.g. \"right place\". Leave it out for cells the words form shouldn't count (\"not in word\"). */\n readonly words?: string;\n};\n\n/**\n * Turns a grid of cell kinds into share rows. Each row's words form counts the cells of each\n * kind that has `words`, in the legend's order: \"Guess 2: 1 right place, 2 wrong place\". A row\n * with nothing to count reads `<label>: <empty>`.\n */\nexport const gridRows = <Kind extends string>(\n grid: readonly (readonly Kind[])[],\n legend: Readonly<Record<Kind, ShareLegendEntry>>,\n label: (index: number) => string,\n empty = 'none',\n): ShareRow[] =>\n grid.map((cells, index) => {\n const counts = (Object.keys(legend) as Kind[]).flatMap((kind) => {\n const words = legend[kind].words;\n const count = cells.filter((cell) => cell === kind).length;\n return words === undefined || count === 0 ? [] : [`${count} ${words}`];\n });\n return {\n symbols: cells.map((cell) => legend[cell].symbol).join(''),\n words: `${label(index)}: ${counts.length === 0 ? empty : counts.join(', ')}`,\n };\n });\n\n/** A play time for share text and results: \"0:07\", \"1:42\", \"1:02:05\". Whole seconds, rounded down. */\nexport const formatClock = (ms: number): string => {\n if (!Number.isFinite(ms) || ms < 0) throw new RangeError(`Expected a duration in milliseconds, got ${ms}.`);\n const total = Math.floor(ms / 1000);\n const hours = Math.floor(total / 3600);\n const minutes = Math.floor((total % 3600) / 60);\n const seconds = String(total % 60).padStart(2, '0');\n return hours > 0 ? `${hours}:${String(minutes).padStart(2, '0')}:${seconds}` : `${minutes}:${seconds}`;\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DailyResult } from './schemas.js';
|
|
2
|
+
/** A game's streaks on one day. */
|
|
3
|
+
export type Streaks = {
|
|
4
|
+
/** Consecutive puzzle dates finished, ending today or yesterday; 0 after a missed date. */
|
|
5
|
+
readonly current: number;
|
|
6
|
+
/** The longest run of consecutive dates finished. */
|
|
7
|
+
readonly best: number;
|
|
8
|
+
/** Consecutive puzzle dates won, ending today or yesterday. Only shown by games that can be lost. */
|
|
9
|
+
readonly winCurrent: number;
|
|
10
|
+
/** The longest run of consecutive dates won. */
|
|
11
|
+
readonly winBest: number;
|
|
12
|
+
/** The newest puzzle date finished, or null. */
|
|
13
|
+
readonly lastDay: string | null;
|
|
14
|
+
};
|
|
15
|
+
export declare const NO_STREAKS: Streaks;
|
|
16
|
+
/**
|
|
17
|
+
* One game's streaks on `today`, today's UTC date, from that game's results. Streaks count
|
|
18
|
+
* UTC days (D-46).
|
|
19
|
+
*
|
|
20
|
+
* - A date counts as finished when any variant of its puzzle reached an outcome (won, lost or
|
|
21
|
+
* completed, with or without help), and as won when any variant was won.
|
|
22
|
+
* - Each result counts for its puzzle's own date, so finishing yesterday's puzzle after the
|
|
23
|
+
* next release still extends yesterday.
|
|
24
|
+
* - Today not yet played leaves the current streak standing; any missed date before it sets
|
|
25
|
+
* the current streak to 0. A lost day ends the win streak. Days skipped by a changed clock
|
|
26
|
+
* count as missed; repeated dates count once.
|
|
27
|
+
* - Results for dates after `today` (a clock that was set ahead) count toward the best
|
|
28
|
+
* streaks but not the current ones.
|
|
29
|
+
* - Results whose id isn't a daily puzzle id are ignored.
|
|
30
|
+
*/
|
|
31
|
+
export declare const streaks: (results: Iterable<Pick<DailyResult, "puzzleId" | "outcome">>, today: string) => Streaks;
|
package/dist/streaks.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { dayNumber, isIsoDate } from "./dates.js";
|
|
2
|
+
import { parsePuzzleId } from "./ids.js";
|
|
3
|
+
//#region src/streaks.ts
|
|
4
|
+
var NO_STREAKS = {
|
|
5
|
+
current: 0,
|
|
6
|
+
best: 0,
|
|
7
|
+
winCurrent: 0,
|
|
8
|
+
winBest: 0,
|
|
9
|
+
lastDay: null
|
|
10
|
+
};
|
|
11
|
+
var longestRun = (days) => {
|
|
12
|
+
let best = 0;
|
|
13
|
+
for (const day of days) {
|
|
14
|
+
if (days.has(day - 1)) continue;
|
|
15
|
+
let length = 1;
|
|
16
|
+
while (days.has(day + length)) length += 1;
|
|
17
|
+
best = Math.max(best, length);
|
|
18
|
+
}
|
|
19
|
+
return best;
|
|
20
|
+
};
|
|
21
|
+
var runEndingAt = (days, last) => {
|
|
22
|
+
let length = 0;
|
|
23
|
+
while (days.has(last - length)) length += 1;
|
|
24
|
+
return length;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* One game's streaks on `today`, today's UTC date, from that game's results. Streaks count
|
|
28
|
+
* UTC days (D-46).
|
|
29
|
+
*
|
|
30
|
+
* - A date counts as finished when any variant of its puzzle reached an outcome (won, lost or
|
|
31
|
+
* completed, with or without help), and as won when any variant was won.
|
|
32
|
+
* - Each result counts for its puzzle's own date, so finishing yesterday's puzzle after the
|
|
33
|
+
* next release still extends yesterday.
|
|
34
|
+
* - Today not yet played leaves the current streak standing; any missed date before it sets
|
|
35
|
+
* the current streak to 0. A lost day ends the win streak. Days skipped by a changed clock
|
|
36
|
+
* count as missed; repeated dates count once.
|
|
37
|
+
* - Results for dates after `today` (a clock that was set ahead) count toward the best
|
|
38
|
+
* streaks but not the current ones.
|
|
39
|
+
* - Results whose id isn't a daily puzzle id are ignored.
|
|
40
|
+
*/
|
|
41
|
+
var streaks = (results, today) => {
|
|
42
|
+
if (!isIsoDate(today)) throw new RangeError(`Expected a "yyyy-mm-dd" date, got ${JSON.stringify(today)}.`);
|
|
43
|
+
const finished = /* @__PURE__ */ new Set();
|
|
44
|
+
const won = /* @__PURE__ */ new Set();
|
|
45
|
+
let lastDay = null;
|
|
46
|
+
for (const result of results) {
|
|
47
|
+
const ref = parsePuzzleId(result.puzzleId);
|
|
48
|
+
if (ref === null) continue;
|
|
49
|
+
const day = dayNumber(ref.date);
|
|
50
|
+
finished.add(day);
|
|
51
|
+
if (result.outcome === "won") won.add(day);
|
|
52
|
+
if (lastDay === null || ref.date > lastDay) lastDay = ref.date;
|
|
53
|
+
}
|
|
54
|
+
const t = dayNumber(today);
|
|
55
|
+
const anchor = (days) => days.has(t) ? t : days.has(t - 1) ? t - 1 : null;
|
|
56
|
+
const finishedAnchor = anchor(finished);
|
|
57
|
+
const winAnchor = finished.has(t) && !won.has(t) ? null : anchor(won);
|
|
58
|
+
return {
|
|
59
|
+
current: finishedAnchor === null ? 0 : runEndingAt(finished, finishedAnchor),
|
|
60
|
+
best: longestRun(finished),
|
|
61
|
+
winCurrent: winAnchor === null ? 0 : runEndingAt(won, winAnchor),
|
|
62
|
+
winBest: longestRun(won),
|
|
63
|
+
lastDay
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
export { NO_STREAKS, streaks };
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=streaks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaks.js","names":[],"sources":["../src/streaks.ts"],"sourcesContent":["// The streak rules, the same for every daily game (content-pipeline.md, \"Results and\n// streaks\"). Streaks are computed from stored results by this pure function, never kept as\n// a counter that could drift. There are no freezes, no grace days and nothing to buy.\n\nimport { dayNumber, isIsoDate } from './dates.ts';\nimport { parsePuzzleId } from './ids.ts';\nimport type { DailyResult } from './schemas.ts';\n\n/** A game's streaks on one day. */\nexport type Streaks = {\n /** Consecutive puzzle dates finished, ending today or yesterday; 0 after a missed date. */\n readonly current: number;\n /** The longest run of consecutive dates finished. */\n readonly best: number;\n /** Consecutive puzzle dates won, ending today or yesterday. Only shown by games that can be lost. */\n readonly winCurrent: number;\n /** The longest run of consecutive dates won. */\n readonly winBest: number;\n /** The newest puzzle date finished, or null. */\n readonly lastDay: string | null;\n};\n\nexport const NO_STREAKS: Streaks = { current: 0, best: 0, winCurrent: 0, winBest: 0, lastDay: null };\n\nconst longestRun = (days: ReadonlySet<number>): number => {\n let best = 0;\n for (const day of days) {\n if (days.has(day - 1)) continue; // not the start of a run\n let length = 1;\n while (days.has(day + length)) length += 1;\n best = Math.max(best, length);\n }\n return best;\n};\n\nconst runEndingAt = (days: ReadonlySet<number>, last: number): number => {\n let length = 0;\n while (days.has(last - length)) length += 1;\n return length;\n};\n\n/**\n * One game's streaks on `today`, today's UTC date, from that game's results. Streaks count\n * UTC days (D-46).\n *\n * - A date counts as finished when any variant of its puzzle reached an outcome (won, lost or\n * completed, with or without help), and as won when any variant was won.\n * - Each result counts for its puzzle's own date, so finishing yesterday's puzzle after the\n * next release still extends yesterday.\n * - Today not yet played leaves the current streak standing; any missed date before it sets\n * the current streak to 0. A lost day ends the win streak. Days skipped by a changed clock\n * count as missed; repeated dates count once.\n * - Results for dates after `today` (a clock that was set ahead) count toward the best\n * streaks but not the current ones.\n * - Results whose id isn't a daily puzzle id are ignored.\n */\nexport const streaks = (results: Iterable<Pick<DailyResult, 'puzzleId' | 'outcome'>>, today: string): Streaks => {\n if (!isIsoDate(today)) throw new RangeError(`Expected a \"yyyy-mm-dd\" date, got ${JSON.stringify(today)}.`);\n const finished = new Set<number>();\n const won = new Set<number>();\n let lastDay: string | null = null;\n for (const result of results) {\n const ref = parsePuzzleId(result.puzzleId);\n if (ref === null) continue;\n const day = dayNumber(ref.date);\n finished.add(day);\n if (result.outcome === 'won') won.add(day);\n if (lastDay === null || ref.date > lastDay) lastDay = ref.date;\n }\n\n const t = dayNumber(today);\n const anchor = (days: ReadonlySet<number>): number | null => (days.has(t) ? t : days.has(t - 1) ? t - 1 : null);\n\n const finishedAnchor = anchor(finished);\n // A day finished but not won ends the win streak, even today.\n const winAnchor = finished.has(t) && !won.has(t) ? null : anchor(won);\n\n return {\n current: finishedAnchor === null ? 0 : runEndingAt(finished, finishedAnchor),\n best: longestRun(finished),\n winCurrent: winAnchor === null ? 0 : runEndingAt(won, winAnchor),\n winBest: longestRun(won),\n lastDay,\n };\n};\n"],"mappings":";;;AAsBA,IAAa,aAAsB;CAAE,SAAS;CAAG,MAAM;CAAG,YAAY;CAAG,SAAS;CAAG,SAAS;AAAK;AAEnG,IAAM,cAAc,SAAsC;CACxD,IAAI,OAAO;CACX,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,KAAK,IAAI,MAAM,CAAC,GAAG;EACvB,IAAI,SAAS;EACb,OAAO,KAAK,IAAI,MAAM,MAAM,GAAG,UAAU;EACzC,OAAO,KAAK,IAAI,MAAM,MAAM;CAC9B;CACA,OAAO;AACT;AAEA,IAAM,eAAe,MAA2B,SAAyB;CACvE,IAAI,SAAS;CACb,OAAO,KAAK,IAAI,OAAO,MAAM,GAAG,UAAU;CAC1C,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,IAAa,WAAW,SAA8D,UAA2B;CAC/G,IAAI,CAAC,UAAU,KAAK,GAAG,MAAM,IAAI,WAAW,qCAAqC,KAAK,UAAU,KAAK,EAAE,EAAE;CACzG,MAAM,2BAAW,IAAI,IAAY;CACjC,MAAM,sBAAM,IAAI,IAAY;CAC5B,IAAI,UAAyB;CAC7B,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,MAAM,cAAc,OAAO,QAAQ;EACzC,IAAI,QAAQ,MAAM;EAClB,MAAM,MAAM,UAAU,IAAI,IAAI;EAC9B,SAAS,IAAI,GAAG;EAChB,IAAI,OAAO,YAAY,OAAO,IAAI,IAAI,GAAG;EACzC,IAAI,YAAY,QAAQ,IAAI,OAAO,SAAS,UAAU,IAAI;CAC5D;CAEA,MAAM,IAAI,UAAU,KAAK;CACzB,MAAM,UAAU,SAA8C,KAAK,IAAI,CAAC,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI;CAE1G,MAAM,iBAAiB,OAAO,QAAQ;CAEtC,MAAM,YAAY,SAAS,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,OAAO,OAAO,GAAG;CAEpE,OAAO;EACL,SAAS,mBAAmB,OAAO,IAAI,YAAY,UAAU,cAAc;EAC3E,MAAM,WAAW,QAAQ;EACzB,YAAY,cAAc,OAAO,IAAI,YAAY,KAAK,SAAS;EAC/D,SAAS,WAAW,GAAG;EACvB;CACF;AACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as z from 'zod/mini';
|
|
2
|
+
import type { DailyResult } from './schemas.js';
|
|
3
|
+
export declare const dailyWordStatsSchema: z.ZodMiniObject<{
|
|
4
|
+
played: z.ZodMiniInt;
|
|
5
|
+
won: z.ZodMiniInt;
|
|
6
|
+
winPercent: z.ZodMiniInt;
|
|
7
|
+
distribution: z.ZodMiniArray<z.ZodMiniInt>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export type DailyWordStats = z.infer<typeof dailyWordStatsSchema>;
|
|
10
|
+
/** Daily-only statistics from immutable first results; practice and future clock-skew
|
|
11
|
+
* records do not inflate them. Older records can use their spoiler-free row count. */
|
|
12
|
+
export declare const dailyWordStats: (results: Iterable<DailyResult>, today: string) => DailyWordStats;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { parsePuzzleId } from "./ids.js";
|
|
2
|
+
import { isIsoDate } from "@joystyk/kit";
|
|
3
|
+
import * as z from "zod/mini";
|
|
4
|
+
//#region src/word-stats.ts
|
|
5
|
+
var count = z.int().check(z.minimum(0));
|
|
6
|
+
var dailyWordStatsSchema = z.object({
|
|
7
|
+
played: count,
|
|
8
|
+
won: count,
|
|
9
|
+
winPercent: z.int().check(z.minimum(0), z.maximum(100)),
|
|
10
|
+
distribution: z.array(count).check(z.length(6))
|
|
11
|
+
}).check(z.refine((s) => s.won <= s.played && s.distribution.reduce((a, b) => a + b, 0) <= s.won && s.winPercent === (s.played === 0 ? 0 : Math.round(s.won * 100 / s.played)), "Inconsistent Daily Word statistics."));
|
|
12
|
+
/** Daily-only statistics from immutable first results; practice and future clock-skew
|
|
13
|
+
* records do not inflate them. Older records can use their spoiler-free row count. */
|
|
14
|
+
var dailyWordStats = (results, today) => {
|
|
15
|
+
if (!isIsoDate(today)) throw new RangeError("Expected a real statistics date.");
|
|
16
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17
|
+
const distribution = [
|
|
18
|
+
0,
|
|
19
|
+
0,
|
|
20
|
+
0,
|
|
21
|
+
0,
|
|
22
|
+
0,
|
|
23
|
+
0
|
|
24
|
+
];
|
|
25
|
+
let played = 0, won = 0;
|
|
26
|
+
for (const result of results) {
|
|
27
|
+
const ref = parsePuzzleId(result.puzzleId);
|
|
28
|
+
if (ref?.gameId !== "daily-word" || ref.date > today || seen.has(result.puzzleId) || !["won", "lost"].includes(result.outcome)) continue;
|
|
29
|
+
seen.add(result.puzzleId);
|
|
30
|
+
played += 1;
|
|
31
|
+
if (result.outcome !== "won") continue;
|
|
32
|
+
won += 1;
|
|
33
|
+
const n = result.stats["guesses"] ?? result.shareRows.length;
|
|
34
|
+
if (Number.isInteger(n) && n >= 1 && n <= 6) distribution[n - 1] += 1;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
played,
|
|
38
|
+
won,
|
|
39
|
+
winPercent: played === 0 ? 0 : Math.round(won * 100 / played),
|
|
40
|
+
distribution
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
export { dailyWordStats, dailyWordStatsSchema };
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=word-stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"word-stats.js","names":[],"sources":["../src/word-stats.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport { isIsoDate } from '@joystyk/kit';\nimport { parsePuzzleId } from './ids.ts';\nimport type { DailyResult } from './schemas.ts';\n\nconst count = z.int().check(z.minimum(0));\nexport const dailyWordStatsSchema = z\n .object({\n played: count,\n won: count,\n winPercent: z.int().check(z.minimum(0), z.maximum(100)),\n distribution: z.array(count).check(z.length(6)),\n })\n .check(\n z.refine(\n (s) =>\n s.won <= s.played &&\n s.distribution.reduce((a, b) => a + b, 0) <= s.won &&\n s.winPercent === (s.played === 0 ? 0 : Math.round((s.won * 100) / s.played)),\n 'Inconsistent Daily Word statistics.',\n ),\n );\nexport type DailyWordStats = z.infer<typeof dailyWordStatsSchema>;\n\n/** Daily-only statistics from immutable first results; practice and future clock-skew\n * records do not inflate them. Older records can use their spoiler-free row count. */\nexport const dailyWordStats = (results: Iterable<DailyResult>, today: string): DailyWordStats => {\n if (!isIsoDate(today)) throw new RangeError('Expected a real statistics date.');\n const seen = new Set<string>();\n const distribution = [0, 0, 0, 0, 0, 0];\n let played = 0,\n won = 0;\n for (const result of results) {\n const ref = parsePuzzleId(result.puzzleId);\n if (\n ref?.gameId !== 'daily-word' ||\n ref.date > today ||\n seen.has(result.puzzleId) ||\n !['won', 'lost'].includes(result.outcome)\n )\n continue;\n seen.add(result.puzzleId);\n played += 1;\n if (result.outcome !== 'won') continue;\n won += 1;\n const n = result.stats['guesses'] ?? result.shareRows.length;\n if (Number.isInteger(n) && n >= 1 && n <= 6) distribution[n - 1]! += 1;\n }\n return { played, won, winPercent: played === 0 ? 0 : Math.round((won * 100) / played), distribution };\n};\n"],"mappings":";;;;AAKA,IAAM,QAAQ,EAAE,IAAI,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AACxC,IAAa,uBAAuB,EACjC,OAAO;CACN,QAAQ;CACR,KAAK;CACL,YAAY,EAAE,IAAI,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,EAAE,QAAQ,GAAG,CAAC;CACtD,cAAc,EAAE,MAAM,KAAK,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAChD,CAAC,CAAC,CACD,MACC,EAAE,QACC,MACC,EAAE,OAAO,EAAE,UACX,EAAE,aAAa,QAAQ,GAAG,MAAM,IAAI,GAAG,CAAC,KAAK,EAAE,OAC/C,EAAE,gBAAgB,EAAE,WAAW,IAAI,IAAI,KAAK,MAAO,EAAE,MAAM,MAAO,EAAE,MAAM,IAC5E,qCACF,CACF;;;AAKF,IAAa,kBAAkB,SAAgC,UAAkC;CAC/F,IAAI,CAAC,UAAU,KAAK,GAAG,MAAM,IAAI,WAAW,kCAAkC;CAC9E,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,eAAe;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;CAAC;CACtC,IAAI,SAAS,GACX,MAAM;CACR,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,MAAM,cAAc,OAAO,QAAQ;EACzC,IACE,KAAK,WAAW,gBAChB,IAAI,OAAO,SACX,KAAK,IAAI,OAAO,QAAQ,KACxB,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,SAAS,OAAO,OAAO,GAExC;EACF,KAAK,IAAI,OAAO,QAAQ;EACxB,UAAU;EACV,IAAI,OAAO,YAAY,OAAO;EAC9B,OAAO;EACP,MAAM,IAAI,OAAO,MAAM,cAAc,OAAO,UAAU;EACtD,IAAI,OAAO,UAAU,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG,aAAa,IAAI,MAAO;CACvE;CACA,OAAO;EAAE;EAAQ;EAAK,YAAY,WAAW,IAAI,IAAI,KAAK,MAAO,MAAM,MAAO,MAAM;EAAG;CAAa;AACtG"}
|
package/package.json
CHANGED
|
@@ -1,20 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joystyk/daily",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Daily puzzles: the UTC puzzle day, release times in local time, puzzle ids, streaks, spoiler-free share text, and a content client with an offline cache.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./browser": {
|
|
14
|
+
"types": "./dist/browser/index.d.ts",
|
|
15
|
+
"default": "./dist/browser/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"zod": "4.6.5",
|
|
20
|
+
"@joystyk/contracts": "0.2.0",
|
|
21
|
+
"@joystyk/kit": "0.2.0",
|
|
22
|
+
"@joystyk/storage": "0.2.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "5.9.3",
|
|
26
|
+
"vite": "8.3.3",
|
|
27
|
+
"vitest": "4.1.11"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"NOTICE",
|
|
37
|
+
"CHANGELOG.md"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=24.15.0"
|
|
41
|
+
},
|
|
5
42
|
"license": "MIT",
|
|
6
43
|
"repository": {
|
|
7
44
|
"type": "git",
|
|
8
45
|
"url": "git+https://github.com/joystyk-games/game-libraries.git",
|
|
9
46
|
"directory": "packages/daily"
|
|
10
47
|
},
|
|
11
|
-
"homepage": "https://github.com/joystyk-games/game-libraries",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"access": "public"
|
|
48
|
+
"homepage": "https://github.com/joystyk-games/game-libraries/tree/main/packages/daily#readme",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/joystyk-games/game-libraries/issues"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.core.json",
|
|
54
|
+
"build": "node ../../scripts/build-package.ts"
|
|
19
55
|
}
|
|
20
|
-
}
|
|
56
|
+
}
|