ice-web-components 1.2.0 → 1.4.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.
@@ -8,6 +8,14 @@ export declare class ICELabel extends ICEWidget {
8
8
  constructor(props?: any);
9
9
  setText(text: string): this;
10
10
  getText(): string;
11
+ /**
12
+ * 改文字颜色(动态强调 / 置灰用,比如 BIOS 自检行的灰→黄→绿、菜单选中态)。
13
+ *
14
+ * 必须走内层 `ICEText`:文字是它画的,构造期 `style` 已经下沉到那一层,
15
+ * 之后再 `setState({ style })` 只会改到外壳容器,屏幕上的字纹丝不动。
16
+ * 只覆盖 `fillStyle`,字号 / 字体 / 对齐这些原样保留。
17
+ */
18
+ setTextColor(color: string): this;
11
19
  /**
12
20
  * 让标签自身占据文字的实测尺寸。
13
21
  *
@@ -112,3 +112,8 @@ export * from './model/ICEChip8Model';
112
112
  export * from './model/ICE2048Model';
113
113
  export * from './model/ICEHistoryModel';
114
114
  export * from './model/ICEPixelModel';
115
+ export * from './model/ICEBiosModel';
116
+ export * from './model/ICETracePlayerModel';
117
+ export * from './model/ICESortModel';
118
+ export * from './model/ICEMazeModel';
119
+ export * from './model/ICEDosModel';
@@ -0,0 +1,124 @@
1
+ /**
2
+ * 掌机 BIOS 的纯逻辑(不碰 canvas、不碰 DOM)。
3
+ *
4
+ * 掌机开机先跑一段「自检 → 菜单」的引导,然后才把控制权交给卡带:
5
+ * 自检**按时序推进**(页面每帧调 `tick(dt)`),菜单是**光标 + 确认**的老式控制台交互。
6
+ * 页面只负责把 `getSteps()` / `getMenuEntries()` 画成文字,并执行 `confirm()` 返回的动作 ——
7
+ * 状态机与渲染分开,所以「自检要跑多久、菜单怎么绕、设置存到哪」都能在 node 里单测。
8
+ *
9
+ * 约定:
10
+ * - 自检项由 `steps` 注入(默认 5 项:CPU / RAM / VRAM / SOUND / CART),每项有自己的耗时;
11
+ * - 相位机:`post`(自检中)→ `menu`(启动菜单)或 `boot`(直接启动,快速启动开着时);
12
+ * `settings`(设置页)从菜单进、`back()` 回菜单;
13
+ * - 菜单光标上下**循环**(到头绕回去,老式 BIOS 都这样),进设置前记下位置、回来还停在那儿;
14
+ * - 设置(快速启动 / 默认卡带)落到注入的存储里;坏数据、写盘失败一律降级,绝不抛。
15
+ */
16
+ export declare type ICEBiosPhase = 'post' | 'menu' | 'settings' | 'boot';
17
+ export interface ICEBiosStep {
18
+ /** 稳定 key(页面拿它做节点 id / 维护增量更新) */
19
+ key: string;
20
+ label: string;
21
+ detail: string;
22
+ /** 这一项「检测」要花多久(毫秒) */
23
+ duration: number;
24
+ }
25
+ export interface ICEBiosStepState extends ICEBiosStep {
26
+ state: 'pending' | 'running' | 'ok';
27
+ /** 当前项的进度 0..1;非当前项是 0 或 1 */
28
+ progress: number;
29
+ }
30
+ export interface ICEBiosCartridge {
31
+ key: string;
32
+ label: string;
33
+ }
34
+ export interface ICEBiosMenuEntry {
35
+ key: string;
36
+ label: string;
37
+ type: 'cartridge' | 'settings' | 'boot';
38
+ }
39
+ export interface ICEBiosSettings {
40
+ /** 开着就跳过菜单直接启动默认卡带(开机自检还是要跑) */
41
+ quickBoot: boolean;
42
+ defaultCartridge: string;
43
+ }
44
+ export declare type ICEBiosAction = {
45
+ type: 'boot';
46
+ cartridge: string;
47
+ } | {
48
+ type: 'settings';
49
+ } | {
50
+ type: 'menu';
51
+ } | {
52
+ type: 'none';
53
+ };
54
+ export interface ICEBiosStorage {
55
+ getItem(key: string): string | null;
56
+ setItem(key: string, value: string): void;
57
+ }
58
+ export interface ICEBiosOptions {
59
+ cartridges: ICEBiosCartridge[];
60
+ /** 自检项,默认 `ICE_BIOS_DEFAULT_STEPS` */
61
+ steps?: ICEBiosStep[];
62
+ /** 初始设置(优先级低于存储里的值) */
63
+ settings?: Partial<ICEBiosSettings>;
64
+ /** 存储实现;传 null 表示只放内存;不传则用 globalThis.localStorage(没有就退化成内存) */
65
+ storage?: ICEBiosStorage | null;
66
+ /** 存储键前缀,默认 `ice-arcade-bios-` */
67
+ prefix?: string;
68
+ }
69
+ export declare type ICEBiosListener = (model: ICEBiosModel) => void;
70
+ /** 默认自检项:每一项都是一句「老式 BIOS 会印在屏幕上的话」。 */
71
+ export declare const ICE_BIOS_DEFAULT_STEPS: ICEBiosStep[];
72
+ export declare class ICEBiosModel {
73
+ private steps;
74
+ private cartridges;
75
+ private storage;
76
+ private storageKey;
77
+ private listeners;
78
+ private phase;
79
+ private elapsed;
80
+ private cursor;
81
+ private cursorBeforeSettings;
82
+ private settings;
83
+ constructor(options: ICEBiosOptions);
84
+ getPhase(): ICEBiosPhase;
85
+ getCartridges(): ICEBiosCartridge[];
86
+ getPostElapsed(): number;
87
+ /** 自检总时长(毫秒)。 */
88
+ getPostDuration(): number;
89
+ /** 自检进度 0..1(按时间算,页面拿它画进度条)。 */
90
+ getPostProgress(): number;
91
+ /** 自检项与它们此刻的状态(当前项 running、前面的 ok)。 */
92
+ getSteps(): ICEBiosStepState[];
93
+ /** 菜单项:卡带列表 + 设置 + 退出并启动。 */
94
+ getMenuEntries(): ICEBiosMenuEntry[];
95
+ getCursor(): number;
96
+ getSelectedEntry(): ICEBiosMenuEntry | null;
97
+ getSettings(): ICEBiosSettings;
98
+ isQuickBoot(): boolean;
99
+ getDefaultCartridge(): string;
100
+ /** 推进自检(页面每帧调用),返回是否发生了变化。 */
101
+ tick(delta: number): boolean;
102
+ moveCursor(delta: number): void;
103
+ setCursor(index: number): void;
104
+ /** 确认当前项:只返回动作,执行动作是页面的事(好测)。 */
105
+ confirm(): ICEBiosAction;
106
+ /** 返回上一层(设置 → 菜单)。 */
107
+ back(): void;
108
+ /** 从游戏里回到 BIOS 菜单(掌机的「复位」)。 */
109
+ openMenu(): void;
110
+ /** 重新跑一遍自检(再开一次机)。 */
111
+ restart(): void;
112
+ toggleQuickBoot(): boolean;
113
+ setDefaultCartridge(key: string): void;
114
+ addChangeListener(listener: ICEBiosListener): () => void;
115
+ private __alignCursorToDefault;
116
+ /** 卡带 key 必须真的存在,否则回退到第一个卡带。 */
117
+ private __normalizeCartridge;
118
+ private __load;
119
+ private __readStorage;
120
+ private __save;
121
+ private __defaultStorage;
122
+ private notify;
123
+ }
124
+ export default ICEBiosModel;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * DOS 终端(虚拟文件系统 + 命令解释器,纯逻辑,不碰 DOM)。
3
+ *
4
+ * 页面只做三件事:把 `run()` 返回的行画成文字、把键盘输入交给模型、按 `effect` 做副作用
5
+ * (`cls` 清屏 / `exit` 退出)。所以「命令怎么解析、路径怎么算、文件怎么改」全都能在 node 里断言。
6
+ *
7
+ * 约定:
8
+ * - 路径分隔符 `\` 与 `/` 等价;支持绝对(`\GAMES`)、相对(`GAMES`)、`.`、`..`;
9
+ * - 命令大小写不敏感(`DIR` = `dir`),参数里的文件名也大小写不敏感;
10
+ * - `run()` 从不抛:任何坏输入都变成一行 `error`,终端不会因为打错字崩掉;
11
+ * - `echo x > a.txt` / `echo x >> a.txt` 也是模型的一部分(重定向在解析层处理掉);
12
+ * - 历史和 Tab 补全归模型管(真终端也有这两个),页面只管按键。
13
+ */
14
+ export declare type ICEDosLineType = 'output' | 'error';
15
+ export interface ICEDosLine {
16
+ text: string;
17
+ type: ICEDosLineType;
18
+ }
19
+ export interface ICEDosEffect {
20
+ clear?: boolean;
21
+ exit?: boolean;
22
+ }
23
+ export interface ICEDosResult {
24
+ lines: ICEDosLine[];
25
+ effect?: ICEDosEffect;
26
+ }
27
+ export interface ICEDosOptions {
28
+ /** 时间源(`date` / `time` 用),测试可注入固定时间 */
29
+ now?: () => Date;
30
+ }
31
+ export declare class ICEDosModel {
32
+ private root;
33
+ private cwd;
34
+ private history;
35
+ private historyIndex;
36
+ private now;
37
+ constructor(options?: ICEDosOptions);
38
+ getCwd(): string;
39
+ getPrompt(): string;
40
+ getBanner(): string[];
41
+ getHistory(): string[];
42
+ /** 执行一行命令。永不抛:坏输入变成一行 error。 */
43
+ run(input: string): ICEDosResult;
44
+ historyPrev(): string;
45
+ historyNext(): string;
46
+ /**
47
+ * Tab 补全:命令名补到唯一前缀(带空格),路径按当前目录补(目录名不带空格,方便继续往下打)。
48
+ * 没有匹配 / 多个匹配时原样返回(真 DOS 会再按一次列出候选,这里从简)。
49
+ */
50
+ complete(input: string): string;
51
+ private __dir;
52
+ private __cd;
53
+ private __mkdir;
54
+ private __rmdir;
55
+ private __type;
56
+ private __echo;
57
+ private __copy;
58
+ private __rename;
59
+ private __del;
60
+ private __tree;
61
+ private __current;
62
+ private __resolve;
63
+ private __resolveParent;
64
+ private __parentOf;
65
+ /** 目标节点在树里的路径(从根目录起算,不含 C:)。 */
66
+ private __pathOf;
67
+ /** 相对路径 → 分段(处理 `.` / `..` / 绝对路径)。 */
68
+ private __segments;
69
+ private __baseName;
70
+ private __removeChild;
71
+ private __completePath;
72
+ private __sortedChildren;
73
+ private __fileSize;
74
+ private __fileRow;
75
+ private __nodeDate;
76
+ private __volumeLabel;
77
+ private __formatDate;
78
+ private __formatTime;
79
+ private __lines;
80
+ private __error;
81
+ /** 出厂文件系统:一个像样的小 DOS 盘(命令都能玩起来)。 */
82
+ private __createSeed;
83
+ }
84
+ export default ICEDosModel;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 迷宫寻路轨迹(纯逻辑,不碰 canvas)。
3
+ *
4
+ * 和排序那边同一套路:**先算完整段过程录成一串帧**,回放交给 `ICETracePlayerModel`,
5
+ * 画面由页面用一张 ICETileMap 画(每格一个状态)。
6
+ *
7
+ * 网格是「行优先的一维数组」,每格一个状态(空 / 墙 / 起点 / 终点 / 已访问 / 边界 / 路径)。
8
+ * 四种算法在无权网格上每步代价都是 1,区别只在**怎么挑下一个格子**:
9
+ * - BFS:队列 → 层序扩展,首次到达即最短;
10
+ * - DFS:栈 → 一条道走到黑,找得到但不保证最短;
11
+ * - Dijkstra:按代价取最小(无权网格里等价于 BFS,保留它是为了「代价」这条语义);
12
+ * - A*:代价 + 曼哈顿距离启发 → 同样最短,但访问的格子更少。
13
+ */
14
+ export declare const ICE_MAZE_CELL: {
15
+ readonly EMPTY: 0;
16
+ readonly WALL: 1;
17
+ readonly START: 2;
18
+ readonly GOAL: 3;
19
+ readonly VISITED: 4;
20
+ readonly FRONTIER: 5;
21
+ readonly PATH: 6;
22
+ };
23
+ export interface ICEMazeAlgorithm {
24
+ key: string;
25
+ label: string;
26
+ note: string;
27
+ }
28
+ export interface ICEMazeFrame {
29
+ /** 本帧的网格快照(可视化直接照着画) */
30
+ cells: number[];
31
+ /** 当前正在处理的格子(-1 表示没有) */
32
+ current: number;
33
+ /** 待探索的边界 */
34
+ frontier: number[];
35
+ /** 已经处理过的格子 */
36
+ visited: number[];
37
+ /** 找到路径之后才有(按起点 → 终点排序) */
38
+ path: number[];
39
+ reached: boolean;
40
+ note: string;
41
+ /** 访问过多少格(代价指标) */
42
+ visitedCount: number;
43
+ }
44
+ export interface ICEMazeOptions {
45
+ rows?: number;
46
+ cols?: number;
47
+ random?: () => number;
48
+ }
49
+ export declare const ICE_MAZE_ALGORITHMS: ICEMazeAlgorithm[];
50
+ export declare class ICEMazeModel {
51
+ private rows;
52
+ private cols;
53
+ private random;
54
+ private walls;
55
+ private start;
56
+ private goal;
57
+ constructor(options?: ICEMazeOptions);
58
+ getRows(): number;
59
+ getCols(): number;
60
+ getStart(): [number, number];
61
+ getGoal(): [number, number];
62
+ getAlgorithms(): ICEMazeAlgorithm[];
63
+ /** 可视化用的网格状态(起点/终点/墙)。 */
64
+ getCells(): number[];
65
+ getCell(row: number, col: number): number;
66
+ isWall(row: number, col: number): boolean;
67
+ setWall(row: number, col: number, wall: boolean): boolean;
68
+ toggleWall(row: number, col: number): boolean;
69
+ clearWalls(): void;
70
+ /** 按密度随机撒墙(起点终点永远不盖)。 */
71
+ randomWalls(density?: number): void;
72
+ setStart(row: number, col: number): boolean;
73
+ setGoal(row: number, col: number): boolean;
74
+ solve(algorithm: string): ICEMazeFrame[];
75
+ private __inside;
76
+ private __index;
77
+ }
78
+ export default ICEMazeModel;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 排序轨迹(纯逻辑,不碰 canvas)。
3
+ *
4
+ * 它只做一件事:**把排序算法跑一遍,把过程录成一串帧**。回放交给 `ICETracePlayerModel`,
5
+ * 画柱子交给页面(`examples/algorithm-sandbox.html` 里用一张 ICETileMap 画)。
6
+ *
7
+ * 一帧里带四样东西,正好对应可视化要画的四样:
8
+ * - `values`:当前数组(柱子高度);
9
+ * - `compare`:正在比较的下标(黄色);
10
+ * - `swap`:刚刚交换 / 写入的下标(红色);
11
+ * - `sortedFrom`:从这里往后已经就位(绿色)。
12
+ * 再带上 `comparisons` / `swaps` 两个计数 —— 页面上当「代价」展示,也是算法的客观指标。
13
+ *
14
+ * 为什么「先录轨迹、再回放」而不是「一边算一边画」:
15
+ * 1. 算法本身能被单测(最终升序、每帧只是重排、比较次数上界);
16
+ * 2. 回放可以随便暂停、单步、倒带、变速,算法不用知道「现在第几帧」;
17
+ * 3. 同一段轨迹可以换任意渲染方式(柱子 / 数字 / 音效),互不影响。
18
+ */
19
+ export interface ICESortAlgorithm {
20
+ key: string;
21
+ label: string;
22
+ complexity: string;
23
+ }
24
+ export interface ICESortFrame {
25
+ values: number[];
26
+ compare: number[];
27
+ swap: number[];
28
+ /** 从这里往后的元素已经就位;排完是 0 */
29
+ sortedFrom: number;
30
+ note: string;
31
+ comparisons: number;
32
+ swaps: number;
33
+ }
34
+ export interface ICESortOptions {
35
+ /** 元素个数,默认 24 */
36
+ size?: number;
37
+ /** 最大值(柱子最高多少),默认 32 */
38
+ max?: number;
39
+ /** 随机源,测试可注入 */
40
+ random?: () => number;
41
+ }
42
+ export declare const ICE_SORT_ALGORITHMS: ICESortAlgorithm[];
43
+ export declare class ICESortModel {
44
+ private size;
45
+ private max;
46
+ private random;
47
+ private values;
48
+ constructor(options?: ICESortOptions);
49
+ getSize(): number;
50
+ getMax(): number;
51
+ getArray(): number[];
52
+ getAlgorithms(): ICESortAlgorithm[];
53
+ /** 直接给一组数据(会取整、夹到 1..max)。 */
54
+ setArray(values: number[]): void;
55
+ /** 换一批新数据(1..max 的随机数)。 */
56
+ randomize(): void;
57
+ /** 洗牌:把**当前这批数字**重新打乱(同一批数据换个顺序,方便对比不同算法)。 */
58
+ shuffle(): void;
59
+ /** 跑一遍算法,返回整段轨迹。 */
60
+ run(algorithm: string): ICESortFrame[];
61
+ private __normalize;
62
+ }
63
+ export default ICESortModel;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 「轨迹播放器」:把一串预先算好的帧按时间回放(纯逻辑,不碰 canvas)。
3
+ *
4
+ * 算法可视化(排序、寻路、正则匹配、Diff…)都是同一个套路:**先把整段过程算成一串帧,
5
+ * 再按时间回放**。播放 / 暂停 / 单步 / 调速 / 进度 / 到头停住这套逻辑跟具体算法无关,
6
+ * 所以单独抽出来 —— 算法只负责产帧(`ICESortModel` / `ICEMazeModel`),播放器只管「第几帧、要不要走」。
7
+ *
8
+ * 约定:
9
+ * - `load(frames)` 从头开始(游标 0、暂停);帧是只读的,播放器不复制、不改写;
10
+ * - `tick(dt)` 只在播放态推进,按 `stepsPerSecond` 换算(一帧 = 1000 / speed 毫秒):
11
+ * 攒够几帧就走几帧(页面每帧调一次,所以正常就是每帧一帧;卡顿后不会「欠着时间」);
12
+ * - 走到最后一帧自动停(`isFinished()`),此时再 `play()` 会从头重放;
13
+ * - 速度夹在 1..60 步/秒,非法值忽略(NaN / 0 / 负数都当没调用)。
14
+ */
15
+ export declare type ICETraceListener = (model: ICETracePlayerModel<any>) => void;
16
+ export interface ICETracePlayerOptions {
17
+ /** 初始速度(步/秒),默认 6 */
18
+ speed?: number;
19
+ }
20
+ export declare class ICETracePlayerModel<T> {
21
+ private frames;
22
+ private index;
23
+ private playing;
24
+ private speed;
25
+ private accumulator;
26
+ private listeners;
27
+ constructor(options?: ICETracePlayerOptions);
28
+ getFrameCount(): number;
29
+ getIndex(): number;
30
+ getFrame(): T | null;
31
+ getFrames(): T[];
32
+ isPlaying(): boolean;
33
+ getSpeed(): number;
34
+ getProgress(): number;
35
+ /** 已经走到(或停在)最后一帧。 */
36
+ isFinished(): boolean;
37
+ load(frames: T[]): void;
38
+ play(): void;
39
+ pause(): void;
40
+ togglePlay(): void;
41
+ stepForward(): boolean;
42
+ stepBackward(): boolean;
43
+ seek(index: number): boolean;
44
+ reset(): void;
45
+ setSpeed(stepsPerSecond: number): void;
46
+ /** 播放中按真实时间推进(页面每帧调用)。 */
47
+ tick(delta: number): boolean;
48
+ addChangeListener(listener: ICETraceListener): () => void;
49
+ /** 夹到 1..60;非法值返回 null(调用方据此忽略)。 */
50
+ private __clampSpeed;
51
+ private notify;
52
+ }
53
+ export default ICETracePlayerModel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ice-web-components",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Swing-style Canvas UI components built on ice-render.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -23,6 +23,8 @@
23
23
  "qa:xp": "node scripts/qa-xp.mjs",
24
24
  "qa:arcade": "node scripts/qa-arcade.mjs",
25
25
  "qa:pixel": "node scripts/qa-pixel.mjs",
26
+ "qa:algo": "node scripts/qa-algo.mjs",
27
+ "qa:dos": "node scripts/qa-dos.mjs",
26
28
  "docs:api": "node scripts/gen-docs.mjs",
27
29
  "docs:check": "node scripts/check-docs.mjs",
28
30
  "docs": "npm run docs:api && npm run docs:check",