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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/index.mjs";
@@ -0,0 +1,2 @@
1
+
2
+ export { };
@@ -0,0 +1,2 @@
1
+
2
+ export { };
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.1-alpha.1",
4
- "description": "Placeholder for an upcoming project",
5
- "main": "index.js",
6
- "license": "MIT"
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