atari 0.0.1-alpha.1 → 1.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/README.md +59 -0
- package/bin/atari.mjs +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +812 -0
- package/package.json +49 -5
- package/index.js +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# atari
|
|
2
|
+
|
|
3
|
+
Retro Atari games in your terminal. Play classic arcade games like Snake, Pong, and Breakout directly from your command line.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g atari
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Launch interactive menu
|
|
15
|
+
atari
|
|
16
|
+
|
|
17
|
+
# Play a specific game directly
|
|
18
|
+
atari snake
|
|
19
|
+
atari pong
|
|
20
|
+
atari breakout
|
|
21
|
+
|
|
22
|
+
# List available games
|
|
23
|
+
atari --list
|
|
24
|
+
|
|
25
|
+
# Show help
|
|
26
|
+
atari --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Games
|
|
30
|
+
|
|
31
|
+
- **Snake** - Classic snake game, eat food and grow longer
|
|
32
|
+
- **Pong** - The original Atari hit, bounce the ball
|
|
33
|
+
- **Breakout** - Break the bricks with your paddle
|
|
34
|
+
|
|
35
|
+
## Controls
|
|
36
|
+
|
|
37
|
+
| Key | Action |
|
|
38
|
+
|-----|--------|
|
|
39
|
+
| Arrow keys / WASD | Move |
|
|
40
|
+
| Space | Action (shoot, pause) |
|
|
41
|
+
| Q | Quit game |
|
|
42
|
+
| R | Restart |
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Install dependencies
|
|
48
|
+
bun install
|
|
49
|
+
|
|
50
|
+
# Run in development mode
|
|
51
|
+
bun run dev
|
|
52
|
+
|
|
53
|
+
# Build for production
|
|
54
|
+
bun run build
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
package/bin/atari.mjs
ADDED
package/dist/index.d.mts
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
import { defineCommand, runMain } from 'citty';
|
|
2
|
+
import figlet from 'figlet';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { intro, select, isCancel, outro, spinner } from '@clack/prompts';
|
|
5
|
+
import { stdout, stdin } from 'process';
|
|
6
|
+
|
|
7
|
+
async function showBanner() {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
figlet.text(
|
|
10
|
+
"ATARI",
|
|
11
|
+
{
|
|
12
|
+
font: "ANSI Shadow",
|
|
13
|
+
horizontalLayout: "default",
|
|
14
|
+
verticalLayout: "default"
|
|
15
|
+
},
|
|
16
|
+
(err, data) => {
|
|
17
|
+
if (err) {
|
|
18
|
+
console.log(pc.bold(pc.red("\n ATARI ARCADE\n")));
|
|
19
|
+
resolve();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(pc.red(data));
|
|
23
|
+
console.log(pc.dim(" Retro games in your terminal\n"));
|
|
24
|
+
resolve();
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ANSI = {
|
|
31
|
+
// Cursor
|
|
32
|
+
hideCursor: "\x1B[?25l",
|
|
33
|
+
showCursor: "\x1B[?25h",
|
|
34
|
+
moveTo: (x, y) => `\x1B[${y};${x}H`,
|
|
35
|
+
moveToStart: "\x1B[H",
|
|
36
|
+
// Screen
|
|
37
|
+
clearScreen: "\x1B[2J",
|
|
38
|
+
clearLine: "\x1B[2K",
|
|
39
|
+
// Colors (foreground)
|
|
40
|
+
reset: "\x1B[0m",
|
|
41
|
+
bold: "\x1B[1m",
|
|
42
|
+
dim: "\x1B[2m",
|
|
43
|
+
black: "\x1B[30m",
|
|
44
|
+
red: "\x1B[31m",
|
|
45
|
+
green: "\x1B[32m",
|
|
46
|
+
yellow: "\x1B[33m",
|
|
47
|
+
blue: "\x1B[34m",
|
|
48
|
+
magenta: "\x1B[35m",
|
|
49
|
+
cyan: "\x1B[36m",
|
|
50
|
+
white: "\x1B[37m",
|
|
51
|
+
// Background
|
|
52
|
+
bgBlack: "\x1B[40m",
|
|
53
|
+
bgRed: "\x1B[41m",
|
|
54
|
+
bgGreen: "\x1B[42m",
|
|
55
|
+
bgYellow: "\x1B[43m",
|
|
56
|
+
bgBlue: "\x1B[44m",
|
|
57
|
+
bgMagenta: "\x1B[45m",
|
|
58
|
+
bgCyan: "\x1B[46m",
|
|
59
|
+
bgWhite: "\x1B[47m"
|
|
60
|
+
};
|
|
61
|
+
function getScreenSize() {
|
|
62
|
+
return {
|
|
63
|
+
width: stdout.columns || 80,
|
|
64
|
+
height: stdout.rows || 24
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function clearScreen() {
|
|
68
|
+
stdout.write(ANSI.clearScreen + ANSI.moveToStart);
|
|
69
|
+
}
|
|
70
|
+
function hideCursor() {
|
|
71
|
+
stdout.write(ANSI.hideCursor);
|
|
72
|
+
}
|
|
73
|
+
function showCursor() {
|
|
74
|
+
stdout.write(ANSI.showCursor);
|
|
75
|
+
}
|
|
76
|
+
function writeAt(x, y, text) {
|
|
77
|
+
stdout.write(ANSI.moveTo(x, y) + text);
|
|
78
|
+
}
|
|
79
|
+
function drawBox(x, y, width, height, color = ANSI.white) {
|
|
80
|
+
const horizontal = "\u2500".repeat(width - 2);
|
|
81
|
+
const top = `\u250C${horizontal}\u2510`;
|
|
82
|
+
const bottom = `\u2514${horizontal}\u2518`;
|
|
83
|
+
writeAt(x, y, color + top + ANSI.reset);
|
|
84
|
+
for (let i = 1; i < height - 1; i++) {
|
|
85
|
+
writeAt(x, y + i, color + "\u2502" + ANSI.reset);
|
|
86
|
+
writeAt(x + width - 1, y + i, color + "\u2502" + ANSI.reset);
|
|
87
|
+
}
|
|
88
|
+
writeAt(x, y + height - 1, color + bottom + ANSI.reset);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const KEY_CODES = {
|
|
92
|
+
// Arrow keys
|
|
93
|
+
"\x1B[A": "up",
|
|
94
|
+
"\x1B[B": "down",
|
|
95
|
+
"\x1B[C": "right",
|
|
96
|
+
"\x1B[D": "left",
|
|
97
|
+
// WASD
|
|
98
|
+
w: "up",
|
|
99
|
+
W: "up",
|
|
100
|
+
a: "left",
|
|
101
|
+
A: "left",
|
|
102
|
+
s: "down",
|
|
103
|
+
S: "down",
|
|
104
|
+
d: "right",
|
|
105
|
+
D: "right",
|
|
106
|
+
// Special keys
|
|
107
|
+
" ": "space",
|
|
108
|
+
"\r": "enter",
|
|
109
|
+
"\x1B": "escape",
|
|
110
|
+
q: "q",
|
|
111
|
+
Q: "q",
|
|
112
|
+
r: "r",
|
|
113
|
+
R: "r",
|
|
114
|
+
p: "p",
|
|
115
|
+
P: "p"
|
|
116
|
+
};
|
|
117
|
+
let isListening = false;
|
|
118
|
+
let currentHandler = null;
|
|
119
|
+
function parseKey(data) {
|
|
120
|
+
const str = data.toString();
|
|
121
|
+
return KEY_CODES[str] || "unknown";
|
|
122
|
+
}
|
|
123
|
+
function startKeyboardListener(handler) {
|
|
124
|
+
if (isListening) {
|
|
125
|
+
stopKeyboardListener();
|
|
126
|
+
}
|
|
127
|
+
currentHandler = handler;
|
|
128
|
+
isListening = true;
|
|
129
|
+
stdin.setRawMode(true);
|
|
130
|
+
stdin.resume();
|
|
131
|
+
stdin.setEncoding("utf8");
|
|
132
|
+
stdin.on("data", onKeyPress);
|
|
133
|
+
}
|
|
134
|
+
function stopKeyboardListener() {
|
|
135
|
+
if (!isListening) return;
|
|
136
|
+
stdin.off("data", onKeyPress);
|
|
137
|
+
stdin.setRawMode(false);
|
|
138
|
+
stdin.pause();
|
|
139
|
+
currentHandler = null;
|
|
140
|
+
isListening = false;
|
|
141
|
+
}
|
|
142
|
+
function onKeyPress(data) {
|
|
143
|
+
if (!currentHandler) return;
|
|
144
|
+
const key = parseKey(data);
|
|
145
|
+
if (data.toString() === "") {
|
|
146
|
+
stopKeyboardListener();
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
currentHandler(key, data);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function beep() {
|
|
153
|
+
stdout.write("\x07");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const MAX_WIDTH$2 = 40;
|
|
157
|
+
const MAX_HEIGHT$2 = 18;
|
|
158
|
+
const INITIAL_SPEED = 120;
|
|
159
|
+
const VERTICAL_MULTIPLIER = 1.8;
|
|
160
|
+
const SPEED_INCREMENT = 5;
|
|
161
|
+
const snake = {
|
|
162
|
+
name: "Snake",
|
|
163
|
+
description: "Classic snake game - eat food and grow longer",
|
|
164
|
+
async start() {
|
|
165
|
+
const screen = getScreenSize();
|
|
166
|
+
const gameWidth = Math.min(MAX_WIDTH$2, screen.width - 4);
|
|
167
|
+
const gameHeight = Math.min(MAX_HEIGHT$2, screen.height - 8);
|
|
168
|
+
const offsetX = Math.max(1, Math.floor((screen.width - gameWidth - 2) / 2));
|
|
169
|
+
const offsetY = Math.max(2, Math.floor((screen.height - gameHeight - 4) / 2));
|
|
170
|
+
const state = {
|
|
171
|
+
snake: [{ x: Math.floor(gameWidth / 2), y: Math.floor(gameHeight / 2) }],
|
|
172
|
+
food: { x: 0, y: 0 },
|
|
173
|
+
direction: "right",
|
|
174
|
+
nextDirection: "right",
|
|
175
|
+
score: 0,
|
|
176
|
+
gameOver: false,
|
|
177
|
+
isPaused: false,
|
|
178
|
+
speed: INITIAL_SPEED,
|
|
179
|
+
gameWidth,
|
|
180
|
+
gameHeight
|
|
181
|
+
};
|
|
182
|
+
placeFood();
|
|
183
|
+
function placeFood() {
|
|
184
|
+
let newFood;
|
|
185
|
+
do {
|
|
186
|
+
newFood = {
|
|
187
|
+
x: Math.floor(Math.random() * state.gameWidth),
|
|
188
|
+
y: Math.floor(Math.random() * state.gameHeight)
|
|
189
|
+
};
|
|
190
|
+
} while (state.snake.some((s) => s.x === newFood.x && s.y === newFood.y));
|
|
191
|
+
state.food = newFood;
|
|
192
|
+
}
|
|
193
|
+
hideCursor();
|
|
194
|
+
clearScreen();
|
|
195
|
+
drawBox(offsetX, offsetY, gameWidth + 2, gameHeight + 2, ANSI.cyan);
|
|
196
|
+
writeAt(offsetX + Math.floor(gameWidth / 2) - 2, offsetY - 1, ANSI.bold + ANSI.yellow + "SNAKE" + ANSI.reset);
|
|
197
|
+
let intervalId;
|
|
198
|
+
function getSpeed() {
|
|
199
|
+
const isVertical = state.direction === "up" || state.direction === "down";
|
|
200
|
+
return isVertical ? state.speed * VERTICAL_MULTIPLIER : state.speed;
|
|
201
|
+
}
|
|
202
|
+
function gameLoop() {
|
|
203
|
+
if (state.gameOver) {
|
|
204
|
+
renderGameOver();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!state.isPaused) {
|
|
208
|
+
update();
|
|
209
|
+
render();
|
|
210
|
+
} else {
|
|
211
|
+
renderPaused();
|
|
212
|
+
}
|
|
213
|
+
clearInterval(intervalId);
|
|
214
|
+
intervalId = setInterval(gameLoop, getSpeed());
|
|
215
|
+
}
|
|
216
|
+
function update() {
|
|
217
|
+
state.direction = state.nextDirection;
|
|
218
|
+
const head = { ...state.snake[0] };
|
|
219
|
+
switch (state.direction) {
|
|
220
|
+
case "up":
|
|
221
|
+
head.y--;
|
|
222
|
+
break;
|
|
223
|
+
case "down":
|
|
224
|
+
head.y++;
|
|
225
|
+
break;
|
|
226
|
+
case "left":
|
|
227
|
+
head.x--;
|
|
228
|
+
break;
|
|
229
|
+
case "right":
|
|
230
|
+
head.x++;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
if (head.x < 0 || head.x >= gameWidth || head.y < 0 || head.y >= gameHeight) {
|
|
234
|
+
state.gameOver = true;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (state.snake.some((s) => s.x === head.x && s.y === head.y)) {
|
|
238
|
+
state.gameOver = true;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
state.snake.unshift(head);
|
|
242
|
+
if (head.x === state.food.x && head.y === state.food.y) {
|
|
243
|
+
state.score += 10;
|
|
244
|
+
state.speed = Math.max(40, state.speed - SPEED_INCREMENT);
|
|
245
|
+
beep();
|
|
246
|
+
placeFood();
|
|
247
|
+
} else {
|
|
248
|
+
state.snake.pop();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function render() {
|
|
252
|
+
for (let y = 1; y <= gameHeight; y++) {
|
|
253
|
+
writeAt(offsetX + 1, offsetY + y, " ".repeat(gameWidth));
|
|
254
|
+
}
|
|
255
|
+
writeAt(offsetX + state.food.x + 1, offsetY + state.food.y + 1, ANSI.red + "o" + ANSI.reset);
|
|
256
|
+
state.snake.forEach((segment, i) => {
|
|
257
|
+
const color = i === 0 ? ANSI.green : ANSI.cyan;
|
|
258
|
+
writeAt(offsetX + segment.x + 1, offsetY + segment.y + 1, color + "#" + ANSI.reset);
|
|
259
|
+
});
|
|
260
|
+
writeAt(offsetX, offsetY + gameHeight + 3, `${ANSI.yellow}Score: ${state.score}${ANSI.reset} ${ANSI.dim}Q:Quit P:Pause${ANSI.reset}`);
|
|
261
|
+
}
|
|
262
|
+
function renderPaused() {
|
|
263
|
+
writeAt(offsetX + Math.floor(gameWidth / 2) - 3, offsetY + Math.floor(gameHeight / 2), ANSI.yellow + "PAUSED" + ANSI.reset);
|
|
264
|
+
}
|
|
265
|
+
function renderGameOver() {
|
|
266
|
+
const cx = offsetX + Math.floor(gameWidth / 2);
|
|
267
|
+
const cy = offsetY + Math.floor(gameHeight / 2);
|
|
268
|
+
writeAt(cx - 5, cy - 1, ANSI.red + ANSI.bold + "GAME OVER!" + ANSI.reset);
|
|
269
|
+
writeAt(cx - 5, cy + 1, `Score: ${ANSI.yellow}${state.score}${ANSI.reset}`);
|
|
270
|
+
writeAt(cx - 8, cy + 3, ANSI.dim + "R:Restart Q:Quit" + ANSI.reset);
|
|
271
|
+
}
|
|
272
|
+
function cleanup() {
|
|
273
|
+
clearInterval(intervalId);
|
|
274
|
+
stopKeyboardListener();
|
|
275
|
+
showCursor();
|
|
276
|
+
clearScreen();
|
|
277
|
+
}
|
|
278
|
+
function resetGame() {
|
|
279
|
+
state.snake = [{ x: Math.floor(gameWidth / 2), y: Math.floor(gameHeight / 2) }];
|
|
280
|
+
state.direction = "right";
|
|
281
|
+
state.nextDirection = "right";
|
|
282
|
+
state.score = 0;
|
|
283
|
+
state.gameOver = false;
|
|
284
|
+
state.isPaused = false;
|
|
285
|
+
state.speed = INITIAL_SPEED;
|
|
286
|
+
placeFood();
|
|
287
|
+
clearScreen();
|
|
288
|
+
drawBox(offsetX, offsetY, gameWidth + 2, gameHeight + 2, ANSI.cyan);
|
|
289
|
+
writeAt(offsetX + Math.floor(gameWidth / 2) - 2, offsetY - 1, ANSI.bold + ANSI.yellow + "SNAKE" + ANSI.reset);
|
|
290
|
+
}
|
|
291
|
+
intervalId = setInterval(gameLoop, state.speed);
|
|
292
|
+
return new Promise((resolve) => {
|
|
293
|
+
startKeyboardListener((key) => {
|
|
294
|
+
if (key === "q") {
|
|
295
|
+
cleanup();
|
|
296
|
+
resolve();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (key === "r" && state.gameOver) {
|
|
300
|
+
resetGame();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (key === "p" || key === "space") {
|
|
304
|
+
state.isPaused = !state.isPaused;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (key === "up" && state.direction !== "down") state.nextDirection = "up";
|
|
308
|
+
else if (key === "down" && state.direction !== "up") state.nextDirection = "down";
|
|
309
|
+
else if (key === "left" && state.direction !== "right") state.nextDirection = "left";
|
|
310
|
+
else if (key === "right" && state.direction !== "left") state.nextDirection = "right";
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const MAX_WIDTH$1 = 50;
|
|
317
|
+
const MAX_HEIGHT$1 = 18;
|
|
318
|
+
const PADDLE_HEIGHT = 4;
|
|
319
|
+
const WINNING_SCORE = 5;
|
|
320
|
+
const FRAME_TIME$1 = 50;
|
|
321
|
+
const AI_SPEED = 0.8;
|
|
322
|
+
const pong = {
|
|
323
|
+
name: "Pong",
|
|
324
|
+
description: "The original Atari hit - bounce the ball",
|
|
325
|
+
async start() {
|
|
326
|
+
const screen = getScreenSize();
|
|
327
|
+
const gameWidth = Math.min(MAX_WIDTH$1, screen.width - 4);
|
|
328
|
+
const gameHeight = Math.min(MAX_HEIGHT$1, screen.height - 8);
|
|
329
|
+
const offsetX = Math.max(1, Math.floor((screen.width - gameWidth - 2) / 2));
|
|
330
|
+
const offsetY = Math.max(2, Math.floor((screen.height - gameHeight - 4) / 2));
|
|
331
|
+
const state = {
|
|
332
|
+
ballX: Math.floor(gameWidth / 2),
|
|
333
|
+
ballY: Math.floor(gameHeight / 2),
|
|
334
|
+
ballVX: Math.random() > 0.5 ? 0.8 : -0.8,
|
|
335
|
+
ballVY: (Math.random() - 0.5) * 0.8,
|
|
336
|
+
leftPaddleY: Math.floor((gameHeight - PADDLE_HEIGHT) / 2),
|
|
337
|
+
rightPaddleY: Math.floor((gameHeight - PADDLE_HEIGHT) / 2),
|
|
338
|
+
leftScore: 0,
|
|
339
|
+
rightScore: 0,
|
|
340
|
+
gameOver: false,
|
|
341
|
+
isPaused: false,
|
|
342
|
+
winner: null};
|
|
343
|
+
let moveUp = false;
|
|
344
|
+
let moveDown = false;
|
|
345
|
+
hideCursor();
|
|
346
|
+
clearScreen();
|
|
347
|
+
drawBox(offsetX, offsetY, gameWidth + 2, gameHeight + 2, ANSI.cyan);
|
|
348
|
+
writeAt(offsetX + Math.floor(gameWidth / 2) - 1, offsetY + gameHeight + 3, ANSI.bold + ANSI.yellow + "PONG" + ANSI.reset);
|
|
349
|
+
let intervalId;
|
|
350
|
+
function gameLoop() {
|
|
351
|
+
if (state.gameOver) {
|
|
352
|
+
renderGameOver();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (!state.isPaused) {
|
|
356
|
+
processInput();
|
|
357
|
+
updateAI();
|
|
358
|
+
update();
|
|
359
|
+
render();
|
|
360
|
+
} else {
|
|
361
|
+
renderPaused();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function processInput() {
|
|
365
|
+
if (moveUp) {
|
|
366
|
+
state.leftPaddleY = Math.max(0, state.leftPaddleY - 1);
|
|
367
|
+
}
|
|
368
|
+
if (moveDown) {
|
|
369
|
+
state.leftPaddleY = Math.min(gameHeight - PADDLE_HEIGHT, state.leftPaddleY + 1);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function updateAI() {
|
|
373
|
+
const paddleCenter = state.rightPaddleY + PADDLE_HEIGHT / 2;
|
|
374
|
+
const diff = state.ballY - paddleCenter;
|
|
375
|
+
if (state.ballVX > 0) {
|
|
376
|
+
if (diff > 1 && Math.random() < AI_SPEED) {
|
|
377
|
+
state.rightPaddleY = Math.min(gameHeight - PADDLE_HEIGHT, state.rightPaddleY + 1);
|
|
378
|
+
} else if (diff < -1 && Math.random() < AI_SPEED) {
|
|
379
|
+
state.rightPaddleY = Math.max(0, state.rightPaddleY - 1);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function update() {
|
|
384
|
+
state.ballX += state.ballVX;
|
|
385
|
+
state.ballY += state.ballVY;
|
|
386
|
+
if (state.ballY <= 0 || state.ballY >= gameHeight - 1) {
|
|
387
|
+
state.ballVY *= -1;
|
|
388
|
+
state.ballY = Math.max(0, Math.min(gameHeight - 1, state.ballY));
|
|
389
|
+
}
|
|
390
|
+
if (state.ballX <= 2 && state.ballY >= state.leftPaddleY && state.ballY < state.leftPaddleY + PADDLE_HEIGHT) {
|
|
391
|
+
state.ballVX = Math.abs(state.ballVX) * 1.05;
|
|
392
|
+
state.ballVX = Math.min(state.ballVX, 2);
|
|
393
|
+
const hitPos = (state.ballY - state.leftPaddleY) / PADDLE_HEIGHT;
|
|
394
|
+
state.ballVY = (hitPos - 0.5) * 1.5;
|
|
395
|
+
beep();
|
|
396
|
+
}
|
|
397
|
+
if (state.ballX >= gameWidth - 3 && state.ballY >= state.rightPaddleY && state.ballY < state.rightPaddleY + PADDLE_HEIGHT) {
|
|
398
|
+
state.ballVX = -Math.abs(state.ballVX) * 1.05;
|
|
399
|
+
state.ballVX = Math.max(state.ballVX, -2);
|
|
400
|
+
const hitPos = (state.ballY - state.rightPaddleY) / PADDLE_HEIGHT;
|
|
401
|
+
state.ballVY = (hitPos - 0.5) * 1.5;
|
|
402
|
+
beep();
|
|
403
|
+
}
|
|
404
|
+
if (state.ballX <= 0) {
|
|
405
|
+
state.rightScore++;
|
|
406
|
+
if (state.rightScore >= WINNING_SCORE) {
|
|
407
|
+
state.gameOver = true;
|
|
408
|
+
state.winner = "ai";
|
|
409
|
+
} else {
|
|
410
|
+
resetBall();
|
|
411
|
+
}
|
|
412
|
+
} else if (state.ballX >= gameWidth - 1) {
|
|
413
|
+
state.leftScore++;
|
|
414
|
+
if (state.leftScore >= WINNING_SCORE) {
|
|
415
|
+
state.gameOver = true;
|
|
416
|
+
state.winner = "player";
|
|
417
|
+
} else {
|
|
418
|
+
resetBall();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function resetBall() {
|
|
423
|
+
state.ballX = Math.floor(gameWidth / 2);
|
|
424
|
+
state.ballY = Math.floor(gameHeight / 2);
|
|
425
|
+
state.ballVX = (Math.random() > 0.5 ? 1 : -1) * 0.8;
|
|
426
|
+
state.ballVY = (Math.random() - 0.5) * 0.8;
|
|
427
|
+
}
|
|
428
|
+
function render() {
|
|
429
|
+
for (let y = 1; y <= gameHeight; y++) {
|
|
430
|
+
writeAt(offsetX + 1, offsetY + y, " ".repeat(gameWidth));
|
|
431
|
+
}
|
|
432
|
+
for (let y = 1; y <= gameHeight; y++) {
|
|
433
|
+
if (y % 2 === 0) {
|
|
434
|
+
writeAt(offsetX + Math.floor(gameWidth / 2) + 1, offsetY + y, ANSI.dim + ":" + ANSI.reset);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const bx = Math.round(state.ballX);
|
|
438
|
+
const by = Math.round(state.ballY);
|
|
439
|
+
if (bx >= 0 && bx < gameWidth && by >= 0 && by < gameHeight) {
|
|
440
|
+
writeAt(offsetX + bx + 1, offsetY + by + 1, ANSI.white + "o" + ANSI.reset);
|
|
441
|
+
}
|
|
442
|
+
for (let i = 0; i < PADDLE_HEIGHT; i++) {
|
|
443
|
+
const py = state.leftPaddleY + i;
|
|
444
|
+
if (py >= 0 && py < gameHeight) {
|
|
445
|
+
writeAt(offsetX + 2, offsetY + py + 1, ANSI.green + "#" + ANSI.reset);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
for (let i = 0; i < PADDLE_HEIGHT; i++) {
|
|
449
|
+
const py = state.rightPaddleY + i;
|
|
450
|
+
if (py >= 0 && py < gameHeight) {
|
|
451
|
+
writeAt(offsetX + gameWidth - 1, offsetY + py + 1, ANSI.red + "#" + ANSI.reset);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
writeAt(offsetX + Math.floor(gameWidth / 4), offsetY - 1, ANSI.green + state.leftScore.toString() + ANSI.reset);
|
|
455
|
+
writeAt(offsetX + Math.floor(3 * gameWidth / 4), offsetY - 1, ANSI.red + state.rightScore.toString() + ANSI.reset);
|
|
456
|
+
writeAt(
|
|
457
|
+
offsetX,
|
|
458
|
+
offsetY + gameHeight + 3,
|
|
459
|
+
`${ANSI.dim}W/S or Arrows: Move | Q: Quit | P: Pause${ANSI.reset}`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
function renderPaused() {
|
|
463
|
+
writeAt(
|
|
464
|
+
offsetX + Math.floor(gameWidth / 2) - 3,
|
|
465
|
+
offsetY + Math.floor(gameHeight / 2),
|
|
466
|
+
ANSI.yellow + "PAUSED" + ANSI.reset
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
function renderGameOver() {
|
|
470
|
+
const cx = offsetX + Math.floor(gameWidth / 2);
|
|
471
|
+
const cy = offsetY + Math.floor(gameHeight / 2);
|
|
472
|
+
if (state.winner === "player") {
|
|
473
|
+
writeAt(cx - 4, cy - 1, ANSI.green + ANSI.bold + "YOU WIN!" + ANSI.reset);
|
|
474
|
+
} else {
|
|
475
|
+
writeAt(cx - 4, cy - 1, ANSI.red + ANSI.bold + "AI WINS!" + ANSI.reset);
|
|
476
|
+
}
|
|
477
|
+
writeAt(cx - 7, cy + 1, `${ANSI.green}${state.leftScore}${ANSI.reset} - ${ANSI.red}${state.rightScore}${ANSI.reset}`);
|
|
478
|
+
writeAt(cx - 8, cy + 3, ANSI.dim + "R:Restart Q:Quit" + ANSI.reset);
|
|
479
|
+
}
|
|
480
|
+
function cleanup() {
|
|
481
|
+
clearInterval(intervalId);
|
|
482
|
+
stopKeyboardListener();
|
|
483
|
+
showCursor();
|
|
484
|
+
clearScreen();
|
|
485
|
+
}
|
|
486
|
+
function resetGame() {
|
|
487
|
+
state.ballX = Math.floor(gameWidth / 2);
|
|
488
|
+
state.ballY = Math.floor(gameHeight / 2);
|
|
489
|
+
state.ballVX = (Math.random() > 0.5 ? 1 : -1) * 0.8;
|
|
490
|
+
state.ballVY = (Math.random() - 0.5) * 0.8;
|
|
491
|
+
state.leftPaddleY = Math.floor((gameHeight - PADDLE_HEIGHT) / 2);
|
|
492
|
+
state.rightPaddleY = Math.floor((gameHeight - PADDLE_HEIGHT) / 2);
|
|
493
|
+
state.leftScore = 0;
|
|
494
|
+
state.rightScore = 0;
|
|
495
|
+
state.gameOver = false;
|
|
496
|
+
state.isPaused = false;
|
|
497
|
+
state.winner = null;
|
|
498
|
+
clearScreen();
|
|
499
|
+
drawBox(offsetX, offsetY, gameWidth + 2, gameHeight + 2, ANSI.cyan);
|
|
500
|
+
writeAt(offsetX + Math.floor(gameWidth / 2) - 1, offsetY + gameHeight + 3, ANSI.bold + ANSI.yellow + "PONG" + ANSI.reset);
|
|
501
|
+
}
|
|
502
|
+
intervalId = setInterval(gameLoop, FRAME_TIME$1);
|
|
503
|
+
return new Promise((resolve) => {
|
|
504
|
+
startKeyboardListener((key) => {
|
|
505
|
+
if (key === "q") {
|
|
506
|
+
cleanup();
|
|
507
|
+
resolve();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (key === "r" && state.gameOver) {
|
|
511
|
+
resetGame();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (key === "p") {
|
|
515
|
+
state.isPaused = !state.isPaused;
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (key === "up") {
|
|
519
|
+
moveUp = true;
|
|
520
|
+
setTimeout(() => moveUp = false, FRAME_TIME$1 * 2);
|
|
521
|
+
}
|
|
522
|
+
if (key === "down") {
|
|
523
|
+
moveDown = true;
|
|
524
|
+
setTimeout(() => moveDown = false, FRAME_TIME$1 * 2);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const MAX_WIDTH = 44;
|
|
532
|
+
const MAX_HEIGHT = 20;
|
|
533
|
+
const PADDLE_WIDTH = 8;
|
|
534
|
+
const BRICK_WIDTH = 4;
|
|
535
|
+
const BRICK_ROWS = 4;
|
|
536
|
+
const FRAME_TIME = 50;
|
|
537
|
+
const BRICK_COLORS = [
|
|
538
|
+
{ color: ANSI.red, points: 40 },
|
|
539
|
+
{ color: ANSI.yellow, points: 30 },
|
|
540
|
+
{ color: ANSI.green, points: 20 },
|
|
541
|
+
{ color: ANSI.cyan, points: 10 }
|
|
542
|
+
];
|
|
543
|
+
const breakout = {
|
|
544
|
+
name: "Breakout",
|
|
545
|
+
description: "Break the bricks with your paddle",
|
|
546
|
+
async start() {
|
|
547
|
+
const screen = getScreenSize();
|
|
548
|
+
const gameWidth = Math.min(MAX_WIDTH, screen.width - 4);
|
|
549
|
+
const gameHeight = Math.min(MAX_HEIGHT, screen.height - 8);
|
|
550
|
+
const offsetX = Math.max(1, Math.floor((screen.width - gameWidth - 2) / 2));
|
|
551
|
+
const offsetY = Math.max(2, Math.floor((screen.height - gameHeight - 4) / 2));
|
|
552
|
+
const bricksPerRow = Math.floor((gameWidth - 2) / (BRICK_WIDTH + 1));
|
|
553
|
+
function createBricks() {
|
|
554
|
+
const bricks = [];
|
|
555
|
+
for (let row = 0; row < BRICK_ROWS; row++) {
|
|
556
|
+
const { color, points } = BRICK_COLORS[row % BRICK_COLORS.length];
|
|
557
|
+
for (let col = 0; col < bricksPerRow; col++) {
|
|
558
|
+
bricks.push({
|
|
559
|
+
x: 1 + col * (BRICK_WIDTH + 1),
|
|
560
|
+
y: 2 + row,
|
|
561
|
+
color,
|
|
562
|
+
points,
|
|
563
|
+
destroyed: false
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return bricks;
|
|
568
|
+
}
|
|
569
|
+
const state = {
|
|
570
|
+
ballX: Math.floor(gameWidth / 2),
|
|
571
|
+
ballY: gameHeight - 4,
|
|
572
|
+
ballVX: 0.5,
|
|
573
|
+
ballVY: -0.5,
|
|
574
|
+
paddleX: Math.floor((gameWidth - PADDLE_WIDTH) / 2),
|
|
575
|
+
bricks: createBricks(),
|
|
576
|
+
score: 0,
|
|
577
|
+
lives: 3,
|
|
578
|
+
gameOver: false,
|
|
579
|
+
isPaused: false,
|
|
580
|
+
won: false};
|
|
581
|
+
let moveLeft = false;
|
|
582
|
+
let moveRight = false;
|
|
583
|
+
hideCursor();
|
|
584
|
+
clearScreen();
|
|
585
|
+
drawBox(offsetX, offsetY, gameWidth + 2, gameHeight + 2, ANSI.cyan);
|
|
586
|
+
writeAt(offsetX + Math.floor(gameWidth / 2) - 3, offsetY - 1, ANSI.bold + ANSI.yellow + "BREAKOUT" + ANSI.reset);
|
|
587
|
+
let intervalId;
|
|
588
|
+
function gameLoop() {
|
|
589
|
+
if (state.gameOver) {
|
|
590
|
+
renderGameOver();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (!state.isPaused) {
|
|
594
|
+
processInput();
|
|
595
|
+
update();
|
|
596
|
+
render();
|
|
597
|
+
} else {
|
|
598
|
+
renderPaused();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function processInput() {
|
|
602
|
+
if (moveLeft) {
|
|
603
|
+
state.paddleX = Math.max(0, state.paddleX - 2);
|
|
604
|
+
}
|
|
605
|
+
if (moveRight) {
|
|
606
|
+
state.paddleX = Math.min(gameWidth - PADDLE_WIDTH, state.paddleX + 2);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function update() {
|
|
610
|
+
state.ballX += state.ballVX;
|
|
611
|
+
state.ballY += state.ballVY;
|
|
612
|
+
if (state.ballX <= 0 || state.ballX >= gameWidth - 1) {
|
|
613
|
+
state.ballVX *= -1;
|
|
614
|
+
state.ballX = Math.max(0, Math.min(gameWidth - 1, state.ballX));
|
|
615
|
+
}
|
|
616
|
+
if (state.ballY <= 0) {
|
|
617
|
+
state.ballVY *= -1;
|
|
618
|
+
state.ballY = 0;
|
|
619
|
+
}
|
|
620
|
+
if (state.ballY >= gameHeight - 2 && state.ballY < gameHeight && state.ballX >= state.paddleX - 1 && state.ballX <= state.paddleX + PADDLE_WIDTH) {
|
|
621
|
+
state.ballVY = -Math.abs(state.ballVY);
|
|
622
|
+
const hitPos = (state.ballX - state.paddleX) / PADDLE_WIDTH;
|
|
623
|
+
state.ballVX = (hitPos - 0.5) * 1.5;
|
|
624
|
+
state.ballY = gameHeight - 3;
|
|
625
|
+
beep();
|
|
626
|
+
}
|
|
627
|
+
if (state.ballY >= gameHeight) {
|
|
628
|
+
state.lives--;
|
|
629
|
+
if (state.lives <= 0) {
|
|
630
|
+
state.gameOver = true;
|
|
631
|
+
state.won = false;
|
|
632
|
+
} else {
|
|
633
|
+
resetBall();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const bx = Math.round(state.ballX);
|
|
637
|
+
const by = Math.round(state.ballY);
|
|
638
|
+
for (const brick of state.bricks) {
|
|
639
|
+
if (brick.destroyed) continue;
|
|
640
|
+
if (bx >= brick.x && bx < brick.x + BRICK_WIDTH && by === brick.y) {
|
|
641
|
+
brick.destroyed = true;
|
|
642
|
+
state.score += brick.points;
|
|
643
|
+
state.ballVY *= -1;
|
|
644
|
+
beep();
|
|
645
|
+
if (state.bricks.every((b) => b.destroyed)) {
|
|
646
|
+
state.gameOver = true;
|
|
647
|
+
state.won = true;
|
|
648
|
+
}
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function resetBall() {
|
|
654
|
+
state.ballX = Math.floor(gameWidth / 2);
|
|
655
|
+
state.ballY = gameHeight - 4;
|
|
656
|
+
state.ballVX = (Math.random() > 0.5 ? 1 : -1) * 0.5;
|
|
657
|
+
state.ballVY = -0.5;
|
|
658
|
+
}
|
|
659
|
+
function render() {
|
|
660
|
+
for (let y = 1; y <= gameHeight; y++) {
|
|
661
|
+
writeAt(offsetX + 1, offsetY + y, " ".repeat(gameWidth));
|
|
662
|
+
}
|
|
663
|
+
for (const brick of state.bricks) {
|
|
664
|
+
if (brick.destroyed) continue;
|
|
665
|
+
writeAt(offsetX + brick.x + 1, offsetY + brick.y + 1, brick.color + "=".repeat(BRICK_WIDTH) + ANSI.reset);
|
|
666
|
+
}
|
|
667
|
+
const bx = Math.round(state.ballX);
|
|
668
|
+
const by = Math.round(state.ballY);
|
|
669
|
+
if (bx >= 0 && bx < gameWidth && by >= 0 && by < gameHeight) {
|
|
670
|
+
writeAt(offsetX + bx + 1, offsetY + by + 1, ANSI.white + "o" + ANSI.reset);
|
|
671
|
+
}
|
|
672
|
+
writeAt(offsetX + state.paddleX + 1, offsetY + gameHeight, ANSI.green + "=".repeat(PADDLE_WIDTH) + ANSI.reset);
|
|
673
|
+
const hearts = ANSI.red + "\u2665".repeat(state.lives) + ANSI.reset;
|
|
674
|
+
writeAt(offsetX, offsetY + gameHeight + 3, `${ANSI.yellow}Score: ${state.score}${ANSI.reset} | ${hearts} | ${ANSI.dim}Q:Quit P:Pause${ANSI.reset}`);
|
|
675
|
+
}
|
|
676
|
+
function renderPaused() {
|
|
677
|
+
writeAt(offsetX + Math.floor(gameWidth / 2) - 3, offsetY + Math.floor(gameHeight / 2), ANSI.yellow + "PAUSED" + ANSI.reset);
|
|
678
|
+
}
|
|
679
|
+
function renderGameOver() {
|
|
680
|
+
const cx = offsetX + Math.floor(gameWidth / 2);
|
|
681
|
+
const cy = offsetY + Math.floor(gameHeight / 2);
|
|
682
|
+
if (state.won) {
|
|
683
|
+
writeAt(cx - 4, cy - 1, ANSI.green + ANSI.bold + "YOU WIN!" + ANSI.reset);
|
|
684
|
+
} else {
|
|
685
|
+
writeAt(cx - 5, cy - 1, ANSI.red + ANSI.bold + "GAME OVER!" + ANSI.reset);
|
|
686
|
+
}
|
|
687
|
+
writeAt(cx - 5, cy + 1, `Score: ${ANSI.yellow}${state.score}${ANSI.reset}`);
|
|
688
|
+
writeAt(cx - 8, cy + 3, ANSI.dim + "R:Restart Q:Quit" + ANSI.reset);
|
|
689
|
+
}
|
|
690
|
+
function cleanup() {
|
|
691
|
+
clearInterval(intervalId);
|
|
692
|
+
stopKeyboardListener();
|
|
693
|
+
showCursor();
|
|
694
|
+
clearScreen();
|
|
695
|
+
}
|
|
696
|
+
function resetGame() {
|
|
697
|
+
state.ballX = Math.floor(gameWidth / 2);
|
|
698
|
+
state.ballY = gameHeight - 4;
|
|
699
|
+
state.ballVX = (Math.random() > 0.5 ? 1 : -1) * 0.5;
|
|
700
|
+
state.ballVY = -0.5;
|
|
701
|
+
state.paddleX = Math.floor((gameWidth - PADDLE_WIDTH) / 2);
|
|
702
|
+
state.bricks = createBricks();
|
|
703
|
+
state.score = 0;
|
|
704
|
+
state.lives = 3;
|
|
705
|
+
state.gameOver = false;
|
|
706
|
+
state.isPaused = false;
|
|
707
|
+
state.won = false;
|
|
708
|
+
clearScreen();
|
|
709
|
+
drawBox(offsetX, offsetY, gameWidth + 2, gameHeight + 2, ANSI.cyan);
|
|
710
|
+
writeAt(offsetX + Math.floor(gameWidth / 2) - 3, offsetY - 1, ANSI.bold + ANSI.yellow + "BREAKOUT" + ANSI.reset);
|
|
711
|
+
}
|
|
712
|
+
intervalId = setInterval(gameLoop, FRAME_TIME);
|
|
713
|
+
return new Promise((resolve) => {
|
|
714
|
+
startKeyboardListener((key) => {
|
|
715
|
+
if (key === "q") {
|
|
716
|
+
cleanup();
|
|
717
|
+
resolve();
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (key === "r" && state.gameOver) {
|
|
721
|
+
resetGame();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (key === "p" || key === "space") {
|
|
725
|
+
state.isPaused = !state.isPaused;
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (key === "left") {
|
|
729
|
+
moveLeft = true;
|
|
730
|
+
setTimeout(() => moveLeft = false, FRAME_TIME * 2);
|
|
731
|
+
}
|
|
732
|
+
if (key === "right") {
|
|
733
|
+
moveRight = true;
|
|
734
|
+
setTimeout(() => moveRight = false, FRAME_TIME * 2);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const games = {
|
|
742
|
+
snake,
|
|
743
|
+
pong,
|
|
744
|
+
breakout
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
async function showMenu() {
|
|
748
|
+
intro(pc.inverse(" Welcome to Atari Arcade! "));
|
|
749
|
+
const gameOptions = Object.entries(games).map(([key, game]) => ({
|
|
750
|
+
value: key,
|
|
751
|
+
label: game.name,
|
|
752
|
+
hint: game.description
|
|
753
|
+
}));
|
|
754
|
+
const selectedGame = await select({
|
|
755
|
+
message: "Choose your game:",
|
|
756
|
+
options: gameOptions
|
|
757
|
+
});
|
|
758
|
+
if (isCancel(selectedGame)) {
|
|
759
|
+
outro(pc.dim("Thanks for visiting!"));
|
|
760
|
+
process.exit(0);
|
|
761
|
+
}
|
|
762
|
+
const s = spinner();
|
|
763
|
+
s.start("Loading game...");
|
|
764
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
765
|
+
s.stop("Game loaded!");
|
|
766
|
+
await games[selectedGame].start();
|
|
767
|
+
outro(pc.green("Thanks for playing!"));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const main = defineCommand({
|
|
771
|
+
meta: {
|
|
772
|
+
name: "atari",
|
|
773
|
+
version: "1.0.0",
|
|
774
|
+
description: "Retro Atari games in your terminal"
|
|
775
|
+
},
|
|
776
|
+
args: {
|
|
777
|
+
game: {
|
|
778
|
+
type: "positional",
|
|
779
|
+
description: "Game to play (snake, pong, breakout)",
|
|
780
|
+
required: false
|
|
781
|
+
},
|
|
782
|
+
list: {
|
|
783
|
+
type: "boolean",
|
|
784
|
+
alias: "l",
|
|
785
|
+
description: "List available games"
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
async run({ args }) {
|
|
789
|
+
await showBanner();
|
|
790
|
+
if (args.list) {
|
|
791
|
+
console.log("\nAvailable games:");
|
|
792
|
+
for (const [key, game] of Object.entries(games)) {
|
|
793
|
+
console.log(` - ${key}: ${game.description}`);
|
|
794
|
+
}
|
|
795
|
+
console.log("\nUsage: atari <game>");
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (args.game) {
|
|
799
|
+
const gameKey = args.game;
|
|
800
|
+
if (games[gameKey]) {
|
|
801
|
+
await games[gameKey].start();
|
|
802
|
+
} else {
|
|
803
|
+
console.error(`Unknown game: ${args.game}`);
|
|
804
|
+
console.log("Available games: " + Object.keys(games).join(", "));
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
await showMenu();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
runMain(main);
|
package/package.json
CHANGED
|
@@ -1,7 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atari",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Retro Atari games in your terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"atari": "./bin/atari.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"bin",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./dist/index.mjs",
|
|
17
|
+
"types": "./dist/index.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "unbuild",
|
|
22
|
+
"dev": "tsx src/index.ts",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"atari",
|
|
27
|
+
"games",
|
|
28
|
+
"terminal",
|
|
29
|
+
"cli",
|
|
30
|
+
"retro",
|
|
31
|
+
"arcade",
|
|
32
|
+
"snake",
|
|
33
|
+
"pong",
|
|
34
|
+
"breakout"
|
|
35
|
+
],
|
|
36
|
+
"author": "",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@clack/prompts": "^0.11.0",
|
|
40
|
+
"citty": "^0.2.0",
|
|
41
|
+
"figlet": "^1.8.0",
|
|
42
|
+
"picocolors": "^1.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/figlet": "^1.7.0",
|
|
46
|
+
"@types/node": "^22.0.0",
|
|
47
|
+
"tsx": "^4.19.0",
|
|
48
|
+
"typescript": "^5.7.0",
|
|
49
|
+
"unbuild": "^3.5.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
// placeholder
|