@khorsheed/dsh-game 0.0.0-stage → 0.1.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.en.md +53 -0
  3. package/README.i18n.yaml +6 -0
  4. package/README.md +52 -2
  5. package/cordis.patch.yml +23 -0
  6. package/lib/client.js +30348 -0
  7. package/lib/index.js +4154 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +3769 -0
  10. package/lib/typert.remote-client.d.ts +101 -0
  11. package/lib/typert.remote-client.js +2082 -0
  12. package/lib/types/agent.d.ts +20 -0
  13. package/lib/types/agent.js +42 -0
  14. package/lib/types/checks.d.ts +44 -0
  15. package/lib/types/checks.js +267 -0
  16. package/lib/types/client/DesignView.d.ts +22 -0
  17. package/lib/types/client/DesignView.js +134 -0
  18. package/lib/types/client/ElementsView.d.ts +21 -0
  19. package/lib/types/client/ElementsView.js +187 -0
  20. package/lib/types/client/GameTab.d.ts +14 -0
  21. package/lib/types/client/GameTab.js +256 -0
  22. package/lib/types/client/PlayFrame.d.ts +54 -0
  23. package/lib/types/client/PlayFrame.js +129 -0
  24. package/lib/types/client/PlayView.d.ts +19 -0
  25. package/lib/types/client/PlayView.js +161 -0
  26. package/lib/types/client/Preview3D.d.ts +50 -0
  27. package/lib/types/client/Preview3D.js +176 -0
  28. package/lib/types/client/ProjectSwitcher.d.ts +20 -0
  29. package/lib/types/client/ProjectSwitcher.js +75 -0
  30. package/lib/types/client/Recipe3DEditor.d.ts +19 -0
  31. package/lib/types/client/Recipe3DEditor.js +44 -0
  32. package/lib/types/client/StyleGallery.d.ts +18 -0
  33. package/lib/types/client/StyleGallery.js +27 -0
  34. package/lib/types/client/TurnCard.d.ts +62 -0
  35. package/lib/types/client/TurnCard.js +100 -0
  36. package/lib/types/client/VersionsView.d.ts +17 -0
  37. package/lib/types/client/VersionsView.js +68 -0
  38. package/lib/types/client/chat-node.d.ts +42 -0
  39. package/lib/types/client/chat-node.js +77 -0
  40. package/lib/types/client/contract.d.ts +125 -0
  41. package/lib/types/client/contract.js +6 -0
  42. package/lib/types/client/definition.d.ts +22 -0
  43. package/lib/types/client/definition.js +28 -0
  44. package/lib/types/client/index.d.ts +21 -0
  45. package/lib/types/client/index.js +229 -0
  46. package/lib/types/client/locales.d.ts +376 -0
  47. package/lib/types/client/locales.js +727 -0
  48. package/lib/types/client/looks.d.ts +36 -0
  49. package/lib/types/client/looks.js +105 -0
  50. package/lib/types/client/markdown.d.ts +11 -0
  51. package/lib/types/client/markdown.js +52 -0
  52. package/lib/types/client/preset-visibility.d.ts +94 -0
  53. package/lib/types/client/preset-visibility.js +115 -0
  54. package/lib/types/client/preview-entry.d.ts +24 -0
  55. package/lib/types/client/preview-entry.js +107 -0
  56. package/lib/types/client/refs.d.ts +36 -0
  57. package/lib/types/client/refs.js +44 -0
  58. package/lib/types/client/style-samples.d.ts +5 -0
  59. package/lib/types/client/style-samples.js +196 -0
  60. package/lib/types/fsutil.d.ts +26 -0
  61. package/lib/types/fsutil.js +104 -0
  62. package/lib/types/hooks.d.ts +83 -0
  63. package/lib/types/hooks.js +108 -0
  64. package/lib/types/index.d.ts +30 -0
  65. package/lib/types/index.js +22 -0
  66. package/lib/types/kits/canvas2d.d.ts +6 -0
  67. package/lib/types/kits/canvas2d.js +60 -0
  68. package/lib/types/kits/ids.d.ts +11 -0
  69. package/lib/types/kits/ids.js +6 -0
  70. package/lib/types/kits/index.d.ts +26 -0
  71. package/lib/types/kits/index.js +28 -0
  72. package/lib/types/kits/three3d.d.ts +12 -0
  73. package/lib/types/kits/three3d.js +188 -0
  74. package/lib/types/kits/types.d.ts +52 -0
  75. package/lib/types/kits/types.js +6 -0
  76. package/lib/types/look3d.d.ts +20 -0
  77. package/lib/types/look3d.js +175 -0
  78. package/lib/types/panel-session.d.ts +21 -0
  79. package/lib/types/panel-session.js +16 -0
  80. package/lib/types/project.d.ts +132 -0
  81. package/lib/types/project.js +494 -0
  82. package/lib/types/remote.d.ts +182 -0
  83. package/lib/types/remote.js +256 -0
  84. package/lib/types/skills.d.ts +13 -0
  85. package/lib/types/skills.js +140 -0
  86. package/lib/types/store.d.ts +325 -0
  87. package/lib/types/store.js +1285 -0
  88. package/lib/types/tools.d.ts +12 -0
  89. package/lib/types/tools.js +301 -0
  90. package/lib/types/trash.d.ts +5 -0
  91. package/lib/types/trash.js +64 -0
  92. package/lib/types/types.d.ts +522 -0
  93. package/lib/types/types.js +85 -0
  94. package/lib/types/vfs.d.ts +92 -0
  95. package/lib/types/vfs.js +217 -0
  96. package/locale/en.json +6 -0
  97. package/locale/zh.json +6 -0
  98. package/package.json +132 -3
  99. package/runtime/dsh3d.js +1279 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * The art-style gallery's samples: one small scene (a keeper, a lamp post, a
