@buntui/extensions 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/framerate.d.ts +3 -0
- package/dist/framerate.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +2 -0
- package/dist/matrix.d.ts +3 -0
- package/dist/matrix.js +4 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +7 -0
- package/dist/snake.d.ts +3 -0
- package/dist/snake.js +4 -0
- package/dist/utils/color.d.ts +10 -0
- package/dist/utils/color.js +35 -0
- package/dist/videoplayer.d.ts +3 -0
- package/dist/videoplayer.js +4 -0
- package/dist/widgets/framerate/FrameRateWatcher.d.ts +20 -0
- package/dist/widgets/framerate/FrameRateWatcher.js +87 -0
- package/dist/widgets/logger/LoggerWidget.d.ts +33 -0
- package/dist/widgets/logger/LoggerWidget.js +170 -0
- package/dist/widgets/matrix/MatrixWidget.d.ts +13 -0
- package/dist/widgets/matrix/MatrixWidget.js +139 -0
- package/dist/widgets/matrix/charset.d.ts +1 -0
- package/dist/widgets/matrix/charset.js +11 -0
- package/dist/widgets/matrix/defaults.d.ts +3 -0
- package/dist/widgets/matrix/defaults.js +17 -0
- package/dist/widgets/matrix/matrix-column.d.ts +24 -0
- package/dist/widgets/matrix/matrix-column.js +58 -0
- package/dist/widgets/matrix/types.d.ts +38 -0
- package/dist/widgets/matrix/types.js +0 -0
- package/dist/widgets/snake/SnakeWidget.d.ts +14 -0
- package/dist/widgets/snake/SnakeWidget.js +384 -0
- package/dist/widgets/snake/defaults.d.ts +3 -0
- package/dist/widgets/snake/defaults.js +19 -0
- package/dist/widgets/snake/types.d.ts +25 -0
- package/dist/widgets/snake/types.js +0 -0
- package/dist/widgets/videoplayer/VideoPlayerWidget.d.ts +16 -0
- package/dist/widgets/videoplayer/VideoPlayerWidget.js +465 -0
- package/dist/widgets/videoplayer/braille.d.ts +18 -0
- package/dist/widgets/videoplayer/braille.js +69 -0
- package/dist/widgets/videoplayer/defaults.d.ts +3 -0
- package/dist/widgets/videoplayer/defaults.js +17 -0
- package/dist/widgets/videoplayer/types.d.ts +21 -0
- package/dist/widgets/videoplayer/types.js +0 -0
- package/package.json +50 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function randomInt(min, max) {
|
|
2
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
3
|
+
}
|
|
4
|
+
function randomChar(charset) {
|
|
5
|
+
return charset[Math.floor(Math.random() * charset.length)];
|
|
6
|
+
}
|
|
7
|
+
export function createColumn(config) {
|
|
8
|
+
const { maxTrail, speedRange, minTrail, charset, tickIntervalRange } = config;
|
|
9
|
+
const speed = randomInt(speedRange.min, speedRange.max);
|
|
10
|
+
const trailLength = randomInt(minTrail, maxTrail);
|
|
11
|
+
const tickInterval = randomInt(tickIntervalRange.min, tickIntervalRange.max);
|
|
12
|
+
return {
|
|
13
|
+
headY: randomInt(-maxTrail, 0),
|
|
14
|
+
trailLength,
|
|
15
|
+
speed,
|
|
16
|
+
active: true,
|
|
17
|
+
cooldown: 0,
|
|
18
|
+
chars: Array.from({ length: trailLength }, () => randomChar(charset)),
|
|
19
|
+
accumulator: randomInt(0, tickInterval),
|
|
20
|
+
tickInterval,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function tickColumn(col, dt, density, config) {
|
|
24
|
+
const { charset } = config;
|
|
25
|
+
if (!col.active) {
|
|
26
|
+
col.cooldown--;
|
|
27
|
+
if (col.cooldown <= 0) {
|
|
28
|
+
if (Math.random() < density) {
|
|
29
|
+
const restarted = createColumn(config);
|
|
30
|
+
col.headY = restarted.headY;
|
|
31
|
+
col.trailLength = restarted.trailLength;
|
|
32
|
+
col.speed = restarted.speed;
|
|
33
|
+
col.active = true;
|
|
34
|
+
col.chars = restarted.chars;
|
|
35
|
+
col.accumulator = restarted.accumulator;
|
|
36
|
+
col.tickInterval = restarted.tickInterval;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
col.cooldown = randomInt(1, 10);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
col.accumulator += dt;
|
|
45
|
+
if (col.accumulator < col.tickInterval) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
col.accumulator -= col.tickInterval;
|
|
49
|
+
for (let i = 0; i < col.speed; i++) {
|
|
50
|
+
col.chars.unshift(randomChar(charset));
|
|
51
|
+
}
|
|
52
|
+
col.chars.length = col.trailLength;
|
|
53
|
+
col.headY += col.speed;
|
|
54
|
+
if (col.headY - col.trailLength > config.maxY) {
|
|
55
|
+
col.active = false;
|
|
56
|
+
col.cooldown = randomInt(1, 30);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TuiSizeValue } from '@buntui/core';
|
|
2
|
+
export type MatrixColorScheme = {
|
|
3
|
+
/** RGBA color for the bright lead character (default: bright green) */
|
|
4
|
+
leadRgba: number;
|
|
5
|
+
/** RGBA color for the trail characters (default: medium green) */
|
|
6
|
+
trailRgba: number;
|
|
7
|
+
/** RGBA background behind everything (default: opaque black) */
|
|
8
|
+
bgRgba: number;
|
|
9
|
+
};
|
|
10
|
+
export type MatrixSpeedRange = {
|
|
11
|
+
/** Minimum column speed in cells-per-frame */
|
|
12
|
+
min: number;
|
|
13
|
+
/** Maximum column speed in cells-per-frame */
|
|
14
|
+
max: number;
|
|
15
|
+
};
|
|
16
|
+
export type MatrixWidgetOptions = {
|
|
17
|
+
x?: TuiSizeValue;
|
|
18
|
+
y?: TuiSizeValue;
|
|
19
|
+
width?: TuiSizeValue;
|
|
20
|
+
height?: TuiSizeValue;
|
|
21
|
+
/** Color scheme overrides. Partially applied over defaults. */
|
|
22
|
+
colorScheme?: Partial<MatrixColorScheme>;
|
|
23
|
+
/** Speed range for column falls (default: {min:1, max:3}) */
|
|
24
|
+
speedRange?: MatrixSpeedRange;
|
|
25
|
+
/** Minimum trail length (default: 5) */
|
|
26
|
+
minTrailLength?: number;
|
|
27
|
+
/** Maximum trail length (default: 20) */
|
|
28
|
+
maxTrailLength?: number;
|
|
29
|
+
/** Character density: 0.0-1.0 fraction of active columns (default: 0.8) */
|
|
30
|
+
density?: number;
|
|
31
|
+
/** Custom character set. Array of code points. Overrides built-in sets. */
|
|
32
|
+
charset?: number[];
|
|
33
|
+
/** Per-column tick interval range in ms. Lower = faster. (default: {min:60, max:150}) */
|
|
34
|
+
tickIntervalRange?: {
|
|
35
|
+
min: number;
|
|
36
|
+
max: number;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type DrawListBuffer, type TuiWidgetRect, type KeyboardEvent, widgets } from '@buntui/core';
|
|
2
|
+
import type { SnakeWidgetOptions } from './types';
|
|
3
|
+
export declare class SnakeWidget extends widgets.InteractiveWidget {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(options?: SnakeWidgetOptions);
|
|
6
|
+
updateRect(rect: Partial<TuiWidgetRect>): void;
|
|
7
|
+
containsPoint(x: number, y: number): boolean;
|
|
8
|
+
get rect(): TuiWidgetRect;
|
|
9
|
+
handleKey(event: KeyboardEvent): void;
|
|
10
|
+
update(dt: number): void;
|
|
11
|
+
emitDrawCommands(buffer: DrawListBuffer): void;
|
|
12
|
+
}
|
|
13
|
+
export declare function createSnakeWidget(options?: SnakeWidgetOptions): SnakeWidget;
|
|
14
|
+
export default SnakeWidget;
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { rgbToRgba, widgets, } from '@buntui/core';
|
|
2
|
+
import { DEFAULT_SNAKE_COLOR_SCHEME, DEFAULT_SNAKE_OPTIONS } from './defaults';
|
|
3
|
+
const CHAR_HEAD = 0x00_40; // '@'
|
|
4
|
+
const CHAR_BODY = 0x25_88; // '█'
|
|
5
|
+
const CHAR_FOOD = 0x26_05; // '★'
|
|
6
|
+
const MIN_TICK_INTERVAL = 50;
|
|
7
|
+
const OPPOSITE = {
|
|
8
|
+
up: 'down',
|
|
9
|
+
down: 'up',
|
|
10
|
+
left: 'right',
|
|
11
|
+
right: 'left',
|
|
12
|
+
};
|
|
13
|
+
export class SnakeWidget extends widgets.InteractiveWidget {
|
|
14
|
+
#x;
|
|
15
|
+
#y;
|
|
16
|
+
#width;
|
|
17
|
+
#height;
|
|
18
|
+
#colorScheme;
|
|
19
|
+
#tickInterval;
|
|
20
|
+
#initialSpeed;
|
|
21
|
+
#speedIncrement;
|
|
22
|
+
#accumulator = 0;
|
|
23
|
+
#state = 'idle';
|
|
24
|
+
#direction = 'right';
|
|
25
|
+
#nextDirection = 'right';
|
|
26
|
+
#snake = [];
|
|
27
|
+
#food = { x: 0, y: 0 };
|
|
28
|
+
#score = 0;
|
|
29
|
+
#highScore = 0;
|
|
30
|
+
#initialized = false;
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
super();
|
|
33
|
+
const resolved = { ...DEFAULT_SNAKE_OPTIONS, ...options };
|
|
34
|
+
const rect = this.initRect(resolved.x, resolved.y, resolved.width, resolved.height);
|
|
35
|
+
this.#x = rect.x;
|
|
36
|
+
this.#y = rect.y;
|
|
37
|
+
this.#width = rect.width;
|
|
38
|
+
this.#height = rect.height;
|
|
39
|
+
const schemeOverride = resolved.colorScheme ?? {};
|
|
40
|
+
this.#colorScheme = {
|
|
41
|
+
headRgba: schemeOverride.headRgba ?? DEFAULT_SNAKE_COLOR_SCHEME.headRgba,
|
|
42
|
+
bodyRgba: schemeOverride.bodyRgba ?? DEFAULT_SNAKE_COLOR_SCHEME.bodyRgba,
|
|
43
|
+
foodRgba: schemeOverride.foodRgba ?? DEFAULT_SNAKE_COLOR_SCHEME.foodRgba,
|
|
44
|
+
borderRgba: schemeOverride.borderRgba ?? DEFAULT_SNAKE_COLOR_SCHEME.borderRgba,
|
|
45
|
+
bgRgba: schemeOverride.bgRgba ?? DEFAULT_SNAKE_COLOR_SCHEME.bgRgba,
|
|
46
|
+
textRgba: schemeOverride.textRgba ?? DEFAULT_SNAKE_COLOR_SCHEME.textRgba,
|
|
47
|
+
scoreTextRgba: schemeOverride.scoreTextRgba ?? DEFAULT_SNAKE_COLOR_SCHEME.scoreTextRgba,
|
|
48
|
+
};
|
|
49
|
+
this.#tickInterval = resolved.tickInterval ?? 150;
|
|
50
|
+
this.#initialSpeed = this.#tickInterval;
|
|
51
|
+
this.#speedIncrement = resolved.speedIncrement ?? 5;
|
|
52
|
+
}
|
|
53
|
+
updateRect(rect) {
|
|
54
|
+
if (rect.x !== undefined) {
|
|
55
|
+
this.#x = rect.x;
|
|
56
|
+
}
|
|
57
|
+
if (rect.y !== undefined) {
|
|
58
|
+
this.#y = rect.y;
|
|
59
|
+
}
|
|
60
|
+
if (rect.width !== undefined) {
|
|
61
|
+
this.#width = rect.width;
|
|
62
|
+
}
|
|
63
|
+
if (rect.height !== undefined) {
|
|
64
|
+
this.#height = rect.height;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
containsPoint(x, y) {
|
|
68
|
+
return x >= this.#x
|
|
69
|
+
&& x < this.#x + this.#width
|
|
70
|
+
&& y >= this.#y
|
|
71
|
+
&& y < this.#y + this.#height;
|
|
72
|
+
}
|
|
73
|
+
get rect() {
|
|
74
|
+
return {
|
|
75
|
+
x: this.#x, y: this.#y, width: this.#width, height: this.#height,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
handleKey(event) {
|
|
79
|
+
const { key } = event;
|
|
80
|
+
if (!key) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (this.#state === 'idle') {
|
|
84
|
+
if (key === ' ') {
|
|
85
|
+
this.#startGame();
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (this.#state === 'playing') {
|
|
90
|
+
if (key === 'Escape') {
|
|
91
|
+
this.#resetToIdle();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.#handleDirection(key);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Gameover
|
|
98
|
+
if (key === ' ') {
|
|
99
|
+
this.#startGame();
|
|
100
|
+
}
|
|
101
|
+
else if (key === 'Escape') {
|
|
102
|
+
this.#resetToIdle();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
update(dt) {
|
|
106
|
+
if (this.#state !== 'playing') {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.#accumulator += dt;
|
|
110
|
+
if (this.#accumulator >= this.#tickInterval) {
|
|
111
|
+
this.#accumulator -= this.#tickInterval;
|
|
112
|
+
this.#direction = this.#nextDirection;
|
|
113
|
+
this.#tick(this.#width - 2, this.#height - 2);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
emitDrawCommands(buffer) {
|
|
117
|
+
const w = this.#width;
|
|
118
|
+
const h = this.#height;
|
|
119
|
+
if (w <= 0 || h <= 0) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const gridW = w - 2;
|
|
123
|
+
const gridH = h - 2;
|
|
124
|
+
if (gridW < 2 || gridH < 2) {
|
|
125
|
+
buffer.pushClip(this.#x, this.#y, w, h);
|
|
126
|
+
buffer.drawText({
|
|
127
|
+
x: this.#x,
|
|
128
|
+
y: this.#y,
|
|
129
|
+
text: 'Too small',
|
|
130
|
+
fgRgba: this.#colorScheme.textRgba,
|
|
131
|
+
bgRgba: this.#colorScheme.bgRgba,
|
|
132
|
+
});
|
|
133
|
+
buffer.popClip();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.#ensureInitialized(gridW, gridH);
|
|
137
|
+
const absX = this.#x;
|
|
138
|
+
const absY = this.#y;
|
|
139
|
+
buffer.pushClip(absX, absY, w, h);
|
|
140
|
+
// Background
|
|
141
|
+
buffer.drawRect({
|
|
142
|
+
x: absX, y: absY, width: w, height: h, bgRgba: this.#colorScheme.bgRgba,
|
|
143
|
+
});
|
|
144
|
+
// Border
|
|
145
|
+
buffer.drawBorder({
|
|
146
|
+
x: absX,
|
|
147
|
+
y: absY,
|
|
148
|
+
width: w,
|
|
149
|
+
height: h,
|
|
150
|
+
colorRgba: this.#colorScheme.borderRgba,
|
|
151
|
+
style: 1,
|
|
152
|
+
sides: 0b1111,
|
|
153
|
+
});
|
|
154
|
+
// Snake body (tail to head so head draws on top)
|
|
155
|
+
const { bgRgba } = this.#colorScheme;
|
|
156
|
+
for (let i = this.#snake.length - 1; i >= 0; i--) {
|
|
157
|
+
const seg = this.#snake[i];
|
|
158
|
+
const isHead = i === 0;
|
|
159
|
+
buffer.drawChar({
|
|
160
|
+
x: absX + 1 + seg.x,
|
|
161
|
+
y: absY + 1 + seg.y,
|
|
162
|
+
char: isHead ? CHAR_HEAD : CHAR_BODY,
|
|
163
|
+
fgRgba: isHead ? this.#colorScheme.headRgba : this.#colorScheme.bodyRgba,
|
|
164
|
+
bgRgba,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// Food
|
|
168
|
+
if (this.#state !== 'idle') {
|
|
169
|
+
buffer.drawChar({
|
|
170
|
+
x: absX + 1 + this.#food.x,
|
|
171
|
+
y: absY + 1 + this.#food.y,
|
|
172
|
+
char: CHAR_FOOD,
|
|
173
|
+
fgRgba: this.#colorScheme.foodRgba,
|
|
174
|
+
bgRgba,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// Score (on top border row)
|
|
178
|
+
const scoreText = ` Score: ${this.#score} `;
|
|
179
|
+
buffer.drawText({
|
|
180
|
+
x: absX + 1,
|
|
181
|
+
y: absY,
|
|
182
|
+
text: scoreText,
|
|
183
|
+
fgRgba: this.#colorScheme.scoreTextRgba,
|
|
184
|
+
bgRgba: this.#colorScheme.borderRgba,
|
|
185
|
+
});
|
|
186
|
+
// High score
|
|
187
|
+
if (this.#highScore > 0) {
|
|
188
|
+
const hiText = `Hi: ${this.#highScore} `;
|
|
189
|
+
const hiX = absX + w - 1 - hiText.length;
|
|
190
|
+
if (hiX > absX + scoreText.length) {
|
|
191
|
+
buffer.drawText({
|
|
192
|
+
x: hiX,
|
|
193
|
+
y: absY,
|
|
194
|
+
text: hiText,
|
|
195
|
+
fgRgba: this.#colorScheme.textRgba,
|
|
196
|
+
bgRgba: this.#colorScheme.borderRgba,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// State messages
|
|
201
|
+
const midY = absY + 1 + Math.floor(gridH / 2);
|
|
202
|
+
if (this.#state === 'idle') {
|
|
203
|
+
const message = 'Press SPACE to start';
|
|
204
|
+
buffer.drawText({
|
|
205
|
+
x: absX + 1 + Math.floor((gridW - message.length) / 2),
|
|
206
|
+
y: midY,
|
|
207
|
+
text: message,
|
|
208
|
+
fgRgba: this.#colorScheme.textRgba,
|
|
209
|
+
bgRgba,
|
|
210
|
+
});
|
|
211
|
+
const hint = 'Arrow keys to move';
|
|
212
|
+
buffer.drawText({
|
|
213
|
+
x: absX + 1 + Math.floor((gridW - hint.length) / 2),
|
|
214
|
+
y: midY + 1,
|
|
215
|
+
text: hint,
|
|
216
|
+
fgRgba: rgbToRgba(0x88, 0x88, 0x88),
|
|
217
|
+
bgRgba,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
else if (this.#state === 'gameover') {
|
|
221
|
+
const message = 'GAME OVER';
|
|
222
|
+
buffer.drawText({
|
|
223
|
+
x: absX + 1 + Math.floor((gridW - message.length) / 2),
|
|
224
|
+
y: midY - 1,
|
|
225
|
+
text: message,
|
|
226
|
+
fgRgba: rgbToRgba(0xFF, 0x00, 0x00),
|
|
227
|
+
bgRgba,
|
|
228
|
+
});
|
|
229
|
+
const scoreMessage = `Final Score: ${this.#score}`;
|
|
230
|
+
buffer.drawText({
|
|
231
|
+
x: absX + 1 + Math.floor((gridW - scoreMessage.length) / 2),
|
|
232
|
+
y: midY,
|
|
233
|
+
text: scoreMessage,
|
|
234
|
+
fgRgba: this.#colorScheme.scoreTextRgba,
|
|
235
|
+
bgRgba,
|
|
236
|
+
});
|
|
237
|
+
const restart = 'SPACE to restart / ESC to menu';
|
|
238
|
+
buffer.drawText({
|
|
239
|
+
x: absX + 1 + Math.floor((gridW - restart.length) / 2),
|
|
240
|
+
y: midY + 1,
|
|
241
|
+
text: restart,
|
|
242
|
+
fgRgba: rgbToRgba(0x88, 0x88, 0x88),
|
|
243
|
+
bgRgba,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
buffer.popClip();
|
|
247
|
+
}
|
|
248
|
+
#handleDirection(key) {
|
|
249
|
+
let dir;
|
|
250
|
+
switch (key) {
|
|
251
|
+
case 'ArrowUp': {
|
|
252
|
+
dir = 'up';
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case 'ArrowDown': {
|
|
256
|
+
dir = 'down';
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case 'ArrowLeft': {
|
|
260
|
+
dir = 'left';
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case 'ArrowRight': {
|
|
264
|
+
dir = 'right';
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
default: {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (dir !== OPPOSITE[this.#direction]) {
|
|
272
|
+
this.#nextDirection = dir;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
#ensureInitialized(gridW, gridH) {
|
|
276
|
+
if (this.#initialized) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.#initialized = true;
|
|
280
|
+
const startX = Math.floor(gridW / 2);
|
|
281
|
+
const startY = Math.floor(gridH / 2);
|
|
282
|
+
this.#snake = [
|
|
283
|
+
{ x: startX, y: startY },
|
|
284
|
+
{ x: startX - 1, y: startY },
|
|
285
|
+
{ x: startX - 2, y: startY },
|
|
286
|
+
];
|
|
287
|
+
this.#spawnFood(gridW, gridH);
|
|
288
|
+
}
|
|
289
|
+
#startGame() {
|
|
290
|
+
const gridW = this.#width - 2;
|
|
291
|
+
const gridH = this.#height - 2;
|
|
292
|
+
const startX = Math.floor(gridW / 2);
|
|
293
|
+
const startY = Math.floor(gridH / 2);
|
|
294
|
+
this.#snake = [
|
|
295
|
+
{ x: startX, y: startY },
|
|
296
|
+
{ x: startX - 1, y: startY },
|
|
297
|
+
{ x: startX - 2, y: startY },
|
|
298
|
+
];
|
|
299
|
+
this.#direction = 'right';
|
|
300
|
+
this.#nextDirection = 'right';
|
|
301
|
+
this.#score = 0;
|
|
302
|
+
this.#tickInterval = this.#initialSpeed;
|
|
303
|
+
this.#state = 'playing';
|
|
304
|
+
this.#accumulator = 0;
|
|
305
|
+
this.#spawnFood(gridW, gridH);
|
|
306
|
+
}
|
|
307
|
+
#resetToIdle() {
|
|
308
|
+
this.#state = 'idle';
|
|
309
|
+
this.#score = 0;
|
|
310
|
+
this.#snake = [];
|
|
311
|
+
this.#initialized = false;
|
|
312
|
+
}
|
|
313
|
+
#tick(gridW, gridH) {
|
|
314
|
+
const head = this.#snake[0];
|
|
315
|
+
let newX = head.x;
|
|
316
|
+
let newY = head.y;
|
|
317
|
+
switch (this.#direction) {
|
|
318
|
+
case 'up': {
|
|
319
|
+
newY--;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
case 'down': {
|
|
323
|
+
newY++;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case 'left': {
|
|
327
|
+
newX--;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case 'right': {
|
|
331
|
+
newX++;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Wall collision
|
|
336
|
+
if (newX < 0 || newX >= gridW || newY < 0 || newY >= gridH) {
|
|
337
|
+
this.#endGame();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// Self collision
|
|
341
|
+
for (const seg of this.#snake) {
|
|
342
|
+
if (seg.x === newX && seg.y === newY) {
|
|
343
|
+
this.#endGame();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Move
|
|
348
|
+
this.#snake.unshift({ x: newX, y: newY });
|
|
349
|
+
// Food check
|
|
350
|
+
if (newX === this.#food.x && newY === this.#food.y) {
|
|
351
|
+
this.#score++;
|
|
352
|
+
this.#tickInterval = Math.max(MIN_TICK_INTERVAL, this.#tickInterval - this.#speedIncrement);
|
|
353
|
+
this.#spawnFood(gridW, gridH);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
this.#snake.pop();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
#endGame() {
|
|
360
|
+
this.#state = 'gameover';
|
|
361
|
+
if (this.#score > this.#highScore) {
|
|
362
|
+
this.#highScore = this.#score;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
#spawnFood(gridW, gridH) {
|
|
366
|
+
const snakeSet = new Set(this.#snake.map(p => `${p.x},${p.y}`));
|
|
367
|
+
let attempts = 0;
|
|
368
|
+
while (attempts < 1000) {
|
|
369
|
+
const fx = Math.floor(Math.random() * gridW);
|
|
370
|
+
const fy = Math.floor(Math.random() * gridH);
|
|
371
|
+
if (!snakeSet.has(`${fx},${fy}`)) {
|
|
372
|
+
this.#food = { x: fx, y: fy };
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
attempts++;
|
|
376
|
+
}
|
|
377
|
+
// Fallback: just pick any position
|
|
378
|
+
this.#food = { x: 0, y: 0 };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
export function createSnakeWidget(options) {
|
|
382
|
+
return new SnakeWidget(options);
|
|
383
|
+
}
|
|
384
|
+
export default SnakeWidget;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { rgbToRgba } from '@buntui/core';
|
|
2
|
+
export const DEFAULT_SNAKE_COLOR_SCHEME = {
|
|
3
|
+
headRgba: rgbToRgba(0x00, 0xFF, 0x00),
|
|
4
|
+
bodyRgba: rgbToRgba(0x00, 0xAA, 0x00),
|
|
5
|
+
foodRgba: rgbToRgba(0xFF, 0x55, 0x00),
|
|
6
|
+
borderRgba: rgbToRgba(0x55, 0x55, 0x55),
|
|
7
|
+
bgRgba: rgbToRgba(0x00, 0x00, 0x00),
|
|
8
|
+
textRgba: rgbToRgba(0xFF, 0xFF, 0xFF),
|
|
9
|
+
scoreTextRgba: rgbToRgba(0xFF, 0xFF, 0x00),
|
|
10
|
+
};
|
|
11
|
+
export const DEFAULT_SNAKE_OPTIONS = {
|
|
12
|
+
x: 0,
|
|
13
|
+
y: 0,
|
|
14
|
+
width: '100%',
|
|
15
|
+
height: '100%',
|
|
16
|
+
colorScheme: DEFAULT_SNAKE_COLOR_SCHEME,
|
|
17
|
+
tickInterval: 150,
|
|
18
|
+
speedIncrement: 5,
|
|
19
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TuiSizeValue } from '@buntui/core';
|
|
2
|
+
export type SnakeDirection = 'up' | 'down' | 'left' | 'right';
|
|
3
|
+
export type SnakeGameState = 'idle' | 'playing' | 'gameover';
|
|
4
|
+
export type SnakePoint = {
|
|
5
|
+
readonly x: number;
|
|
6
|
+
readonly y: number;
|
|
7
|
+
};
|
|
8
|
+
export type SnakeColorScheme = {
|
|
9
|
+
headRgba: number;
|
|
10
|
+
bodyRgba: number;
|
|
11
|
+
foodRgba: number;
|
|
12
|
+
borderRgba: number;
|
|
13
|
+
bgRgba: number;
|
|
14
|
+
textRgba: number;
|
|
15
|
+
scoreTextRgba: number;
|
|
16
|
+
};
|
|
17
|
+
export type SnakeWidgetOptions = {
|
|
18
|
+
x?: TuiSizeValue;
|
|
19
|
+
y?: TuiSizeValue;
|
|
20
|
+
width?: TuiSizeValue;
|
|
21
|
+
height?: TuiSizeValue;
|
|
22
|
+
colorScheme?: Partial<SnakeColorScheme>;
|
|
23
|
+
tickInterval?: number;
|
|
24
|
+
speedIncrement?: number;
|
|
25
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type DrawListBuffer, type KeyboardEvent, type TuiWidgetRect, widgets } from '@buntui/core';
|
|
2
|
+
import type { VideoPlayerWidgetOptions } from './types';
|
|
3
|
+
export declare class VideoPlayerWidget extends widgets.InteractiveWidget {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(options?: VideoPlayerWidgetOptions);
|
|
6
|
+
mounted(): void;
|
|
7
|
+
unmounted(): void;
|
|
8
|
+
updateRect(rect: Partial<TuiWidgetRect>): void;
|
|
9
|
+
containsPoint(x: number, y: number): boolean;
|
|
10
|
+
get rect(): TuiWidgetRect;
|
|
11
|
+
handleKey(event: KeyboardEvent): void;
|
|
12
|
+
update(dt: number): void;
|
|
13
|
+
emitDrawCommands(buffer: DrawListBuffer): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function createVideoPlayerWidget(options?: VideoPlayerWidgetOptions): VideoPlayerWidget;
|
|
16
|
+
export default VideoPlayerWidget;
|