dskcode 0.1.20 → 0.1.21
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/dist/index.js +507 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1182,38 +1182,58 @@ function estimateMessageTokens(msg) {
|
|
|
1182
1182
|
}
|
|
1183
1183
|
return estimateTokens(text) + 10;
|
|
1184
1184
|
}
|
|
1185
|
+
function groupIntoTurns(messages) {
|
|
1186
|
+
const turns = [];
|
|
1187
|
+
let current = null;
|
|
1188
|
+
for (const msg of messages) {
|
|
1189
|
+
if (msg.role === "user") {
|
|
1190
|
+
if (current) turns.push(current);
|
|
1191
|
+
current = [msg];
|
|
1192
|
+
} else {
|
|
1193
|
+
if (!current) current = [];
|
|
1194
|
+
current.push(msg);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
if (current && current.length > 0) turns.push(current);
|
|
1198
|
+
return turns;
|
|
1199
|
+
}
|
|
1200
|
+
function estimateTurnTokens(turn) {
|
|
1201
|
+
let sum = 0;
|
|
1202
|
+
for (const msg of turn) sum += estimateMessageTokens(msg);
|
|
1203
|
+
return sum;
|
|
1204
|
+
}
|
|
1185
1205
|
function trimMessages(messages, opts) {
|
|
1186
1206
|
const meta = getModelMeta(opts.model);
|
|
1187
1207
|
const maxInputTokens = meta.contextWindow - opts.reservedForOutput;
|
|
1188
1208
|
const systemTokens = estimateTokens(opts.systemPrompt);
|
|
1189
1209
|
let remaining = maxInputTokens - systemTokens;
|
|
1190
|
-
const
|
|
1210
|
+
const turns = groupIntoTurns(messages);
|
|
1211
|
+
const preservedTurns = [];
|
|
1191
1212
|
let roundsPreserved = 0;
|
|
1192
|
-
for (let i =
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
roundsPreserved++;
|
|
1196
|
-
}
|
|
1213
|
+
for (let i = turns.length - 1; i >= 0 && roundsPreserved < opts.preserveRecentRounds; i--) {
|
|
1214
|
+
preservedTurns.unshift(turns[i]);
|
|
1215
|
+
roundsPreserved++;
|
|
1197
1216
|
}
|
|
1217
|
+
const preserved = preservedTurns.flat();
|
|
1198
1218
|
for (const msg of preserved) {
|
|
1199
1219
|
remaining -= estimateMessageTokens(msg);
|
|
1200
1220
|
}
|
|
1201
1221
|
if (remaining < 0) {
|
|
1202
|
-
while (
|
|
1203
|
-
const
|
|
1204
|
-
remaining +=
|
|
1222
|
+
while (preservedTurns.length > 1 && remaining < 0) {
|
|
1223
|
+
const dropped = preservedTurns.shift();
|
|
1224
|
+
remaining += estimateTurnTokens(dropped);
|
|
1205
1225
|
}
|
|
1206
|
-
return [
|
|
1226
|
+
return [preservedTurns.flat(), true];
|
|
1207
1227
|
}
|
|
1208
|
-
const
|
|
1209
|
-
const
|
|
1210
|
-
for (let i =
|
|
1211
|
-
const cost =
|
|
1228
|
+
const olderTurns = turns.slice(0, turns.length - preservedTurns.length);
|
|
1229
|
+
const keptTurns = [];
|
|
1230
|
+
for (let i = olderTurns.length - 1; i >= 0; i--) {
|
|
1231
|
+
const cost = estimateTurnTokens(olderTurns[i]);
|
|
1212
1232
|
if (remaining - cost < 0) break;
|
|
1213
1233
|
remaining -= cost;
|
|
1214
|
-
|
|
1234
|
+
keptTurns.unshift(olderTurns[i]);
|
|
1215
1235
|
}
|
|
1216
|
-
const result = [...
|
|
1236
|
+
const result = [...keptTurns, ...preservedTurns].flat();
|
|
1217
1237
|
const trimmed = result.length < messages.length;
|
|
1218
1238
|
return [result, trimmed];
|
|
1219
1239
|
}
|
|
@@ -1815,7 +1835,8 @@ ${item.result.diff.patch}`;
|
|
|
1815
1835
|
async #executeBatch(calls) {
|
|
1816
1836
|
const toolCtx = {
|
|
1817
1837
|
cwd: this.#options.cwd,
|
|
1818
|
-
signal: this.#abortController.signal
|
|
1838
|
+
signal: this.#abortController.signal,
|
|
1839
|
+
writeRoots: this.#options.writeRoots
|
|
1819
1840
|
};
|
|
1820
1841
|
const allReadOnly = calls.every((tc) => {
|
|
1821
1842
|
const tool = this.#toolRegistry.get(tc.name);
|
|
@@ -1977,6 +1998,39 @@ function resolvePath(inputPath, cwd) {
|
|
|
1977
1998
|
const resolved = isAbsolute(inputPath) ? inputPath : resolve(cwd, inputPath);
|
|
1978
1999
|
return resolve(resolved);
|
|
1979
2000
|
}
|
|
2001
|
+
async function realPath(target) {
|
|
2002
|
+
try {
|
|
2003
|
+
return await realpath2(target);
|
|
2004
|
+
} catch {
|
|
2005
|
+
const parent = resolve(target, "..");
|
|
2006
|
+
try {
|
|
2007
|
+
const realParent = await realpath2(parent);
|
|
2008
|
+
return resolve(realParent, relative(parent, target));
|
|
2009
|
+
} catch {
|
|
2010
|
+
return resolve(target);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
async function confine(allowedRoots, target) {
|
|
2015
|
+
if (allowedRoots.length === 0) {
|
|
2016
|
+
return { ok: true };
|
|
2017
|
+
}
|
|
2018
|
+
const realTarget = await realPath(target);
|
|
2019
|
+
for (const root of allowedRoots) {
|
|
2020
|
+
const realRoot = await realPath(root);
|
|
2021
|
+
const rel = relative(realRoot, realTarget);
|
|
2022
|
+
if (!rel.startsWith("..") && rel !== "" && !rel.startsWith("/") && !rel.startsWith("\\")) {
|
|
2023
|
+
return { ok: true };
|
|
2024
|
+
}
|
|
2025
|
+
if (realTarget === realRoot) {
|
|
2026
|
+
return { ok: true };
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return {
|
|
2030
|
+
ok: false,
|
|
2031
|
+
error: `\u8DEF\u5F84 "${target}" \u4E0D\u5728\u5141\u8BB8\u7684\u5199\u5165\u8303\u56F4\u5185 ${allowedRoots.join(", ")}`
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
1980
2034
|
function truncateOutput(content, maxLength = DEFAULT_MAX_OUTPUT_LENGTH) {
|
|
1981
2035
|
if (content.length <= maxLength) return content;
|
|
1982
2036
|
const truncated = content.slice(0, maxLength);
|
|
@@ -2320,6 +2374,41 @@ function computeFileDiff(oldContent, newContent, filePath) {
|
|
|
2320
2374
|
};
|
|
2321
2375
|
}
|
|
2322
2376
|
|
|
2377
|
+
// src/tool/eol.ts
|
|
2378
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
2379
|
+
function detectEol(text) {
|
|
2380
|
+
if (text.length === 0) return "\n";
|
|
2381
|
+
const crlfIdx = text.indexOf("\r\n");
|
|
2382
|
+
const lfIdx = text.indexOf("\n");
|
|
2383
|
+
if (crlfIdx !== -1 && (lfIdx === -1 || crlfIdx <= lfIdx)) {
|
|
2384
|
+
return "\r\n";
|
|
2385
|
+
}
|
|
2386
|
+
return "\n";
|
|
2387
|
+
}
|
|
2388
|
+
function hasTrailingNewline(text) {
|
|
2389
|
+
return text.endsWith("\n") || text.endsWith("\r\n");
|
|
2390
|
+
}
|
|
2391
|
+
function normalizeEol(originalContent, newContent) {
|
|
2392
|
+
const targetEol = detectEol(originalContent);
|
|
2393
|
+
const lines = newContent.replace(/\r\n/g, "\n").split("\n");
|
|
2394
|
+
const originalHasTrailing = hasTrailingNewline(originalContent);
|
|
2395
|
+
const newHasTrailing = lines.length > 0 && lines[lines.length - 1] === "";
|
|
2396
|
+
let result = lines.join(targetEol);
|
|
2397
|
+
if (originalHasTrailing && !newHasTrailing) {
|
|
2398
|
+
result += targetEol;
|
|
2399
|
+
}
|
|
2400
|
+
if (!originalHasTrailing && newHasTrailing) {
|
|
2401
|
+
if (result.endsWith(targetEol)) {
|
|
2402
|
+
result = result.slice(0, -targetEol.length);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
return result;
|
|
2406
|
+
}
|
|
2407
|
+
async function writeFileWithEol(filePath, originalContent, newContent) {
|
|
2408
|
+
const content = originalContent.length > 0 ? normalizeEol(originalContent, newContent) : newContent;
|
|
2409
|
+
await writeFile3(filePath, content, "utf-8");
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2323
2412
|
// src/tool/builtins/read-file.ts
|
|
2324
2413
|
import { readFile as readFile4, stat } from "fs/promises";
|
|
2325
2414
|
import { open } from "fs/promises";
|
|
@@ -2427,7 +2516,7 @@ var readFileTool = {
|
|
|
2427
2516
|
};
|
|
2428
2517
|
|
|
2429
2518
|
// src/tool/builtins/write-file.ts
|
|
2430
|
-
import {
|
|
2519
|
+
import { mkdir as mkdir4, readFile as readFile5 } from "fs/promises";
|
|
2431
2520
|
import { dirname, relative as relative3, basename } from "path";
|
|
2432
2521
|
var writeFileSchema = {
|
|
2433
2522
|
type: "object",
|
|
@@ -2459,6 +2548,12 @@ var writeFileTool = {
|
|
|
2459
2548
|
}
|
|
2460
2549
|
const filePath = resolvePath(params.path, ctx.cwd);
|
|
2461
2550
|
try {
|
|
2551
|
+
if (ctx.writeRoots && ctx.writeRoots.length > 0) {
|
|
2552
|
+
const conf = await confine(ctx.writeRoots, filePath);
|
|
2553
|
+
if (!conf.ok) {
|
|
2554
|
+
return { success: false, data: conf.error, error: "OUTSIDE_WRITE_ROOTS" };
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2462
2557
|
let oldContent = "";
|
|
2463
2558
|
let existedBefore = false;
|
|
2464
2559
|
try {
|
|
@@ -2468,7 +2563,7 @@ var writeFileTool = {
|
|
|
2468
2563
|
}
|
|
2469
2564
|
await mkdir4(dirname(filePath), { recursive: true });
|
|
2470
2565
|
const content = String(params.content);
|
|
2471
|
-
await
|
|
2566
|
+
await writeFileWithEol(filePath, oldContent, content);
|
|
2472
2567
|
const diff = computeFileDiff(oldContent, content, filePath);
|
|
2473
2568
|
diff.existedBefore = existedBefore;
|
|
2474
2569
|
const lineCount = content.split("\n").length;
|
|
@@ -2495,7 +2590,7 @@ var writeFileTool = {
|
|
|
2495
2590
|
};
|
|
2496
2591
|
|
|
2497
2592
|
// src/tool/builtins/edit-file.ts
|
|
2498
|
-
import { readFile as readFile6
|
|
2593
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2499
2594
|
import { basename as basename2 } from "path";
|
|
2500
2595
|
var editFileSchema = {
|
|
2501
2596
|
type: "object",
|
|
@@ -2533,6 +2628,12 @@ var editFileTool = {
|
|
|
2533
2628
|
return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 new_text", error: "INVALID_ARGS" };
|
|
2534
2629
|
}
|
|
2535
2630
|
const filePath = resolvePath(params.path, ctx.cwd);
|
|
2631
|
+
if (ctx.writeRoots && ctx.writeRoots.length > 0) {
|
|
2632
|
+
const conf = await confine(ctx.writeRoots, filePath);
|
|
2633
|
+
if (!conf.ok) {
|
|
2634
|
+
return { success: false, data: conf.error, error: "OUTSIDE_WRITE_ROOTS" };
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2536
2637
|
try {
|
|
2537
2638
|
const content = await readFile6(filePath, "utf-8");
|
|
2538
2639
|
const firstIndex = content.indexOf(params.old_text);
|
|
@@ -2552,7 +2653,7 @@ var editFileTool = {
|
|
|
2552
2653
|
};
|
|
2553
2654
|
}
|
|
2554
2655
|
const newContent = content.replace(params.old_text, params.new_text);
|
|
2555
|
-
await
|
|
2656
|
+
await writeFileWithEol(filePath, content, newContent);
|
|
2556
2657
|
const diff = computeFileDiff(content, newContent, filePath);
|
|
2557
2658
|
diff.existedBefore = true;
|
|
2558
2659
|
const beforeText = content.slice(0, firstIndex);
|
|
@@ -2582,7 +2683,7 @@ ${oldLines} \u884C \u2192 ${newLines} \u884C
|
|
|
2582
2683
|
};
|
|
2583
2684
|
|
|
2584
2685
|
// src/tool/builtins/multi-edit.ts
|
|
2585
|
-
import { readFile as readFile7
|
|
2686
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2586
2687
|
import { basename as basename3 } from "path";
|
|
2587
2688
|
var multiEditSchema = {
|
|
2588
2689
|
type: "object",
|
|
@@ -2623,6 +2724,12 @@ var multiEditTool = {
|
|
|
2623
2724
|
return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 edits\uFF08\u975E\u7A7A\u6570\u7EC4\uFF09", error: "INVALID_ARGS" };
|
|
2624
2725
|
}
|
|
2625
2726
|
const filePath = resolvePath(params.path, ctx.cwd);
|
|
2727
|
+
if (ctx.writeRoots && ctx.writeRoots.length > 0) {
|
|
2728
|
+
const conf = await confine(ctx.writeRoots, filePath);
|
|
2729
|
+
if (!conf.ok) {
|
|
2730
|
+
return { success: false, data: conf.error, error: "OUTSIDE_WRITE_ROOTS" };
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2626
2733
|
try {
|
|
2627
2734
|
const originalContent = await readFile7(filePath, "utf-8");
|
|
2628
2735
|
let currentContent = originalContent;
|
|
@@ -2669,7 +2776,7 @@ var multiEditTool = {
|
|
|
2669
2776
|
currentContent = currentContent.replace(step.oldText, step.newText);
|
|
2670
2777
|
}
|
|
2671
2778
|
}
|
|
2672
|
-
await
|
|
2779
|
+
await writeFileWithEol(filePath, originalContent, currentContent);
|
|
2673
2780
|
const diff = computeFileDiff(originalContent, currentContent, filePath);
|
|
2674
2781
|
diff.existedBefore = true;
|
|
2675
2782
|
return {
|
|
@@ -2692,7 +2799,7 @@ var multiEditTool = {
|
|
|
2692
2799
|
};
|
|
2693
2800
|
|
|
2694
2801
|
// src/tool/builtins/delete-range.ts
|
|
2695
|
-
import { readFile as readFile8
|
|
2802
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
2696
2803
|
import { basename as basename4 } from "path";
|
|
2697
2804
|
var deleteRangeSchema = {
|
|
2698
2805
|
type: "object",
|
|
@@ -2750,6 +2857,12 @@ var deleteRangeTool = {
|
|
|
2750
2857
|
}
|
|
2751
2858
|
const filePath = resolvePath(params.path, ctx.cwd);
|
|
2752
2859
|
const inclusive = params.inclusive ?? false;
|
|
2860
|
+
if (ctx.writeRoots && ctx.writeRoots.length > 0) {
|
|
2861
|
+
const conf = await confine(ctx.writeRoots, filePath);
|
|
2862
|
+
if (!conf.ok) {
|
|
2863
|
+
return { success: false, data: conf.error, error: "OUTSIDE_WRITE_ROOTS" };
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2753
2866
|
try {
|
|
2754
2867
|
const content = await readFile8(filePath, "utf-8");
|
|
2755
2868
|
const lines = content.split("\n");
|
|
@@ -2781,14 +2894,8 @@ var deleteRangeTool = {
|
|
|
2781
2894
|
}
|
|
2782
2895
|
const newLines = [...lines.slice(0, rangeStart), ...lines.slice(rangeEnd + 1)];
|
|
2783
2896
|
const newContent = newLines.join("\n");
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
writeContent = newContent + "\n";
|
|
2787
|
-
} else if (content.endsWith("\r\n") && !newContent.endsWith("\r\n") && !newContent.endsWith("\n")) {
|
|
2788
|
-
writeContent = newContent + "\r\n";
|
|
2789
|
-
}
|
|
2790
|
-
await writeFile6(filePath, writeContent, "utf-8");
|
|
2791
|
-
const diff = computeFileDiff(content, writeContent, filePath);
|
|
2897
|
+
await writeFileWithEol(filePath, content, newContent);
|
|
2898
|
+
const diff = computeFileDiff(content, newContent, filePath);
|
|
2792
2899
|
diff.existedBefore = true;
|
|
2793
2900
|
const deletedLines = rangeEnd - rangeStart + 1;
|
|
2794
2901
|
const summary = `\u{1F4DD} \u4FEE\u6539: ${basename4(filePath)} (\u5220 ${deletedLines} \u884C, +${diff.additions} -${diff.deletions})`;
|
|
@@ -4224,7 +4331,7 @@ function BrickBreakerGame({ onExit: _onExit }) {
|
|
|
4224
4331
|
const [initialLevel, setInitialLevel] = useState5(1);
|
|
4225
4332
|
const [selectingLevel, setSelectingLevel] = useState5(true);
|
|
4226
4333
|
const stateRef = useRef3(createInitialState(initialLevel));
|
|
4227
|
-
const [
|
|
4334
|
+
const [tick2, setTick] = useState5(0);
|
|
4228
4335
|
const onExitRef = useRef3(_onExit);
|
|
4229
4336
|
onExitRef.current = _onExit;
|
|
4230
4337
|
useEffect4(() => {
|
|
@@ -4279,7 +4386,7 @@ function BrickBreakerGame({ onExit: _onExit }) {
|
|
|
4279
4386
|
const aliveCount = s.bricks.filter((b) => b.alive).length;
|
|
4280
4387
|
const board = buildBoard(s);
|
|
4281
4388
|
const def = getLevel(s.level);
|
|
4282
|
-
void
|
|
4389
|
+
void tick2;
|
|
4283
4390
|
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
|
|
4284
4391
|
/* @__PURE__ */ jsxs10(Box10, { flexDirection: "row", children: [
|
|
4285
4392
|
/* @__PURE__ */ jsx10(Box10, { width: 20, children: /* @__PURE__ */ jsxs10(Text11, { children: [
|
|
@@ -4754,7 +4861,7 @@ function buildGameView(s, scoreLines, scoreColor, message) {
|
|
|
4754
4861
|
}
|
|
4755
4862
|
function CoderCheck({ onExit: _onExit }) {
|
|
4756
4863
|
const stateRef = useRef4(createInitialState2());
|
|
4757
|
-
const [
|
|
4864
|
+
const [tick2, setTick] = useState6(0);
|
|
4758
4865
|
const [colorOffset, setColorOffset] = useState6(0);
|
|
4759
4866
|
const onExitRef = useRef4(_onExit);
|
|
4760
4867
|
onExitRef.current = _onExit;
|
|
@@ -4832,7 +4939,7 @@ function CoderCheck({ onExit: _onExit }) {
|
|
|
4832
4939
|
const scoreStr = String(s.score).padStart(5, "0");
|
|
4833
4940
|
const scoreLines = buildScoreLines(scoreStr);
|
|
4834
4941
|
const view = buildGameView(s, scoreLines, scoreColor, s.message);
|
|
4835
|
-
void
|
|
4942
|
+
void tick2;
|
|
4836
4943
|
return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, children: [
|
|
4837
4944
|
/* @__PURE__ */ jsx11(Box11, { flexDirection: "row", children: /* @__PURE__ */ jsxs11(Text12, { children: [
|
|
4838
4945
|
"\u751F\u547D ",
|
|
@@ -4889,16 +4996,375 @@ var coder_check_default = {
|
|
|
4889
4996
|
}
|
|
4890
4997
|
};
|
|
4891
4998
|
|
|
4999
|
+
// src/game/snake/engine.ts
|
|
5000
|
+
function isOpposite(a, b) {
|
|
5001
|
+
return a === "UP" && b === "DOWN" || a === "DOWN" && b === "UP" || a === "LEFT" && b === "RIGHT" || a === "RIGHT" && b === "LEFT";
|
|
5002
|
+
}
|
|
5003
|
+
function samePoint(a, b) {
|
|
5004
|
+
return a.x === b.x && a.y === b.y;
|
|
5005
|
+
}
|
|
5006
|
+
function movePoint(p, dir) {
|
|
5007
|
+
switch (dir) {
|
|
5008
|
+
case "UP":
|
|
5009
|
+
return { x: p.x, y: p.y - 1 };
|
|
5010
|
+
case "DOWN":
|
|
5011
|
+
return { x: p.x, y: p.y + 1 };
|
|
5012
|
+
case "LEFT":
|
|
5013
|
+
return { x: p.x - 1, y: p.y };
|
|
5014
|
+
case "RIGHT":
|
|
5015
|
+
return { x: p.x + 1, y: p.y };
|
|
5016
|
+
}
|
|
5017
|
+
}
|
|
5018
|
+
function wrapPoint(p, config) {
|
|
5019
|
+
return {
|
|
5020
|
+
x: (p.x % config.width + config.width) % config.width,
|
|
5021
|
+
y: (p.y % config.height + config.height) % config.height
|
|
5022
|
+
};
|
|
5023
|
+
}
|
|
5024
|
+
function generateFood(snake, config, type, excludePositions = []) {
|
|
5025
|
+
const occupied = new Set(snake.map((p) => `${p.x},${p.y}`));
|
|
5026
|
+
for (const p of excludePositions) {
|
|
5027
|
+
occupied.add(`${p.x},${p.y}`);
|
|
5028
|
+
}
|
|
5029
|
+
const totalCells = config.width * config.height;
|
|
5030
|
+
if (occupied.size >= totalCells) return null;
|
|
5031
|
+
let pos;
|
|
5032
|
+
do {
|
|
5033
|
+
pos = {
|
|
5034
|
+
x: Math.floor(Math.random() * config.width),
|
|
5035
|
+
y: Math.floor(Math.random() * config.height)
|
|
5036
|
+
};
|
|
5037
|
+
} while (occupied.has(`${pos.x},${pos.y}`));
|
|
5038
|
+
return {
|
|
5039
|
+
position: pos,
|
|
5040
|
+
type,
|
|
5041
|
+
...type === "special" ? { spawnedAt: Date.now() } : {}
|
|
5042
|
+
};
|
|
5043
|
+
}
|
|
5044
|
+
function createInitialState3(config) {
|
|
5045
|
+
const startX = Math.floor(config.width / 2);
|
|
5046
|
+
const startY = Math.floor(config.height / 2);
|
|
5047
|
+
const snake = [
|
|
5048
|
+
{ x: startX, y: startY },
|
|
5049
|
+
{ x: startX - 1, y: startY },
|
|
5050
|
+
{ x: startX - 2, y: startY }
|
|
5051
|
+
];
|
|
5052
|
+
const food = generateFood(snake, config, "normal");
|
|
5053
|
+
return {
|
|
5054
|
+
snake,
|
|
5055
|
+
direction: "RIGHT",
|
|
5056
|
+
nextDirection: "RIGHT",
|
|
5057
|
+
food,
|
|
5058
|
+
specialFood: null,
|
|
5059
|
+
score: 0,
|
|
5060
|
+
speedLevel: 1,
|
|
5061
|
+
isGameOver: false,
|
|
5062
|
+
isPaused: false,
|
|
5063
|
+
wallWrap: Math.random() < 0.5,
|
|
5064
|
+
tickInterval: config.initialSpeed
|
|
5065
|
+
};
|
|
5066
|
+
}
|
|
5067
|
+
function tick(state, config) {
|
|
5068
|
+
state.direction = state.nextDirection;
|
|
5069
|
+
const head = state.snake[0];
|
|
5070
|
+
let newHead = movePoint(head, state.direction);
|
|
5071
|
+
const outOfBounds = newHead.x < 0 || newHead.x >= config.width || newHead.y < 0 || newHead.y >= config.height;
|
|
5072
|
+
if (outOfBounds) {
|
|
5073
|
+
if (state.wallWrap) {
|
|
5074
|
+
newHead = wrapPoint(newHead, config);
|
|
5075
|
+
} else {
|
|
5076
|
+
state.isGameOver = true;
|
|
5077
|
+
return;
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
const ateNormal = samePoint(newHead, state.food.position);
|
|
5081
|
+
const ateSpecial = state.specialFood !== null && samePoint(newHead, state.specialFood.position);
|
|
5082
|
+
const willEat = ateNormal || ateSpecial;
|
|
5083
|
+
const bodyToCheck = willEat ? state.snake : state.snake.slice(0, -1);
|
|
5084
|
+
for (const segment of bodyToCheck) {
|
|
5085
|
+
if (samePoint(newHead, segment)) {
|
|
5086
|
+
state.isGameOver = true;
|
|
5087
|
+
return;
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
state.snake.unshift(newHead);
|
|
5091
|
+
if (ateNormal) {
|
|
5092
|
+
state.score += 1;
|
|
5093
|
+
updateSpeed(state, config);
|
|
5094
|
+
const exclude = state.specialFood ? [state.specialFood.position] : [];
|
|
5095
|
+
const newFood = generateFood(state.snake, config, "normal", exclude);
|
|
5096
|
+
if (newFood) {
|
|
5097
|
+
state.food = newFood;
|
|
5098
|
+
} else {
|
|
5099
|
+
state.isGameOver = true;
|
|
5100
|
+
return;
|
|
5101
|
+
}
|
|
5102
|
+
if (state.specialFood === null && Math.random() < 0.25) {
|
|
5103
|
+
state.specialFood = generateFood(state.snake, config, "special", [state.food.position]);
|
|
5104
|
+
}
|
|
5105
|
+
} else if (ateSpecial) {
|
|
5106
|
+
state.score += 3;
|
|
5107
|
+
updateSpeed(state, config);
|
|
5108
|
+
state.specialFood = null;
|
|
5109
|
+
} else {
|
|
5110
|
+
state.snake.pop();
|
|
5111
|
+
}
|
|
5112
|
+
if (state.specialFood?.spawnedAt !== void 0) {
|
|
5113
|
+
if (Date.now() - state.specialFood.spawnedAt > 5e3) {
|
|
5114
|
+
state.specialFood = null;
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
5118
|
+
function updateSpeed(state, config) {
|
|
5119
|
+
const newLevel = Math.floor(state.score / config.scorePerLevel) + 1;
|
|
5120
|
+
state.speedLevel = newLevel;
|
|
5121
|
+
state.tickInterval = Math.max(
|
|
5122
|
+
config.minSpeed,
|
|
5123
|
+
config.initialSpeed - (newLevel - 1) * config.speedDecrement
|
|
5124
|
+
);
|
|
5125
|
+
}
|
|
5126
|
+
function changeDirection(state, newDir) {
|
|
5127
|
+
if (!isOpposite(newDir, state.direction)) {
|
|
5128
|
+
state.nextDirection = newDir;
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
|
|
5132
|
+
// src/game/snake/renderer.ts
|
|
5133
|
+
import chalk4 from "chalk";
|
|
5134
|
+
var CELL_W = 2;
|
|
5135
|
+
var EMPTY = " ";
|
|
5136
|
+
var BODY_CHAR = "\u2593";
|
|
5137
|
+
var HEAD_CHAR = "\u2593";
|
|
5138
|
+
var NORMAL_FOOD_CHAR = "\u25C6";
|
|
5139
|
+
var SPECIAL_FOOD_CHAR = "\u2605";
|
|
5140
|
+
var CSI = "\x1B[";
|
|
5141
|
+
var CURSOR_HIDE = `${CSI}?25l`;
|
|
5142
|
+
var CURSOR_SHOW = `${CSI}?25h`;
|
|
5143
|
+
var CURSOR_HOME = `${CSI}H`;
|
|
5144
|
+
function hideCursor() {
|
|
5145
|
+
process.stdout.write(CURSOR_HIDE);
|
|
5146
|
+
}
|
|
5147
|
+
function showCursor() {
|
|
5148
|
+
process.stdout.write(CURSOR_SHOW);
|
|
5149
|
+
}
|
|
5150
|
+
function clearScreen() {
|
|
5151
|
+
process.stdout.write(`${CSI}2J${CURSOR_HOME}`);
|
|
5152
|
+
}
|
|
5153
|
+
function cellColor(char, type) {
|
|
5154
|
+
switch (type) {
|
|
5155
|
+
case "snake-head":
|
|
5156
|
+
return chalk4.greenBright(char);
|
|
5157
|
+
case "snake-body":
|
|
5158
|
+
return chalk4.green(char);
|
|
5159
|
+
case "normal-food":
|
|
5160
|
+
return chalk4.red(char);
|
|
5161
|
+
case "special-food":
|
|
5162
|
+
return chalk4.yellow(char);
|
|
5163
|
+
case "empty":
|
|
5164
|
+
return char;
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
function renderToTerminal(state, config) {
|
|
5168
|
+
const lines = buildRenderLines(state, config);
|
|
5169
|
+
process.stdout.write(`${CSI}2J${CURSOR_HOME}` + lines.join("\n"));
|
|
5170
|
+
}
|
|
5171
|
+
function buildRenderLines(state, config) {
|
|
5172
|
+
const { width: W, height: H } = config;
|
|
5173
|
+
const lines = [];
|
|
5174
|
+
const grid = Array.from({ length: H }, () => Array(W).fill("empty"));
|
|
5175
|
+
for (let i = 1; i < state.snake.length; i++) {
|
|
5176
|
+
const seg = state.snake[i];
|
|
5177
|
+
if (seg.y >= 0 && seg.y < H && seg.x >= 0 && seg.x < W) {
|
|
5178
|
+
grid[seg.y][seg.x] = "snake-body";
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
const head = state.snake[0];
|
|
5182
|
+
if (head.y >= 0 && head.y < H && head.x >= 0 && head.x < W) {
|
|
5183
|
+
grid[head.y][head.x] = "snake-head";
|
|
5184
|
+
}
|
|
5185
|
+
const fp = state.food.position;
|
|
5186
|
+
if (grid[fp.y][fp.x] === "empty") {
|
|
5187
|
+
grid[fp.y][fp.x] = "normal-food";
|
|
5188
|
+
}
|
|
5189
|
+
if (state.specialFood) {
|
|
5190
|
+
const sp = state.specialFood.position;
|
|
5191
|
+
if (grid[sp.y][sp.x] === "empty") {
|
|
5192
|
+
grid[sp.y][sp.x] = "special-food";
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
lines.push(" " + chalk4.gray("\u2554" + "\u2550\u2550".repeat(W) + "\u2557"));
|
|
5196
|
+
for (let y = 0; y < H; y++) {
|
|
5197
|
+
let row = " " + chalk4.gray("\u2551");
|
|
5198
|
+
for (let x = 0; x < W; x++) {
|
|
5199
|
+
const cellType = grid[y][x];
|
|
5200
|
+
let char;
|
|
5201
|
+
switch (cellType) {
|
|
5202
|
+
case "snake-head":
|
|
5203
|
+
char = HEAD_CHAR.repeat(CELL_W);
|
|
5204
|
+
break;
|
|
5205
|
+
case "snake-body":
|
|
5206
|
+
char = BODY_CHAR.repeat(CELL_W);
|
|
5207
|
+
break;
|
|
5208
|
+
case "normal-food":
|
|
5209
|
+
char = NORMAL_FOOD_CHAR + " ";
|
|
5210
|
+
break;
|
|
5211
|
+
case "special-food":
|
|
5212
|
+
char = SPECIAL_FOOD_CHAR + " ";
|
|
5213
|
+
break;
|
|
5214
|
+
default:
|
|
5215
|
+
char = EMPTY;
|
|
5216
|
+
}
|
|
5217
|
+
row += cellColor(char, cellType);
|
|
5218
|
+
}
|
|
5219
|
+
row += chalk4.gray("\u2551");
|
|
5220
|
+
lines.push(row);
|
|
5221
|
+
}
|
|
5222
|
+
lines.push(" " + chalk4.gray("\u255A" + "\u2550\u2550".repeat(W) + "\u255D"));
|
|
5223
|
+
lines.push("");
|
|
5224
|
+
lines.push(
|
|
5225
|
+
` ${chalk4.cyan("\u5206\u6570")}: ${state.score} ${chalk4.cyan("\u957F\u5EA6")}: ${state.snake.length} ${chalk4.cyan("\u901F\u5EA6")}: Lv.${state.speedLevel} ${chalk4.cyan("\u8FB9\u754C")}: ${state.wallWrap ? "\u7A7F\u5899" : "\u649E\u5899"}`
|
|
5226
|
+
);
|
|
5227
|
+
if (state.specialFood?.spawnedAt !== void 0) {
|
|
5228
|
+
const remaining = Math.ceil((5e3 - (Date.now() - state.specialFood.spawnedAt)) / 1e3);
|
|
5229
|
+
if (remaining > 0) {
|
|
5230
|
+
lines.push(` ${chalk4.yellow(`\u2B50 \u7279\u6B8A\u98DF\u7269 ${remaining} \u79D2\u540E\u6D88\u5931`)}`);
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
lines.push(` ${chalk4.gray("W/A/S/D \u6216 \u65B9\u5411\u952E\u79FB\u52A8 | P \u6682\u505C | M \u5207\u6362\u6A21\u5F0F | Q \u9000\u51FA")}`);
|
|
5234
|
+
if (state.isPaused) {
|
|
5235
|
+
lines.push("");
|
|
5236
|
+
lines.push(` ${chalk4.yellow("\u23F8 \u5DF2\u6682\u505C \u2014 \u6309 P \u7EE7\u7EED")}`);
|
|
5237
|
+
}
|
|
5238
|
+
if (state.isGameOver) {
|
|
5239
|
+
lines.push("");
|
|
5240
|
+
lines.push(` ${chalk4.red.bold("\u{1F480} \u6E38\u620F\u7ED3\u675F!")} ${chalk4.cyan("\u6700\u7EC8\u5206\u6570: " + state.score)}`);
|
|
5241
|
+
lines.push(` ${chalk4.gray("\u6309 R \u91CD\u65B0\u5F00\u59CB | \u6309 Q \u9000\u51FA")}`);
|
|
5242
|
+
}
|
|
5243
|
+
return lines;
|
|
5244
|
+
}
|
|
5245
|
+
|
|
5246
|
+
// src/game/snake/input.ts
|
|
5247
|
+
import * as readline from "readline";
|
|
5248
|
+
var KEY_TO_DIR = {
|
|
5249
|
+
up: "UP",
|
|
5250
|
+
w: "UP",
|
|
5251
|
+
W: "UP",
|
|
5252
|
+
down: "DOWN",
|
|
5253
|
+
s: "DOWN",
|
|
5254
|
+
S: "DOWN",
|
|
5255
|
+
left: "LEFT",
|
|
5256
|
+
a: "LEFT",
|
|
5257
|
+
A: "LEFT",
|
|
5258
|
+
right: "RIGHT",
|
|
5259
|
+
d: "RIGHT",
|
|
5260
|
+
D: "RIGHT"
|
|
5261
|
+
};
|
|
5262
|
+
function setupInput(ctx, onQuit, onRestart) {
|
|
5263
|
+
readline.emitKeypressEvents(process.stdin);
|
|
5264
|
+
if (process.stdin.isTTY) {
|
|
5265
|
+
process.stdin.setRawMode(true);
|
|
5266
|
+
}
|
|
5267
|
+
const handler = (_, key) => {
|
|
5268
|
+
if (!key || !key.name) return;
|
|
5269
|
+
const { name, ctrl } = key;
|
|
5270
|
+
if (ctrl && (name === "c" || name === "d")) {
|
|
5271
|
+
onQuit();
|
|
5272
|
+
return;
|
|
5273
|
+
}
|
|
5274
|
+
if (name === "q") {
|
|
5275
|
+
onQuit();
|
|
5276
|
+
return;
|
|
5277
|
+
}
|
|
5278
|
+
const state = ctx.state;
|
|
5279
|
+
if (state.isGameOver) {
|
|
5280
|
+
if (name === "r") {
|
|
5281
|
+
ctx.state = onRestart();
|
|
5282
|
+
}
|
|
5283
|
+
return;
|
|
5284
|
+
}
|
|
5285
|
+
if (name === "p") {
|
|
5286
|
+
state.isPaused = !state.isPaused;
|
|
5287
|
+
return;
|
|
5288
|
+
}
|
|
5289
|
+
if (name === "m") {
|
|
5290
|
+
state.wallWrap = !state.wallWrap;
|
|
5291
|
+
return;
|
|
5292
|
+
}
|
|
5293
|
+
const dir = KEY_TO_DIR[name];
|
|
5294
|
+
if (dir) {
|
|
5295
|
+
changeDirection(state, dir);
|
|
5296
|
+
}
|
|
5297
|
+
};
|
|
5298
|
+
process.stdin.on("keypress", handler);
|
|
5299
|
+
return () => {
|
|
5300
|
+
process.stdin.removeListener("keypress", handler);
|
|
5301
|
+
if (process.stdin.isTTY) {
|
|
5302
|
+
process.stdin.setRawMode(false);
|
|
5303
|
+
}
|
|
5304
|
+
};
|
|
5305
|
+
}
|
|
5306
|
+
|
|
5307
|
+
// src/game/snake/index.ts
|
|
5308
|
+
var CONFIG = {
|
|
5309
|
+
width: 20,
|
|
5310
|
+
height: 16,
|
|
5311
|
+
initialSpeed: 180,
|
|
5312
|
+
minSpeed: 60,
|
|
5313
|
+
speedDecrement: 10,
|
|
5314
|
+
scorePerLevel: 5
|
|
5315
|
+
};
|
|
5316
|
+
var IDLE_INTERVAL = 200;
|
|
5317
|
+
var snakeGame = {
|
|
5318
|
+
id: "snake",
|
|
5319
|
+
name: "Big Snake",
|
|
5320
|
+
description: "\u7ECF\u5178\u8D2A\u5403\u86C7\uFF0C\u53CC\u98DF\u7269\u7CFB\u7EDF\u3001\u96BE\u5EA6\u9012\u589E\u3001\u7A7F\u5899/\u649E\u5899\u6A21\u5F0F",
|
|
5321
|
+
play: async () => {
|
|
5322
|
+
const ctx = { state: createInitialState3(CONFIG) };
|
|
5323
|
+
return new Promise((resolve2) => {
|
|
5324
|
+
const cleanupInput = setupInput(
|
|
5325
|
+
ctx,
|
|
5326
|
+
() => {
|
|
5327
|
+
stop = true;
|
|
5328
|
+
clearTimeout(timerId);
|
|
5329
|
+
showCursor();
|
|
5330
|
+
cleanupInput();
|
|
5331
|
+
resolve2();
|
|
5332
|
+
},
|
|
5333
|
+
() => {
|
|
5334
|
+
return createInitialState3(CONFIG);
|
|
5335
|
+
}
|
|
5336
|
+
);
|
|
5337
|
+
clearScreen();
|
|
5338
|
+
hideCursor();
|
|
5339
|
+
let stop = false;
|
|
5340
|
+
let timerId;
|
|
5341
|
+
function loop() {
|
|
5342
|
+
if (stop) return;
|
|
5343
|
+
const s = ctx.state;
|
|
5344
|
+
if (!s.isGameOver && !s.isPaused) {
|
|
5345
|
+
tick(s, CONFIG);
|
|
5346
|
+
}
|
|
5347
|
+
renderToTerminal(s, CONFIG);
|
|
5348
|
+
const interval = s.isGameOver || s.isPaused ? IDLE_INTERVAL : s.tickInterval;
|
|
5349
|
+
timerId = setTimeout(loop, interval);
|
|
5350
|
+
}
|
|
5351
|
+
timerId = setTimeout(loop, 0);
|
|
5352
|
+
});
|
|
5353
|
+
}
|
|
5354
|
+
};
|
|
5355
|
+
var snake_default = snakeGame;
|
|
5356
|
+
|
|
4892
5357
|
// src/game/registry.ts
|
|
4893
5358
|
function initGames() {
|
|
4894
5359
|
registerGame(brick_breaker_default);
|
|
4895
5360
|
registerGame(coder_check_default);
|
|
5361
|
+
registerGame(snake_default);
|
|
4896
5362
|
return listGames();
|
|
4897
5363
|
}
|
|
4898
5364
|
|
|
4899
5365
|
// src/cli/index.tsx
|
|
4900
5366
|
import { render as render4 } from "ink";
|
|
4901
|
-
import
|
|
5367
|
+
import chalk5 from "chalk";
|
|
4902
5368
|
|
|
4903
5369
|
// src/stock/StockList.tsx
|
|
4904
5370
|
import { Box as Box12, Text as Text13, useInput as useInput5 } from "ink";
|
|
@@ -5331,7 +5797,7 @@ function createCli() {
|
|
|
5331
5797
|
const key = await promptForApiKey();
|
|
5332
5798
|
if (!key) process.exit(1);
|
|
5333
5799
|
const savedPath = await saveApiKey(key);
|
|
5334
|
-
console.log(` ${
|
|
5800
|
+
console.log(` ${chalk5.green("\u2714")} API Key \u5DF2\u4FDD\u5B58\u5230 ${chalk5.dim(savedPath)}
|
|
5335
5801
|
`);
|
|
5336
5802
|
const result = await loadAndValidate();
|
|
5337
5803
|
ctx = { ...ctx, config: result.config };
|
|
@@ -5478,8 +5944,8 @@ compdef _dskcode_completion dskcode`);
|
|
|
5478
5944
|
{ code: "sh601899" }
|
|
5479
5945
|
];
|
|
5480
5946
|
const savedPath = await saveStockConfig(defaultSymbols);
|
|
5481
|
-
console.log(`${
|
|
5482
|
-
console.log(`${
|
|
5947
|
+
console.log(`${chalk5.green("\u2714")} \u5DF2\u751F\u6210\u81EA\u9009\u80A1\u914D\u7F6E: ${chalk5.dim(savedPath)}`);
|
|
5948
|
+
console.log(`${chalk5.dim(" \u63D0\u793A: \u53EF\u7F16\u8F91\u4E0A\u8FF0\u6587\u4EF6\u81EA\u5B9A\u4E49\u81EA\u9009\u80A1\u5217\u8868")}
|
|
5483
5949
|
`);
|
|
5484
5950
|
}
|
|
5485
5951
|
const freshResult = await loadAndValidate();
|
|
@@ -5533,7 +5999,7 @@ compdef _dskcode_completion dskcode`);
|
|
|
5533
5999
|
});
|
|
5534
6000
|
if (selectedGame) {
|
|
5535
6001
|
console.log(`
|
|
5536
|
-
\u542F\u52A8\u6E38\u620F: ${
|
|
6002
|
+
\u542F\u52A8\u6E38\u620F: ${chalk5.green(selectedGame.name)}
|
|
5537
6003
|
`);
|
|
5538
6004
|
await selectedGame.play();
|
|
5539
6005
|
}
|