@khorsheed/dsh-game 0.0.0-stage → 0.1.1

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 (101) hide show
  1. package/CHANGELOG.md +25 -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 +30690 -0
  7. package/lib/index.js +4297 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +3876 -0
  10. package/lib/typert.remote-client.d.ts +104 -0
  11. package/lib/typert.remote-client.js +2138 -0
  12. package/lib/types/agent.d.ts +20 -0
  13. package/lib/types/agent.js +43 -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 +195 -0
  20. package/lib/types/client/GameTab.d.ts +14 -0
  21. package/lib/types/client/GameTab.js +261 -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 +163 -0
  26. package/lib/types/client/Preview3D.d.ts +52 -0
  27. package/lib/types/client/Preview3D.js +178 -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 +131 -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 +263 -0
  46. package/lib/types/client/locales.d.ts +379 -0
  47. package/lib/types/client/locales.js +733 -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 +110 -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/reveal.d.ts +43 -0
  59. package/lib/types/client/reveal.js +73 -0
  60. package/lib/types/client/style-samples.d.ts +5 -0
  61. package/lib/types/client/style-samples.js +196 -0
  62. package/lib/types/fsutil.d.ts +26 -0
  63. package/lib/types/fsutil.js +104 -0
  64. package/lib/types/hooks.d.ts +83 -0
  65. package/lib/types/hooks.js +108 -0
  66. package/lib/types/index.d.ts +30 -0
  67. package/lib/types/index.js +22 -0
  68. package/lib/types/kits/canvas2d.d.ts +6 -0
  69. package/lib/types/kits/canvas2d.js +60 -0
  70. package/lib/types/kits/ids.d.ts +11 -0
  71. package/lib/types/kits/ids.js +6 -0
  72. package/lib/types/kits/index.d.ts +26 -0
  73. package/lib/types/kits/index.js +28 -0
  74. package/lib/types/kits/three3d.d.ts +12 -0
  75. package/lib/types/kits/three3d.js +188 -0
  76. package/lib/types/kits/types.d.ts +52 -0
  77. package/lib/types/kits/types.js +6 -0
  78. package/lib/types/look3d.d.ts +20 -0
  79. package/lib/types/look3d.js +175 -0
  80. package/lib/types/panel-session.d.ts +21 -0
  81. package/lib/types/panel-session.js +16 -0
  82. package/lib/types/project.d.ts +138 -0
  83. package/lib/types/project.js +504 -0
  84. package/lib/types/remote.d.ts +184 -0
  85. package/lib/types/remote.js +263 -0
  86. package/lib/types/skills.d.ts +13 -0
  87. package/lib/types/skills.js +141 -0
  88. package/lib/types/store.d.ts +346 -0
  89. package/lib/types/store.js +1383 -0
  90. package/lib/types/tools.d.ts +12 -0
  91. package/lib/types/tools.js +309 -0
  92. package/lib/types/trash.d.ts +5 -0
  93. package/lib/types/trash.js +64 -0
  94. package/lib/types/types.d.ts +548 -0
  95. package/lib/types/types.js +85 -0
  96. package/lib/types/vfs.d.ts +94 -0
  97. package/lib/types/vfs.js +217 -0
  98. package/locale/en.json +6 -0
  99. package/locale/zh.json +6 -0
  100. package/package.json +132 -3
  101. package/runtime/dsh3d.js +1279 -0
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The game tab follows the agent: `game_publish` says where the user should
3
+ * look (the play area, an element's page, the element grid…), the page picks
4
+ * the request up on its poll, and this controller carries it out — unless the
5
+ * user is busy in the panel (a game running, a touch in the last 20 s), in
6
+ * which case it waits as a notice. The page itself never decides where to go.
7
+ *
8
+ * Each request is carried out once; one found on a reload is history. Every
9
+ * decision leaves a `[game:reveal]` console line.
10
+ * @module @khorsheed/dsh-game/client
11
+ */
12
+ import type { NavTarget } from './contract.ts';
13
+ import type { ShowRequest } from '../types.ts';
14
+ /** A request held back while the user was busy. */
15
+ export interface RevealNotice {
16
+ readonly text: string;
17
+ readonly target: NavTarget;
18
+ }
19
+ /** The page-wide controller. */
20
+ export declare class Reveal {
21
+ private readonly open;
22
+ private lastTouch;
23
+ private playing;
24
+ private mounted;
25
+ private noticeNow;
26
+ private readonly listeners;
27
+ /** The last request sequence carried out per project. */
28
+ private readonly done;
29
+ /** @param open - opens the game tab at a place (the plugin's openAt). */
30
+ constructor(open: (target: NavTarget) => void);
31
+ get notice(): RevealNotice | null;
32
+ dismiss(): void;
33
+ follow(): void;
34
+ /** The tab mounted; the result unmounts it. A mounted tab is navigated at any width. */
35
+ mount(): () => void;
36
+ touched(): void;
37
+ setPlaying(on: boolean): void;
38
+ subscribe(listener: () => void): () => void;
39
+ private changed;
40
+ /** The agent's request for a project, as the poll returned it. */
41
+ request(projectId: string, request: ShowRequest, now?: number): 'opened' | 'notice' | 'skipped' | null;
42
+ }
43
+ //# sourceMappingURL=reveal.d.ts.map
@@ -0,0 +1,73 @@
1
+ /** A touch this recent means the user is busy in the panel. */
2
+ const BUSY_MS = 20_000;
3
+ /** Below this window width a closed panel is not opened: it would cover the conversation. */
4
+ const WIDE_PX = 820;
5
+ /** A request older than this, seen first on this page, is history (a reload). */
6
+ const FRESH_MS = 90_000;
7
+ /** The page-wide controller. */
8
+ export class Reveal {
9
+ open;
10
+ lastTouch = 0;
11
+ playing = false;
12
+ mounted = 0;
13
+ noticeNow = null;
14
+ listeners = new Set();
15
+ /** The last request sequence carried out per project. */
16
+ done = new Map();
17
+ /** @param open - opens the game tab at a place (the plugin's openAt). */
18
+ constructor(open) {
19
+ this.open = open;
20
+ }
21
+ get notice() { return this.noticeNow; }
22
+ dismiss() { this.noticeNow = null; this.changed(); }
23
+ follow() {
24
+ const n = this.noticeNow;
25
+ this.dismiss();
26
+ if (n !== null)
27
+ this.open(n.target);
28
+ }
29
+ /** The tab mounted; the result unmounts it. A mounted tab is navigated at any width. */
30
+ mount() {
31
+ this.mounted++;
32
+ return () => { this.mounted--; };
33
+ }
34
+ touched() { this.lastTouch = Date.now(); }
35
+ setPlaying(on) { this.playing = on; }
36
+ subscribe(listener) {
37
+ this.listeners.add(listener);
38
+ return () => { this.listeners.delete(listener); };
39
+ }
40
+ changed() { for (const l of this.listeners)
41
+ l(); }
42
+ /** The agent's request for a project, as the poll returned it. */
43
+ request(projectId, request, now = Date.now()) {
44
+ const seen = this.done.get(projectId);
45
+ this.done.set(projectId, Math.max(request.seq, seen ?? 0));
46
+ if (seen !== undefined ? request.seq <= seen : now - Date.parse(request.at) > FRESH_MS)
47
+ return null;
48
+ const target = {
49
+ view: request.target.view,
50
+ ...(request.target.elementId === undefined ? {} : { elementId: request.target.elementId }),
51
+ ...(request.target.chapterId === undefined ? {} : { chapterId: request.target.chapterId }),
52
+ };
53
+ const busy = this.playing ? 'a game is running' : now - this.lastTouch < BUSY_MS ? 'the panel was just used' : null;
54
+ const narrow = this.mounted === 0 && (typeof window === 'undefined' || window.innerWidth < WIDE_PX);
55
+ const outcome = busy !== null ? 'notice' : narrow ? 'skipped' : 'opened';
56
+ try {
57
+ console.info(`[game:reveal] #${request.seq} ${request.note} → ${outcome}${busy === null ? '' : ` (${busy})`}${narrow && busy === null ? ' (closed panel, narrow window)' : ''}`, target);
58
+ }
59
+ catch {
60
+ // No console.
61
+ }
62
+ if (outcome === 'notice') {
63
+ this.noticeNow = { text: request.note, target };
64
+ this.changed();
65
+ }
66
+ if (outcome === 'opened') {
67
+ this.noticeNow = null;
68
+ this.open(target);
69
+ this.changed();
70
+ }
71
+ return outcome;
72
+ }
73
+ }
@@ -0,0 +1,5 @@
1
+ type G = CanvasRenderingContext2D;
2
+ /** Draw the sample scene for one style into a W×H canvas. */
3
+ export declare function drawStyleSample(g: G, styleId: string, w: number, h: number): void;
4
+ export {};
5
+ //# sourceMappingURL=style-samples.d.ts.map
@@ -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