retro-pong-game 1.1.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,402 @@
1
+ # retro-pong-game
2
+
3
+ A retro-style Pong game that runs in any browser. Drop it onto your website with a single constructor call. No frameworks, no extra configuration required.
4
+
5
+ - Animated starfield or nebular background
6
+ - Particle effects on paddle hits
7
+ - Countdown timer with configurable duration
8
+ - Adaptive AI difficulty (auto-adjusts after scoring streaks)
9
+ - In-game settings panel (changes saved to `localStorage`)
10
+ - Stereo-panned sound effects
11
+ - Fully configurable colors, sizes, and speeds
12
+ - TypeScript types included
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install retro-pong-game
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Quick start
25
+
26
+ ### 1. Add a container element to your HTML
27
+
28
+ ```html
29
+ <div id="game"></div>
30
+ ```
31
+
32
+ The game appends a `<canvas>` inside this element automatically.
33
+
34
+ ### 2. Import and create the game
35
+
36
+ ```js
37
+ import { PongGame } from 'retro-pong-game';
38
+
39
+ const game = new PongGame(
40
+ {
41
+ canvasWidth: 600,
42
+ canvasHeight: 400,
43
+ hasSound: true,
44
+ colors: {
45
+ background: '#1a1a2e',
46
+ paddle: '#e94560',
47
+ ball: '#e94560',
48
+ },
49
+ },
50
+ '#game'
51
+ );
52
+ ```
53
+
54
+ ### 3. Clean up when done
55
+
56
+ Always call `destroy()` before removing the element from the page:
57
+
58
+ ```js
59
+ game.destroy();
60
+ ```
61
+
62
+ ---
63
+
64
+ ## CDN (no npm required)
65
+
66
+ ```html
67
+ <!DOCTYPE html>
68
+ <html lang="en">
69
+ <head>
70
+ <meta charset="UTF-8" />
71
+ <title>Pong</title>
72
+ </head>
73
+ <body>
74
+ <div id="game"></div>
75
+
76
+ <script src="https://unpkg.com/retro-pong-game/dist/pong-game.umd.js"></script>
77
+ <script>
78
+ const game = new PongGame.PongGame(
79
+ { canvasWidth: 600, canvasHeight: 400 },
80
+ '#game'
81
+ );
82
+ </script>
83
+ </body>
84
+ </html>
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Framework examples
90
+
91
+ ### React
92
+
93
+ ```jsx
94
+ import { useEffect, useRef } from 'react';
95
+ import { PongGame } from 'retro-pong-game';
96
+
97
+ export default function PongWidget() {
98
+ const gameRef = useRef(null);
99
+
100
+ useEffect(() => {
101
+ gameRef.current = new PongGame(
102
+ { canvasWidth: 600, canvasHeight: 400 },
103
+ '#pong-container'
104
+ );
105
+
106
+ return () => {
107
+ gameRef.current?.destroy();
108
+ };
109
+ }, []);
110
+
111
+ return <div id="pong-container" />;
112
+ }
113
+ ```
114
+
115
+ ### Vue 3
116
+
117
+ ```vue
118
+ <template>
119
+ <div id="pong-container" />
120
+ </template>
121
+
122
+ <script setup>
123
+ import { onMounted, onBeforeUnmount } from 'vue';
124
+ import { PongGame } from 'retro-pong-game';
125
+
126
+ let game = null;
127
+
128
+ onMounted(() => {
129
+ game = new PongGame({ canvasWidth: 600, canvasHeight: 400 }, '#pong-container');
130
+ });
131
+
132
+ onBeforeUnmount(() => {
133
+ game?.destroy();
134
+ });
135
+ </script>
136
+ ```
137
+
138
+ ### Vanilla HTML + ES modules
139
+
140
+ ```html
141
+ <div id="game"></div>
142
+
143
+ <script type="module">
144
+ import { PongGame } from '/node_modules/retro-pong-game/dist/pong-game.es.js';
145
+
146
+ const game = new PongGame({ canvasWidth: 600, canvasHeight: 400 }, '#game');
147
+ </script>
148
+ ```
149
+
150
+ ### TypeScript
151
+
152
+ Import `PongGameConfig` for full type safety and editor autocomplete on every option:
153
+
154
+ ```ts
155
+ import { PongGame, PongGameConfig } from 'retro-pong-game';
156
+
157
+ const config: PongGameConfig = {
158
+ autoSize: false,
159
+ canvasWidth: 400,
160
+ canvasHeight: 500,
161
+ paddleWidth: 70,
162
+ paddleHeight: 10,
163
+ ballDiameter: 7,
164
+ ballSpeed: 3,
165
+ paddleMoveStep: 4,
166
+ timer: {
167
+ duration: '1:30',
168
+ hasBackgroundCircle: true,
169
+ labelColor: '#ffffff',
170
+ labelFontSize: 12,
171
+ circleColor: '#62cb31',
172
+ circleLineWidth: 5,
173
+ circleBackgroundColor: '#333',
174
+ },
175
+ colors: {
176
+ paddle: '#4daae8',
177
+ ball: '#ff7a59',
178
+ background: '#222',
179
+ centerline: '#9e9c9c',
180
+ score: '#9e9c9c',
181
+ },
182
+ animatedBackground: {
183
+ starfield: true,
184
+ nebular: false,
185
+ },
186
+ difficultyLevel: 1,
187
+ particleBounce: true,
188
+ particleConfig: {
189
+ particlesCount: 20,
190
+ },
191
+ hasSound: true,
192
+ onSoundToggle: (enabled: boolean) => {
193
+ // enabled = true → sound is ON
194
+ // enabled = false → sound is OFF (muted)
195
+ console.log('Sound toggled:', enabled);
196
+ },
197
+ };
198
+
199
+ const game = new PongGame(config, '#game');
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Configuration
205
+
206
+ All options are optional. Omitted values fall back to built-in defaults.
207
+
208
+ ```ts
209
+ new PongGame(config: PongGameConfig, selector: string)
210
+ ```
211
+
212
+ The config is deep-merged in this order (later layers win):
213
+
214
+ 1. Built-in defaults
215
+ 2. Config you pass to the constructor
216
+ 3. Settings the player saved through the in-game settings panel (`localStorage`)
217
+
218
+ ### `PongGameConfig`
219
+
220
+ | Option | Type | Default | Description |
221
+ |---|---|---|---|
222
+ | `autoSize` | `boolean` | `false` | Size the canvas to fill its container element at startup |
223
+ | `autoResize` | `boolean` | `false` | Automatically resize the canvas when the container changes size (uses `ResizeObserver`) |
224
+ | `canvasWidth` | `number` | `400` | Canvas width in px — ignored when `autoSize` is `true` |
225
+ | `canvasHeight` | `number` | `300` | Canvas height in px — ignored when `autoSize` is `true` |
226
+ | `paddleWidth` | `number` | `70` | Paddle width in px |
227
+ | `paddleHeight` | `number` | `10` | Paddle height in px |
228
+ | `ballDiameter` | `number` | `7` | Ball diameter in px |
229
+ | `ballSpeed` | `number` | `3` | Starting ball speed (px per frame) |
230
+ | `paddleMoveStep` | `number` | `4` | Paddle speed while an arrow key is held (px per frame) |
231
+ | `difficultyLevel` | `number` | `1` | Starting AI difficulty — higher = faster and more precise |
232
+ | `particleBounce` | `boolean` | `true` | Particle burst when the ball hits a paddle |
233
+ | `hasSound` | `boolean` | `false` | Enable sound effects |
234
+ | `onSoundToggle` | `(enabled: boolean) => void` | — | Called when the player toggles the sound icon |
235
+ | `timer` | [`TimerConfig`](#timerconfig) | see below | Countdown timer settings |
236
+ | `colors` | [`ColorConfig`](#colorconfig) | see below | Colors for game elements |
237
+ | `animatedBackground` | [`AnimatedBackgroundConfig`](#animatedbackgroundconfig) | see below | Background animation |
238
+ | `particleConfig` | [`ParticleConfig`](#particleconfig) | see below | Particle effect settings |
239
+
240
+ ### `TimerConfig`
241
+
242
+ | Option | Type | Default | Description |
243
+ |---|---|---|---|
244
+ | `duration` | `string` | `"2:00"` | Match duration in `"M:SS"` format |
245
+ | `hasBackgroundCircle` | `boolean` | `false` | Show a circular progress ring around the timer |
246
+ | `labelColor` | `string` | `"#ffffff"` | Timer text color |
247
+ | `labelFontSize` | `number` | `12` | Timer text size in px |
248
+ | `circleColor` | `string` | `"#62cb31"` | Countdown ring stroke color |
249
+ | `circleLineWidth` | `number` | `5` | Countdown ring line width in px |
250
+ | `circleBackgroundColor` | `string` | `"#ff00ff"` | Countdown ring background track color |
251
+
252
+ ### `ColorConfig`
253
+
254
+ | Option | Type | Default | Description |
255
+ |---|---|---|---|
256
+ | `paddle` | `string` | `"#ffffff"` | Paddle color |
257
+ | `ball` | `string` | `"#ffffff"` | Ball color |
258
+ | `background` | `string` | `"#000000"` | Canvas background color |
259
+ | `centerline` | `string` | `"#9e9c9c"` | Center dividing line color |
260
+ | `score` | `string` | `"#9e9c9c"` | Score text color |
261
+
262
+ ### `AnimatedBackgroundConfig`
263
+
264
+ | Option | Type | Default | Description |
265
+ |---|---|---|---|
266
+ | `starfield` | `boolean` | `true` | Scrolling starfield effect |
267
+ | `nebular` | `boolean` | `false` | Scrolling nebular cloud effect |
268
+
269
+ > Enable at most one background effect at a time.
270
+
271
+ ### `ParticleConfig`
272
+
273
+ | Option | Type | Default | Description |
274
+ |---|---|---|---|
275
+ | `particlesCount` | `number` | `20` | Number of particles spawned per paddle hit |
276
+
277
+ ---
278
+
279
+ ## Methods
280
+
281
+ ```js
282
+ // Control the user paddle programmatically (e.g. on-screen buttons, touch)
283
+ game.pressLeft(); // start moving paddle left
284
+ game.releaseLeft(); // stop moving paddle left
285
+ game.pressRight(); // start moving paddle right
286
+ game.releaseRight(); // stop moving paddle right
287
+
288
+ // Enable or disable sound effects
289
+ game.toggleSound(true);
290
+ game.toggleSound(false);
291
+
292
+ // Check if sound is currently enabled
293
+ game.hasSound(); // → boolean
294
+
295
+ // Adjust AI difficulty manually
296
+ game.increaseDifficulty();
297
+ game.decreaseDifficulty();
298
+
299
+ // Resize the canvas when the container changes size
300
+ window.addEventListener('resize', () => game.resizeGame());
301
+
302
+ // Get the internal SoundHelper for advanced audio control
303
+ const soundHelper = game.getSoundHelper();
304
+
305
+ // Tear down the game completely (always call before unmounting)
306
+ game.destroy();
307
+ ```
308
+
309
+ ### Paddle control example
310
+
311
+ Use `pressLeft` / `releaseLeft` / `pressRight` / `releaseRight` to wire on-screen buttons for touch or mouse control:
312
+
313
+ ```js
314
+ function wireButton(btn, pressFn, releaseFn) {
315
+ btn.addEventListener('pointerdown', (e) => {
316
+ e.preventDefault();
317
+ btn.setPointerCapture(e.pointerId);
318
+ pressFn();
319
+ });
320
+ btn.addEventListener('pointerup', releaseFn);
321
+ btn.addEventListener('pointercancel', releaseFn);
322
+ }
323
+
324
+ const game = new PongGame(config, '#my-pong');
325
+
326
+ wireButton(document.getElementById('btn-left'),
327
+ () => game.pressLeft(), () => game.releaseLeft());
328
+ wireButton(document.getElementById('btn-right'),
329
+ () => game.pressRight(), () => game.releaseRight());
330
+ ```
331
+
332
+ ---
333
+
334
+ ## Keyboard controls
335
+
336
+ | Key | Action |
337
+ |---|---|
338
+ | `←` Arrow Left | Move paddle left |
339
+ | `→` Arrow Right | Move paddle right |
340
+ | `P` or `Space` | Pause / resume |
341
+
342
+ The player controls the **bottom** paddle. The **top** paddle is the AI opponent.
343
+
344
+ ---
345
+
346
+ ## Settings panel
347
+
348
+ The game includes a built-in settings panel (gear icon in the corner of the canvas). Players can change colors, background effects, difficulty, timer duration, and more. All changes are saved to `localStorage` and restored automatically on the next visit.
349
+
350
+ To reset all saved settings back to defaults:
351
+
352
+ ```js
353
+ localStorage.removeItem('chriscreativecode.com');
354
+ ```
355
+
356
+ ---
357
+
358
+ ## Resize handling
359
+
360
+ ### Automatic (recommended)
361
+
362
+ Set `autoSize: true` and `autoResize: true` together. The game sizes itself to the container on startup and re-sizes automatically whenever the container changes — no extra code needed.
363
+
364
+ ```js
365
+ const game = new PongGame(
366
+ { autoSize: true, autoResize: true },
367
+ '#game'
368
+ );
369
+ ```
370
+
371
+ The container must have a CSS-defined size (width and height). For example, to fill the viewport:
372
+
373
+ ```css
374
+ #game {
375
+ width: 100%;
376
+ height: 100vh;
377
+ }
378
+ ```
379
+
380
+ ### Manual
381
+
382
+ When `autoResize` is `false` (the default), call `resizeGame()` yourself whenever the container changes size:
383
+
384
+ ```js
385
+ window.addEventListener('resize', () => game.resizeGame());
386
+ ```
387
+
388
+ ### `autoSize` without `autoResize`
389
+
390
+ `autoSize: true` alone sizes the canvas once at startup. The game then has a fixed size until you call `resizeGame()` manually.
391
+
392
+ ---
393
+
394
+ ## Browser support
395
+
396
+ All modern browsers with HTML Canvas support (Chrome, Firefox, Safari, Edge). When importing the ES module build, a bundler (Vite, Webpack, esbuild, Parcel) is recommended for production use.
397
+
398
+ ---
399
+
400
+ ## License
401
+
402
+ MIT © [Chris Schardijn](https://chriscreativecode.com)
@@ -0,0 +1,196 @@
1
+ /** Configuration for the countdown timer displayed during a match. */
2
+ export interface TimerConfig {
3
+ /** Timer duration in "M:SS" format. Default: `"2:00"` */
4
+ duration?: string;
5
+ /** Show a circular progress ring behind the timer. Default: `false` */
6
+ hasBackgroundCircle?: boolean;
7
+ /** Color of the timer label text. Default: `"#ffffff"` */
8
+ labelColor?: string;
9
+ /** Font size of the timer label in pixels. Default: `12` */
10
+ labelFontSize?: number;
11
+ /** Stroke color of the countdown ring. Default: `"#62cb31"` */
12
+ circleColor?: string;
13
+ /** Line width of the countdown ring in pixels. Default: `5` */
14
+ circleLineWidth?: number;
15
+ /** Background track color of the countdown ring. Default: `"#ff00ff"` */
16
+ circleBackgroundColor?: string;
17
+ }
18
+
19
+ /** Color configuration for the main game elements. */
20
+ export interface ColorConfig {
21
+ /** Paddle fill color. Default: `"#ffffff"` */
22
+ paddle?: string;
23
+ /** Ball fill color. Default: `"#ffffff"` */
24
+ ball?: string;
25
+ /** Canvas background color. Default: `"#000000"` */
26
+ background?: string;
27
+ /** Center dividing line color. Default: `"#9e9c9c"` */
28
+ centerline?: string;
29
+ /** Score text color. Default: `"#9e9c9c"` */
30
+ score?: string;
31
+ }
32
+
33
+ /** Animated background effects. Enable at most one at a time. */
34
+ export interface AnimatedBackgroundConfig {
35
+ /** Enable a scrolling starfield effect. Default: `true` */
36
+ starfield?: boolean;
37
+ /** Enable a scrolling nebular cloud effect. Default: `false` */
38
+ nebular?: boolean;
39
+ }
40
+
41
+ /** Configuration for the particle burst on paddle hits. */
42
+ export interface ParticleConfig {
43
+ /** Number of particles spawned per hit. Default: `20` */
44
+ particlesCount?: number;
45
+ }
46
+
47
+ /**
48
+ * Full configuration for `PongGame`.
49
+ *
50
+ * All fields are optional — any omitted value falls back to its built-in default.
51
+ * Options passed here are deep-merged with the defaults and with any settings
52
+ * the player has previously saved through the in-game settings panel.
53
+ */
54
+ export interface PongGameConfig {
55
+ /**
56
+ * Automatically size the canvas to fill its container element at startup.
57
+ * When `true`, `canvasWidth` and `canvasHeight` are ignored.
58
+ * Default: `false`
59
+ */
60
+ autoSize?: boolean;
61
+ /**
62
+ * Automatically resize the canvas whenever the container element changes size.
63
+ * Uses a `ResizeObserver` — no manual `resizeGame()` calls needed.
64
+ * Combine with `autoSize: true` to fill the container on startup as well.
65
+ * Default: `false`
66
+ */
67
+ autoResize?: boolean;
68
+ /**
69
+ * Canvas width in pixels. Ignored when `autoSize` is `true`.
70
+ * Default: `400`
71
+ */
72
+ canvasWidth?: number;
73
+ /**
74
+ * Canvas height in pixels. Ignored when `autoSize` is `true`.
75
+ * Default: `300`
76
+ */
77
+ canvasHeight?: number;
78
+ /** Paddle width in pixels. Default: `70` */
79
+ paddleWidth?: number;
80
+ /** Paddle height in pixels. Default: `10` */
81
+ paddleHeight?: number;
82
+ /** Ball diameter in pixels. Default: `7` */
83
+ ballDiameter?: number;
84
+ /** Starting ball speed in pixels per frame. Default: `3` */
85
+ ballSpeed?: number;
86
+ /** Distance the paddle moves per frame while an arrow key is held. Default: `4` */
87
+ paddleMoveStep?: number;
88
+ /** Countdown timer settings. */
89
+ timer?: TimerConfig;
90
+ /** Colors for the game elements. */
91
+ colors?: ColorConfig;
92
+ /** Animated background effects. */
93
+ animatedBackground?: AnimatedBackgroundConfig;
94
+ /**
95
+ * Starting AI difficulty level.
96
+ * Higher values increase both ball speed and AI prediction precision.
97
+ * Default: `1`
98
+ */
99
+ difficultyLevel?: number;
100
+ /** Emit particle bursts when the ball hits a paddle. Default: `true` */
101
+ particleBounce?: boolean;
102
+ /** Particle burst effect settings. */
103
+ particleConfig?: ParticleConfig;
104
+ /** Enable in-game sound effects. Default: `false` */
105
+ hasSound?: boolean;
106
+ /** Called whenever the player toggles sound on or off via the in-game icon. */
107
+ onSoundToggle?: (enabled: boolean) => void;
108
+ }
109
+
110
+ /**
111
+ * A retro Pong game rendered on an HTML canvas.
112
+ *
113
+ * @example
114
+ * ```html
115
+ * <div id="game"></div>
116
+ * ```
117
+ * ```js
118
+ * import { PongGame } from 'retro-pong-game';
119
+ *
120
+ * const game = new PongGame(
121
+ * {
122
+ * canvasWidth: 600,
123
+ * canvasHeight: 400,
124
+ * hasSound: true,
125
+ * colors: { background: '#1a1a2e', paddle: '#e94560', ball: '#e94560' },
126
+ * },
127
+ * '#game'
128
+ * );
129
+ *
130
+ * // Clean up when done:
131
+ * game.destroy();
132
+ * ```
133
+ */
134
+ export declare class PongGame {
135
+ /**
136
+ * Create and immediately start a new Pong game.
137
+ *
138
+ * A `<canvas>` element is appended to the container matched by `selector`.
139
+ * Initialization is asynchronous — the canvas and first frame appear after
140
+ * a microtask.
141
+ *
142
+ * @param config - Game options. Deep-merged with built-in defaults and any
143
+ * settings previously saved to `localStorage` via the in-game settings panel.
144
+ * @param selector - CSS selector for the host container element,
145
+ * e.g. `"#game"` or `".pong-wrapper"`.
146
+ */
147
+ constructor(config: PongGameConfig, selector: string);
148
+
149
+ /**
150
+ * Return the internal `SoundHelper` instance for advanced audio control.
151
+ * For basic muting, prefer `toggleSound()`.
152
+ */
153
+ getSoundHelper(): unknown;
154
+
155
+ /** Return `true` if sound effects are currently enabled. */
156
+ hasSound(): boolean;
157
+
158
+ /**
159
+ * Enable or disable all sound effects.
160
+ * Also fires the `onSoundToggle` callback defined in the config.
161
+ *
162
+ * @param enabled - `true` to unmute, `false` to mute.
163
+ */
164
+ toggleSound(enabled: boolean): void;
165
+
166
+ /**
167
+ * Increase the AI difficulty and ball speed by one step.
168
+ * This is also triggered automatically when the player scores 3 points in a row.
169
+ */
170
+ increaseDifficulty(): void;
171
+
172
+ /**
173
+ * Decrease the AI difficulty and ball speed by one step.
174
+ * This is also triggered automatically when the AI scores 3 points in a row.
175
+ */
176
+ decreaseDifficulty(): void;
177
+
178
+ /**
179
+ * Resize the canvas and all game elements to fit the current container size.
180
+ * Call this whenever the browser window or container element changes dimensions.
181
+ *
182
+ * @example
183
+ * ```js
184
+ * window.addEventListener('resize', () => game.resizeGame());
185
+ * ```
186
+ */
187
+ resizeGame(): void;
188
+
189
+ /**
190
+ * Fully tear down the game instance: stops the render loop, removes all
191
+ * event listeners, and removes the canvas from the DOM.
192
+ *
193
+ * Always call this before unmounting the page or component that hosts the game.
194
+ */
195
+ destroy(): void;
196
+ }