3
+ * blob creature, the ground) drawn in each built-in style from that style's
4
+ * own palette, so the user compares looks on the same subject. Pure canvas
5
+ * drawing; colors are the style's content, not theme tokens.
6
+ */
7
+ import { ART_STYLES } from "../types.js";
8
+ /** Deterministic jitter for the hand-drawn style. */
9
+ function jitter(seed) {
10
+ const x = Math.sin(seed * 12.9898) * 43758.5453;
11
+ return (x - Math.floor(x)) - 0.5;
12
+ }
13
+ /** A pixel sprite from rows, drawn with `cell`-px blocks. */
14
+ function sprite(g, rows, colors, x, y, cell) {
15
+ rows.forEach((row, ry) => [...row].forEach((ch, rx) => {
16
+ const c = colors[ch];
17
+ if (c === undefined)
18
+ return;
19
+ g.fillStyle = c;
20
+ g.fillRect(x + rx * cell, y + ry * cell, cell, cell);
21
+ }));
22
+ }
23
+ const KEEPER = ['..hh..', '.hhhh.', '..ff..', '.cccc.', 'cccccc', '.cccc.', '.c..c.', '.b..b.'];
24
+ const BLOB = ['..eeee..', '.eeeeee.', 'eeyeeyee', 'eeeeeeee', 'eeemmeee', '.eeeeee.', 'e.e..e.e'];
25
+ /** Draw the sample scene for one style into a W×H canvas. */
26
+ export function drawStyleSample(g, styleId, w, h) {
27
+ const p = (ART_STYLES.find(s => s.id === styleId) ?? ART_STYLES[0]).palette;
28
+ const ground = h * 0.74;
29
+ g.save();
30
+ g.clearRect(0, 0, w, h);
31
+ switch (styleId) {
32
+ case 'pixel': {
33
+ g.fillStyle = p[15];
34
+ g.fillRect(0, 0, w, h);
35
+ for (let i = 0; i < 14; i++) {
36
+ g.fillStyle = p[13];
37
+ g.fillRect((i * 37) % w, (i * 23) % (ground * 0.6), 2, 2);
38
+ }
39
+ g.fillStyle = p[7];
40
+ g.fillRect(0, ground, w, h - ground);
41
+ for (let x = 0; x < w; x += 8) {
42
+ g.fillStyle = p[6];
43
+ g.fillRect(x, ground, 4, 4);
44
+ }
45
+ g.fillStyle = p[14];
46
+ g.fillRect(w * 0.72, ground - 52, 6, 52);
47
+ g.fillStyle = p[4];
48
+ g.fillRect(w * 0.72 - 4, ground - 60, 14, 10);
49
+ sprite(g, KEEPER, { h: p[2], f: p[4], c: p[9], b: p[0] }, w * 0.22, ground - 32, 4);
50
+ sprite(g, BLOB, { e: p[13], y: p[4], m: p[0] }, w * 0.46, ground - 28, 4);
51
+ break;
52
+ }
53
+ case 'flat': {
54
+ g.fillStyle = p[6];
55
+ g.fillRect(0, 0, w, h);
56
+ g.fillStyle = p[3];
57
+ g.beginPath();
58
+ g.arc(w * 0.82, h * 0.22, 14, 0, Math.PI * 2);
59
+ g.fill();
60
+ g.fillStyle = p[1];
61
+ g.beginPath();
62
+ g.roundRect(0, ground, w, h - ground, 0);
63
+ g.fill();
64
+ g.fillStyle = p[0];
65
+ g.beginPath();
66
+ g.roundRect(w * 0.7, ground - 54, 8, 54, 4);
67
+ g.fill();
68
+ g.fillStyle = p[3];
69
+ g.beginPath();
70
+ g.arc(w * 0.7 + 4, ground - 58, 9, 0, Math.PI * 2);
71
+ g.fill();
72
+ g.fillStyle = p[5];
73
+ g.beginPath();
74
+ g.roundRect(w * 0.2, ground - 30, 22, 30, 8);
75
+ g.fill();
76
+ g.fillStyle = p[4];
77
+ g.beginPath();
78
+ g.arc(w * 0.2 + 11, ground - 38, 9, 0, Math.PI * 2);
79
+ g.fill();
80
+ g.fillStyle = p[7];
81
+ g.beginPath();
82
+ g.ellipse(w * 0.5, ground - 14, 18, 14, 0, 0, Math.PI * 2);
83
+ g.fill();
84
+ g.fillStyle = p[6];
85
+ g.beginPath();
86
+ g.arc(w * 0.5 - 6, ground - 17, 3, 0, Math.PI * 2);
87
+ g.arc(w * 0.5 + 6, ground - 17, 3, 0, Math.PI * 2);
88
+ g.fill();
89
+ break;
90
+ }
91
+ case 'line': {
92
+ g.fillStyle = p[1];
93
+ g.fillRect(0, 0, w, h);
94
+ g.strokeStyle = p[0];
95
+ g.lineWidth = 1.6;
96
+ g.lineCap = 'round';
97
+ const wobble = (pts, fill) => {
98
+ g.beginPath();
99
+ pts.forEach(([x, y], i) => { const jx = x + jitter(i + x) * 1.6, jy = y + jitter(i + y) * 1.6; if (i === 0)
100
+ g.moveTo(jx, jy);
101
+ else
102
+ g.lineTo(jx, jy); });
103
+ if (fill) {
104
+ g.fillStyle = fill;
105
+ g.fill();
106
+ }
107
+ g.stroke();
108
+ };
109
+ wobble([[0, ground], [w, ground + 2]]);
110
+ wobble([[w * 0.7, ground], [w * 0.7, ground - 54], [w * 0.7 + 8, ground - 54], [w * 0.7 + 8, ground]], p[5]);
111
+ wobble([[w * 0.2, ground], [w * 0.2, ground - 28], [w * 0.2 + 20, ground - 28], [w * 0.2 + 20, ground]], p[4]);
112
+ g.beginPath();
113
+ g.arc(w * 0.2 + 10, ground - 37, 9, 0, Math.PI * 2);
114
+ g.fillStyle = p[2];
115
+ g.fill();
116
+ g.stroke();
117
+ g.beginPath();
118
+ g.ellipse(w * 0.5, ground - 13, 17, 13, 0, 0, Math.PI * 2);
119
+ g.fillStyle = p[6];
120
+ g.fill();
121
+ g.stroke();
122
+ g.fillStyle = p[0];
123
+ g.fillRect(w * 0.5 - 7, ground - 16, 3, 3);
124
+ g.fillRect(w * 0.5 + 4, ground - 16, 3, 3);
125
+ break;
126
+ }
127
+ case 'neon': {
128
+ g.fillStyle = p[0];
129
+ g.fillRect(0, 0, w, h);
130
+ g.strokeStyle = p[1];
131
+ g.lineWidth = 1;
132
+ for (let x = 0; x < w; x += 16) {
133
+ g.beginPath();
134
+ g.moveTo(x, ground);
135
+ g.lineTo(w / 2 + (x - w / 2) * 2, h);
136
+ g.stroke();
137
+ }
138
+ const glow = (color, draw) => { g.shadowColor = color; g.shadowBlur = 10; g.strokeStyle = color; g.lineWidth = 2; draw(); g.shadowBlur = 0; };
139
+ glow(p[3], () => { g.beginPath(); g.moveTo(0, ground); g.lineTo(w, ground); g.stroke(); });
140
+ glow(p[5], () => { g.strokeRect(w * 0.7, ground - 54, 8, 54); g.beginPath(); g.arc(w * 0.7 + 4, ground - 60, 6, 0, Math.PI * 2); g.stroke(); });
141
+ glow(p[2], () => { g.strokeRect(w * 0.2, ground - 28, 20, 28); g.beginPath(); g.arc(w * 0.2 + 10, ground - 36, 7, 0, Math.PI * 2); g.stroke(); });
142
+ glow(p[4], () => { g.beginPath(); g.ellipse(w * 0.5, ground - 13, 16, 12, 0, 0, Math.PI * 2); g.stroke(); });
143
+ break;
144
+ }
145
+ case 'paper': {
146
+ g.fillStyle = p[0];
147
+ g.fillRect(0, 0, w, h);
148
+ for (let i = 0; i < 60; i++) {
149
+ g.fillStyle = p[1];
150
+ g.globalAlpha = 0.25;
151
+ g.fillRect((i * 53) % w, (i * 29) % h, 3, 1);
152
+ }
153
+ g.globalAlpha = 1;
154
+ const cut = (color, draw, angle = 0) => {
155
+ g.save();
156
+ g.rotate(angle);
157
+ g.shadowColor = 'rgba(60,40,20,0.35)' /* the paper style's own cast shadow */;
158
+ g.shadowBlur = 4;
159
+ g.shadowOffsetY = 2;
160
+ g.fillStyle = color;
161
+ draw();
162
+ g.restore();
163
+ };
164
+ cut(p[4], () => { g.fillRect(0, ground, w, h - ground); });
165
+ cut(p[5], () => { g.beginPath(); g.ellipse(w * 0.3, ground + 4, 60, 14, 0, 0, Math.PI * 2); g.fill(); });
166
+ cut(p[7], () => { g.fillRect(w * 0.7, ground - 54, 8, 54); }, 0.01);
167
+ cut(p[2], () => { g.beginPath(); g.arc(w * 0.7 + 4, ground - 58, 9, 0, Math.PI * 2); g.fill(); });
168
+ cut(p[6], () => { g.fillRect(w * 0.2, ground - 28, 20, 28); }, -0.02);
169
+ cut(p[2], () => { g.beginPath(); g.arc(w * 0.2 + 10, ground - 36, 8, 0, Math.PI * 2); g.fill(); });
170
+ cut(p[3], () => { g.beginPath(); g.ellipse(w * 0.5, ground - 12, 16, 12, 0, 0, Math.PI * 2); g.fill(); });
171
+ break;
172
+ }
173
+ default: {
174
+ g.fillStyle = p[1];
175
+ g.fillRect(0, 0, w, h);
176
+ g.fillStyle = p[0];
177
+ g.fillRect(0, ground, w, 3);
178
+ g.fillRect(w * 0.7, ground - 54, 6, 54);
179
+ g.fillStyle = p[2];
180
+ g.beginPath();
181
+ g.arc(w * 0.7 + 3, ground - 58, 7, 0, Math.PI * 2);
182
+ g.fill();
183
+ g.fillStyle = p[0];
184
+ g.fillRect(w * 0.2, ground - 28, 16, 28);
185
+ g.beginPath();
186
+ g.arc(w * 0.2 + 8, ground - 35, 7, 0, Math.PI * 2);
187
+ g.fill();
188
+ g.strokeStyle = p[0];
189
+ g.lineWidth = 2;
190
+ g.beginPath();
191
+ g.ellipse(w * 0.5, ground - 12, 15, 11, 0, 0, Math.PI * 2);
192
+ g.stroke();
193
+ }
194
+ }
195
+ g.restore();
196
+ }
@@ -0,0 +1,26 @@
1
+ import type { BundleFile } from './types.ts';
2
+ /** Whether `child` is `parent` or inside it. */
3
+ export declare function inside(parent: string, child: string): boolean;
4
+ /** Write a file atomically (tmp + rename), creating its directory. */
5
+ export declare function writeAtomic(path: string, data: string | Uint8Array): Promise<void>;
6
+ /** Read a text file, undefined when absent or unreadable. */
7
+ export declare function readText(path: string): Promise<string | undefined>;
8
+ /** Read JSON, undefined when absent or broken. */
9
+ export declare function readJson<T>(path: string): Promise<T | undefined>;
10
+ /** One walked file: project-relative POSIX path plus its bytes. */
11
+ export interface WalkedFile {
12
+ readonly path: string;
13
+ readonly data: Buffer;
14
+ }
15
+ /**
16
+ * Walk a directory (dot-entries skipped), bounded by the walk limits.
17
+ * @param root - directory to walk.
18
+ * @param keep - project-relative path filter.
19
+ * @returns the files, sorted by path.
20
+ */
21
+ export declare function walk(root: string, keep: (path: string) => boolean): Promise<WalkedFile[]>;
22
+ /** The sandbox bundle form of walked files. */
23
+ export declare function bundleOf(files: readonly WalkedFile[]): Record<string, BundleFile>;
24
+ /** A content fingerprint over (path, bytes) pairs. */
25
+ export declare function fingerprintOf(files: readonly WalkedFile[]): string;
26
+ //# sourceMappingURL=fsutil.d.ts.map
@@ -0,0 +1,104 @@
1
+ /**
2
+ * File helpers the game service builds on: atomic writes, tolerant reads, the
3
+ * bounded project walk, and the sandbox bundle form of walked files.
4
+ * @module @khorsheed/dsh-game
5
+ */
6
+ import { createHash, randomBytes } from 'node:crypto';
7
+ import { mkdir, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
8
+ import { dirname, isAbsolute, join, relative, sep } from 'node:path';
9
+ import { isTextFile, mimeOf } from "./project.js";
10
+ /** Walk limits for one project read: enough for a small game, bounded for a stray huge folder. */
11
+ const MAX_FILES = 600;
12
+ const MAX_FILE_BYTES = 8 * 1024 * 1024;
13
+ const MAX_TOTAL_BYTES = 48 * 1024 * 1024;
14
+ /** Whether `child` is `parent` or inside it. */
15
+ export function inside(parent, child) {
16
+ const rel = relative(parent, child);
17
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
18
+ }
19
+ /** Write a file atomically (tmp + rename), creating its directory. */
20
+ export async function writeAtomic(path, data) {
21
+ await mkdir(dirname(path), { recursive: true });
22
+ const tmp = `${path}.${randomBytes(4).toString('hex')}.tmp`;
23
+ await writeFile(tmp, data);
24
+ await rename(tmp, path);
25
+ }
26
+ /** Read a text file, undefined when absent or unreadable. */
27
+ export async function readText(path) {
28
+ try {
29
+ return await readFile(path, 'utf8');
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ /** Read JSON, undefined when absent or broken. */
36
+ export async function readJson(path) {
37
+ const text = await readText(path);
38
+ if (text === undefined)
39
+ return undefined;
40
+ try {
41
+ return JSON.parse(text);
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ /**
48
+ * Walk a directory (dot-entries skipped), bounded by the walk limits.
49
+ * @param root - directory to walk.
50
+ * @param keep - project-relative path filter.
51
+ * @returns the files, sorted by path.
52
+ */
53
+ export async function walk(root, keep) {
54
+ const out = [];
55
+ let total = 0;
56
+ const visit = async (dir) => {
57
+ let entries;
58
+ try {
59
+ entries = await readdir(dir, { withFileTypes: true });
60
+ }
61
+ catch {
62
+ return;
63
+ }
64
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
65
+ if (out.length >= MAX_FILES || entry.name.startsWith('.') || entry.name === 'node_modules')
66
+ continue;
67
+ const full = join(dir, entry.name);
68
+ const rel = relative(root, full).split(sep).join('/');
69
+ if (entry.isDirectory()) {
70
+ await visit(full);
71
+ continue;
72
+ }
73
+ if (!entry.isFile() || !keep(rel))
74
+ continue;
75
+ const info = await stat(full).catch(() => undefined);
76
+ if (info === undefined || info.size > MAX_FILE_BYTES || total + info.size > MAX_TOTAL_BYTES)
77
+ continue;
78
+ total += info.size;
79
+ out.push({ path: rel, data: await readFile(full) });
80
+ }
81
+ };
82
+ await visit(root);
83
+ return out;
84
+ }
85
+ /** The sandbox bundle form of walked files. */
86
+ export function bundleOf(files) {
87
+ const out = {};
88
+ for (const f of files) {
89
+ out[f.path] = isTextFile(f.path)
90
+ ? { type: mimeOf(f.path), text: f.data.toString('utf8') }
91
+ : { type: mimeOf(f.path), b64: f.data.toString('base64') };
92
+ }
93
+ return out;
94
+ }
95
+ /** A content fingerprint over (path, bytes) pairs. */
96
+ export function fingerprintOf(files) {
97
+ const hash = createHash('sha1');
98
+ for (const f of files) {
99
+ hash.update(f.path);
100
+ hash.update('\0');
101
+ hash.update(createHash('sha1').update(f.data).digest());
102
+ }
103
+ return hash.digest('hex').slice(0, 16);
104
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The end-of-turn loop (proposal §6, probes P0-4/P0-5): when a turn that is
3
+ * bound to a game project is about to close, run the checks on what it
4
+ * changed. A failure is steered back into the SAME turn with the findings —
5
+ * at most {@link FIX_CAP} times — and a pass deploys into the play area (or
6
+ * leaves a candidate when auto-confirm is off).
7
+ *
8
+ * `agent/turn-stopping` fires only for a turn completing normally (never on
9
+ * abort or error), so a user's stop is never overridden. Sessions with no
10
+ * bound project are untouched, which is what keeps the hook inert outside the
11
+ * game preset: only `game_open` binds.
12
+ * @module @khorsheed/dsh-game
13
+ */
14
+ import type { Context } from '@deepseek-ai/cordis';
15
+ import { type ContextFormed } from '@deepseek-ai/dsh-llm';
16
+ import type { GameService } from './store.ts';
17
+ import { type CheckRecord } from './types.ts';
18
+ /** What a dsh-game message is, for the chat row (the text is for the model). */
19
+ export type GameMessageOp = {
20
+ readonly op: 'check-failed';
21
+ readonly attempt: number;
22
+ readonly cap: number;
23
+ readonly failures: readonly {
24
+ where: string;
25
+ message: string;
26
+ }[];
27
+ } | {
28
+ readonly op: 'attached';
29
+ readonly items: readonly string[];
30
+ };
31
+ declare module '@deepseek-ai/dsh-llm' {
32
+ interface MessageSourceMap {
33
+ /** A message the game plugin put into the conversation (check findings, attached changes). */
34
+ 'dsh-game': {
35
+ kind: 'dsh-game';
36
+ plugin: '@khorsheed/dsh-game';
37
+ } & ContextFormed & Partial<GameMessageOp>;
38
+ }
39
+ }
40
+ /** Build a dsh-game user message. */
41
+ export declare function gameMessage(text: string, section: string, op: GameMessageOp): {
42
+ content: {
43
+ type: "text";
44
+ text: string;
45
+ }[];
46
+ source: {
47
+ op: "check-failed";
48
+ attempt: number;
49
+ cap: number;
50
+ failures: readonly {
51
+ where: string;
52
+ message: string;
53
+ }[];
54
+ kind: "dsh-game";
55
+ plugin: "@khorsheed/dsh-game";
56
+ form: "snapshot";
57
+ sections: {
58
+ name: string;
59
+ text: string;
60
+ }[];
61
+ } | {
62
+ op: "attached";
63
+ items: readonly string[];
64
+ kind: "dsh-game";
65
+ plugin: "@khorsheed/dsh-game";
66
+ form: "snapshot";
67
+ sections: {
68
+ name: string;
69
+ text: string;
70
+ }[];
71
+ };
72
+ } & Pick<import("@deepseek-ai/dsh-llm").UserMessage, "id" | "role">;
73
+ /** Automatic fix rounds per turn (decision: at most 2). */
74
+ export declare const FIX_CAP = 2;
75
+ /** The steer text for one failed check (model-facing). */
76
+ export declare function failureText(record: CheckRecord, attempt: number): string;
77
+ /**
78
+ * Register the hooks on the plugin's scope.
79
+ * @param ctx - the core plugin's context.
80
+ * @param store - the service core.
81
+ */
82
+ export declare function registerTurnHooks(ctx: Context, store: GameService): void;
83
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1,108 @@
1
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
2
+ import { PANEL_CHANGES_MARK } from "./types.js";
3
+ /** Build a dsh-game user message. */
4
+ export function gameMessage(text, section, op) {
5
+ return createUserMessage({
6
+ content: [{ type: 'text', text }],
7
+ source: { kind: 'dsh-game', plugin: '@khorsheed/dsh-game', form: 'snapshot', sections: [{ name: section, text }], ...op },
8
+ });
9
+ }
10
+ /** Automatic fix rounds per turn (decision: at most 2). */
11
+ export const FIX_CAP = 2;
12
+ /** The steer text for one failed check (model-facing). */
13
+ export function failureText(record, attempt) {
14
+ const lines = [
15
+ `[游戏检查未通过 ${attempt}/${FIX_CAP}] 这一轮改完的版本没过检查,试玩区还停在上一个版本。`,
16
+ ...record.failures.slice(0, 8).map(f => `- ${f.where}:${f.message}`),
17
+ ];
18
+ if (record.failures.length > 8)
19
+ lines.push(`- ……另有 ${record.failures.length - 8} 项`);
20
+ if (record.skipped !== undefined)
21
+ lines.push(`(${record.skipped})`);
22
+ lines.push(attempt < FIX_CAP
23
+ ? '请修好再结束这一轮:先读报错对应的代码,改完不用告诉我,结束时会自动重新检查。若是测试本身写错了,改测试并说明理由。'
24
+ : '这是最后一次自动修复机会;修不好就如实告诉用户卡在哪里。');
25
+ return lines.join('\n');
26
+ }
27
+ /**
28
+ * Register the hooks on the plugin's scope.
29
+ * @param ctx - the core plugin's context.
30
+ * @param store - the service core.
31
+ */
32
+ export function registerTurnHooks(ctx, store) {
33
+ /** Steers already sent, per `<session>:<turn>`. */
34
+ const steered = new Map();
35
+ ctx.on('agent/turn-stopping', async ({ agent, turn, signal }) => {
36
+ if (signal.aborted)
37
+ return;
38
+ const key = `${agent.session.id}:${turn}`;
39
+ const attempt = steered.get(key) ?? 0;
40
+ let outcome;
41
+ try {
42
+ outcome = await store.checkTurn(agent.session, attempt, FIX_CAP);
43
+ }
44
+ catch (error) {
45
+ ctx.logger.warn('game: end-of-turn check failed to run', error);
46
+ return;
47
+ }
48
+ if (outcome.kind !== 'fail' || signal.aborted)
49
+ return;
50
+ if (attempt >= FIX_CAP)
51
+ return;
52
+ steered.set(key, attempt + 1);
53
+ const text = failureText(outcome.record, attempt + 1);
54
+ agent.steer(gameMessage(text, 'checks', {
55
+ op: 'check-failed', attempt: attempt + 1, cap: FIX_CAP,
56
+ failures: outcome.record.failures.slice(0, 8).map(f => ({ where: f.where, message: f.message.slice(0, 300) })),
57
+ }));
58
+ });
59
+ // Panel changes are sent explicitly (2026-10-10, replacing the automatic
60
+ // attach): the dock writes them into the composer and the user sends. When
61
+ // such a message enters a step, the changes it lists stop being pending.
62
+ // The batch itself is never touched.
63
+ ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
64
+ const decision = await next();
65
+ if (decision.kind === 'reject' || signal.aborted || decision.messages.length === 0)
66
+ return decision;
67
+ const text = decision.messages
68
+ .map(m => m.content.map(block => (block.type === 'text' ? block.text : '')).join('\n'))
69
+ .join('\n');
70
+ if (text.includes(PANEL_CHANGES_MARK)) {
71
+ await store.clearSent(agent.session.id, text).catch((error) => {
72
+ ctx.logger.warn('game: could not clear sent panel changes', error);
73
+ });
74
+ }
75
+ return decision;
76
+ }, { prepend: true });
77
+ // Turn reports: snapshot the bound project when a turn starts, record what
78
+ // it changed when the turn ends (whatever its reason), for the turn-tail card.
79
+ const baselines = new Map();
80
+ ctx.on('session/event', (session, raw) => {
81
+ const event = raw;
82
+ const turn = event?.data?.turn ?? event?.turn;
83
+ if (event?.type === 'turn/start')
84
+ store.setTurnActive(session.id, true);
85
+ if (event?.type === 'turn/end')
86
+ store.setTurnActive(session.id, false);
87
+ if (event?.type === 'turn/start' && turn !== undefined) {
88
+ baselines.set(`${session.id}:${turn}`, store.turnBaseline(session.id).catch(() => undefined));
89
+ return;
90
+ }
91
+ if (event?.type !== 'turn/end')
92
+ return;
93
+ if (turn !== undefined) {
94
+ steered.delete(`${session.id}:${turn}`);
95
+ const key = `${session.id}:${turn}`;
96
+ const base = baselines.get(key);
97
+ baselines.delete(key);
98
+ if (base !== undefined) {
99
+ void base.then(b => (b === undefined ? undefined : store.recordTurn(session.id, turn, b)))
100
+ .catch((error) => { ctx.logger.warn('game: could not record the turn report', error); });
101
+ }
102
+ }
103
+ else
104
+ for (const key of steered.keys())
105
+ if (key.startsWith(`${session.id}:`))
106
+ steered.delete(key);
107
+ });
108
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Game mode, host half: provides the service core (`ctx.gameStore`), mounts
3
+ * its Remote face, and runs the end-of-turn check loop for sessions bound to
4
+ * a project. The model tools, their guidance and the skills live in the
5
+ * `./agent` composition entry (preset-scoped).
6
+ * @module @khorsheed/dsh-game
7
+ */
8
+ import type { Context } from '@deepseek-ai/cordis';
9
+ import { GameService, type GameConfig } from './store.ts';
10
+ export { GameService, resolveGameStateRoot } from './store.ts';
11
+ export type { GameConfig } from './store.ts';
12
+ export { GameRemoteService } from './remote.ts';
13
+ export * from './types.ts';
14
+ declare module '@deepseek-ai/cordis' {
15
+ interface Context {
16
+ /** The game mode's service core. */
17
+ gameStore: GameService;
18
+ }
19
+ }
20
+ /** Cordis plugin name used by loader diagnostics. */
21
+ export declare const name = "game";
22
+ /** The service core writes through the mounted filesystem. */
23
+ export declare const inject: string[];
24
+ /**
25
+ * Plugin body.
26
+ * @param ctx - owning Cordis Context.
27
+ * @param config - plugin config.
28
+ */
29
+ export declare function apply(ctx: Context, config?: GameConfig): void;
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { registerTurnHooks } from "./hooks.js";
2
+ import { GameRemoteService } from "./remote.js";
3
+ import { GameService } from "./store.js";
4
+ export { GameService, resolveGameStateRoot } from "./store.js";
5
+ export { GameRemoteService } from "./remote.js";
6
+ export * from "./types.js";
7
+ /** Cordis plugin name used by loader diagnostics. */
8
+ export const name = 'game';
9
+ /** The service core writes through the mounted filesystem. */
10
+ export const inject = ['fs'];
11
+ /**
12
+ * Plugin body.
13
+ * @param ctx - owning Cordis Context.
14
+ * @param config - plugin config.
15
+ */
16
+ export function apply(ctx, config = {}) {
17
+ const store = new GameService(ctx, config);
18
+ ctx.provide('gameStore', store);
19
+ ctx.effect(() => () => { void store.dispose(); }, 'game: checker browser');
20
+ ctx.plugin(GameRemoteService, {});
21
+ registerTurnHooks(ctx, store);
22
+ }
@@ -0,0 +1,6 @@
1
+ import { type RuntimeKit } from './types.ts';
2
+ /** Blank when the first canvas, scaled to 32×32 through a 2D context, holds one color. */
3
+ export declare const BLANK_PROBE = "(() => {\n const c = document.querySelector('canvas');\n if (!c || c.width <= 0 || c.height <= 0) return null;\n const probe = document.createElement('canvas'); probe.width = 32; probe.height = 32;\n const p = probe.getContext('2d'); p.drawImage(c, 0, 0, 32, 32);\n const d = p.getImageData(0, 0, 32, 32).data; const distinct = new Set();\n for (let i = 0; i < d.length; i += 4) { distinct.add(d[i] + ',' + d[i + 1] + ',' + d[i + 2]); if (distinct.size > 1) break; }\n return distinct.size <= 1;\n})()";
4
+ /** The 2D canvas kit. */
5
+ export declare const CANVAS_2D: RuntimeKit;
6
+ //# sourceMappingURL=canvas2d.d.ts.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The `2d-canvas` kit: native canvas plus an HTML/CSS layer, pixel-row looks
3
+ * drawn with `dshDraw`, the eight built-in 2D art styles. This is the
4
+ * workbench's original runtime, moved behind the kit seam unchanged.
5
+ * @module @khorsheed/dsh-game
6
+ */
7
+ import { normalizeLook, PIXEL_LOOK_HINT } from "../project.js";
8
+ import { ART_STYLES } from "../types.js";
9
+ import { KIT_RUNTIME_FILE } from "./types.js";
10
+ /** The frame runtime: pixel-look drawing helpers over `window.dshElements`. Kept as a string (never `fn.toString()`). */
11
+ const RUNTIME = `(function(){
12
+ var CH = '0123456789abcdefghijklmnopqrstuvwxyz';
13
+ window.dshDraw = function(g, look, state, x, y, scale){
14
+ if (typeof look === 'string') { var el = window.dshElements[look]; look = el && el.look }
15
+ if (!look || !look.states || !look.palette) return false;
16
+ var names = Object.keys(look.states), rows = look.states[state] || look.states[names[0]];
17
+ if (!rows) return false;
18
+ scale = scale || 1;
19
+ for (var r = 0; r < rows.length; r++) for (var c = 0; c < rows[r].length; c++) {
20
+ var col = look.palette[CH.indexOf(rows[r].charAt(c))];
21
+ if (!col) continue;
22
+ g.fillStyle = col; g.fillRect(Math.round(x + c * scale), Math.round(y + r * scale), Math.ceil(scale), Math.ceil(scale));
23
+ }
24
+ return true;
25
+ };
26
+ window.dshLookSize = function(look){
27
+ if (typeof look === 'string') { var el = window.dshElements[look]; look = el && el.look }
28
+ var rows = look && look.states && look.states[Object.keys(look.states)[0]];
29
+ return rows ? { w: rows[0].length, h: rows.length } : { w: 0, h: 0 };
30
+ };
31
+ })();`;
32
+ /** Blank when the first canvas, scaled to 32×32 through a 2D context, holds one color. */
33
+ export const BLANK_PROBE = `(() => {
34
+ const c = document.querySelector('canvas');
35
+ if (!c || c.width <= 0 || c.height <= 0) return null;
36
+ const probe = document.createElement('canvas'); probe.width = 32; probe.height = 32;
37
+ const p = probe.getContext('2d'); p.drawImage(c, 0, 0, 32, 32);
38
+ const d = p.getImageData(0, 0, 32, 32).data; const distinct = new Set();
39
+ for (let i = 0; i < d.length; i += 4) { distinct.add(d[i] + ',' + d[i + 1] + ',' + d[i + 2]); if (distinct.size > 1) break; }
40
+ return distinct.size <= 1;
41
+ })()`;
42
+ const STYLE_LINES = ART_STYLES.map(s => `- \`${s.id}\` ${s.label}:${s.look}`).join('\n');
43
+ /** The 2D canvas kit. */
44
+ export const CANVAS_2D = {
45
+ id: '2d-canvas',
46
+ version: 1,
47
+ label: '2D 画布',
48
+ runtimeFiles: () => ({ [KIT_RUNTIME_FILE]: { type: 'text/javascript', text: RUNTIME } }),
49
+ normalizeLook,
50
+ lookHint: PIXEL_LOOK_HINT,
51
+ blankProbe: BLANK_PROBE,
52
+ styles: ART_STYLES,
53
+ skillSection: `## 运行环境
54
+ - 只有 2D:原生 canvas 加 HTML/CSS 做界面。不能联网、不能引外部 CDN(沙箱 CSP 全拦),所有代码和资源都放项目里。
55
+ - 存档用 \`localStorage\`(面板替你保存在用户浏览器里)。
56
+ - 没有文生图:画面全部用代码画(canvas 绘制、像素数组、SVG)。严格按 game.json 的 artStyle 画:
57
+ ${STYLE_LINES}
58
+ - 外观 look 的格式:\`{ "palette": ["#1a1c2c", "#ffcd75", …], "states": { "idle": ["..00..", ".0110.", …], "move": [...] } }\`。每行一个字符串,\`.\` 透明,\`0-9a-z\` 是 palette 下标;同一状态各行等宽,常用 16×16。颜色优先取 game.json 的 \`palette\`(来自美术风格)。画的时候用 \`dshDraw(ctx2d, 元素id或look, 状态名, x, y, 放大倍数)\`,尺寸用 \`dshLookSize(元素id)\`。
59
+ `,
60
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The shipped kit ids, apart from the kits themselves so the manifest parser
3
+ * (shared with the browser half) can check one without pulling a kit in.
4
+ * @module @khorsheed/dsh-game
5
+ */
6
+ import type { KitId } from './types.ts';
7
+ /** Every kit id this build ships. */
8
+ export declare const KIT_IDS: readonly KitId[];
9
+ /** Whether a value names a shipped kit. */
10
+ export declare function isKitId(value: unknown): value is KitId;
11
+ //# sourceMappingURL=ids.d.ts.map
@@ -0,0 +1,6 @@
1
+ /** Every kit id this build ships. */
2
+ export const KIT_IDS = ['2d-canvas', '3d-three'];
3
+ /** Whether a value names a shipped kit. */
4
+ export function isKitId(value) {
5
+ return typeof value === 'string' && KIT_IDS.includes(value);
6
+ }