@teeg/gfn 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.
Files changed (3) hide show
  1. package/README.md +34 -0
  2. package/bin/pong.js +184 -0
  3. package/package.json +15 -0
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # Terminal Pong
2
+
3
+ Play Pong in your terminal with a tiny Node.js CLI.
4
+
5
+ ## Install
6
+
7
+ 1) Install Node.js (which includes `npm`).
8
+ 2) From this folder (for local testing):
9
+
10
+ ```bash
11
+ npm install -g .
12
+ gfn
13
+ ```
14
+
15
+ ## Run Anywhere
16
+
17
+ Once published to npm, you can run:
18
+
19
+ ```bash
20
+ npx @teeg/gfn
21
+ ```
22
+
23
+ Or install globally:
24
+
25
+ ```bash
26
+ npm install -g @teeg/gfn
27
+ gfn
28
+ ```
29
+
30
+ ## Controls
31
+
32
+ - `W` / `S` or Up / Down: move paddle
33
+ - Space: serve
34
+ - `Q`: quit
package/bin/pong.js ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const stdout = process.stdout;
5
+ const stdin = process.stdin;
6
+
7
+ const ESC = '\u001b[';
8
+ const HIDE_CURSOR = `${ESC}?25l`;
9
+ const SHOW_CURSOR = `${ESC}?25h`;
10
+ const CLEAR = `${ESC}2J${ESC}H`;
11
+
12
+ const FRAME_MS = 33;
13
+ const PADDLE_H = 5;
14
+ const PADDLE_SPEED = 1.4;
15
+ const BALL_SPEED = 0.6;
16
+ const AI_FOLLOW = 0.35;
17
+
18
+ let width = Math.max(40, stdout.columns || 80);
19
+ let height = Math.max(18, stdout.rows || 24);
20
+
21
+ let running = true;
22
+ let servePending = true;
23
+ let scoreLeft = 0;
24
+ let scoreRight = 0;
25
+
26
+ let leftY = Math.floor(height / 2 - PADDLE_H / 2);
27
+ let rightY = leftY;
28
+ let ball = {
29
+ x: Math.floor(width / 2),
30
+ y: Math.floor(height / 2),
31
+ vx: BALL_SPEED,
32
+ vy: BALL_SPEED / 2,
33
+ };
34
+
35
+ function clamp(val, min, max) {
36
+ return Math.max(min, Math.min(max, val));
37
+ }
38
+
39
+ function resetBall(direction) {
40
+ ball.x = Math.floor(width / 2);
41
+ ball.y = Math.floor(height / 2);
42
+ const angle = (Math.random() * 0.6 + 0.2) * (Math.random() < 0.5 ? -1 : 1);
43
+ ball.vx = BALL_SPEED * direction;
44
+ ball.vy = BALL_SPEED * angle;
45
+ servePending = true;
46
+ }
47
+
48
+ function resize() {
49
+ width = Math.max(40, stdout.columns || 80);
50
+ height = Math.max(18, stdout.rows || 24);
51
+ leftY = clamp(leftY, 2, height - PADDLE_H - 2);
52
+ rightY = clamp(rightY, 2, height - PADDLE_H - 2);
53
+ }
54
+
55
+ function handleInput() {
56
+ leftY = clamp(leftY, 2, height - PADDLE_H - 2);
57
+ }
58
+
59
+ function updateAI() {
60
+ const target = ball.y - Math.floor(PADDLE_H / 2);
61
+ rightY += (target - rightY) * AI_FOLLOW;
62
+ rightY = clamp(rightY, 2, height - PADDLE_H - 2);
63
+ }
64
+
65
+ function stepBall() {
66
+ if (servePending) return;
67
+ ball.x += ball.vx;
68
+ ball.y += ball.vy;
69
+
70
+ if (ball.y <= 2 || ball.y >= height - 2) {
71
+ ball.vy *= -1;
72
+ ball.y = clamp(ball.y, 2, height - 2);
73
+ }
74
+
75
+ const leftX = 3;
76
+ const rightX = width - 4;
77
+
78
+ if (ball.x <= leftX + 1) {
79
+ if (ball.y >= leftY && ball.y <= leftY + PADDLE_H) {
80
+ ball.vx = Math.abs(ball.vx);
81
+ ball.x = leftX + 2;
82
+ ball.vy += (Math.random() - 0.5) * 0.15;
83
+ } else {
84
+ scoreRight += 1;
85
+ resetBall(1);
86
+ }
87
+ }
88
+
89
+ if (ball.x >= rightX - 1) {
90
+ if (ball.y >= rightY && ball.y <= rightY + PADDLE_H) {
91
+ ball.vx = -Math.abs(ball.vx);
92
+ ball.x = rightX - 2;
93
+ ball.vy += (Math.random() - 0.5) * 0.15;
94
+ } else {
95
+ scoreLeft += 1;
96
+ resetBall(-1);
97
+ }
98
+ }
99
+ }
100
+
101
+ function draw() {
102
+ let out = CLEAR;
103
+ const top = '+' + '-'.repeat(width - 2) + '+';
104
+ const bottom = top;
105
+ out += top + '\n';
106
+
107
+ for (let row = 1; row < height - 1; row += 1) {
108
+ let line = '|';
109
+ for (let col = 1; col < width - 1; col += 1) {
110
+ const mid = Math.floor(width / 2);
111
+ let ch = ' ';
112
+
113
+ if (col === mid && row % 2 === 0) ch = '.';
114
+
115
+ if (col === 3 && row >= Math.floor(leftY) && row <= Math.floor(leftY + PADDLE_H)) {
116
+ ch = '|';
117
+ }
118
+
119
+ if (col === width - 4 && row >= Math.floor(rightY) && row <= Math.floor(rightY + PADDLE_H)) {
120
+ ch = '|';
121
+ }
122
+
123
+ if (Math.round(ball.x) === col && Math.round(ball.y) === row) {
124
+ ch = 'o';
125
+ }
126
+
127
+ line += ch;
128
+ }
129
+ line += '|';
130
+ out += line + '\n';
131
+ }
132
+
133
+ out += bottom + '\n';
134
+ const status = ` W/S to move, Space to serve, Q to quit ${scoreLeft} : ${scoreRight} `;
135
+ out += status.slice(0, width);
136
+ stdout.write(out);
137
+ }
138
+
139
+ function loop() {
140
+ if (!running) return;
141
+ handleInput();
142
+ updateAI();
143
+ stepBall();
144
+ draw();
145
+ setTimeout(loop, FRAME_MS);
146
+ }
147
+
148
+ function cleanup() {
149
+ stdout.write(SHOW_CURSOR + '\n');
150
+ stdin.setRawMode(false);
151
+ stdin.pause();
152
+ }
153
+
154
+ function start() {
155
+ stdout.write(HIDE_CURSOR);
156
+ stdin.setRawMode(true);
157
+ stdin.resume();
158
+ stdin.setEncoding('utf8');
159
+
160
+ stdin.on('data', (key) => {
161
+ if (key === '\u0003' || key === 'q' || key === 'Q') {
162
+ running = false;
163
+ cleanup();
164
+ process.exit(0);
165
+ }
166
+ if (key === ' ') {
167
+ servePending = false;
168
+ }
169
+ if (key === '\u001b[A' || key === 'w' || key === 'W') {
170
+ leftY -= PADDLE_SPEED * 2;
171
+ }
172
+ if (key === '\u001b[B' || key === 's' || key === 'S') {
173
+ leftY += PADDLE_SPEED * 2;
174
+ }
175
+ });
176
+
177
+ process.on('SIGWINCH', resize);
178
+ process.on('exit', cleanup);
179
+
180
+ resetBall(Math.random() < 0.5 ? -1 : 1);
181
+ loop();
182
+ }
183
+
184
+ start();
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@teeg/gfn",
3
+ "version": "1.0.0",
4
+ "description": "Play Pong in your terminal.",
5
+ "bin": {
6
+ "gfn": "bin/pong.js"
7
+ },
8
+ "license": "MIT",
9
+ "keywords": [
10
+ "pong",
11
+ "terminal",
12
+ "cli",
13
+ "game"
14
+ ]
15
+ }