pacman-contribution-graph 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/src/canvas.ts ADDED
@@ -0,0 +1,200 @@
1
+ import { CELL_SIZE, GAP_SIZE, GRID_HEIGHT, GRID_WIDTH, PACMAN_COLOR, PACMAN_COLOR_DEAD, PACMAN_COLOR_POWERUP } from './constants';
2
+ import { MusicPlayer } from './music-player';
3
+ import { Store } from './store';
4
+ import { Utils } from './utils';
5
+
6
+ const resizeCanvas = () => {
7
+ const canvasWidth = GRID_WIDTH * (CELL_SIZE + GAP_SIZE);
8
+ const canvasHeight = GRID_HEIGHT * (CELL_SIZE + GAP_SIZE) + 20; // Adding some space for months on top
9
+
10
+ Store.config.canvas.width = canvasWidth;
11
+ Store.config.canvas.height = canvasHeight;
12
+ };
13
+
14
+ const drawGrid = () => {
15
+ Store.config.canvas.getContext('2d')!.fillStyle = Utils.getCurrentTheme().gridBackground;
16
+ Store.config.canvas.getContext('2d')!.fillRect(0, 0, Store.config.canvas.width, Store.config.canvas.height);
17
+
18
+ for (let x = 0; x < GRID_HEIGHT; x++) {
19
+ for (let y = 0; y < GRID_WIDTH; y++) {
20
+ const intensity = Store.grid[x][y];
21
+ if (intensity > 0) {
22
+ const adjustedIntensity = intensity < 0.2 ? 0.3 : intensity;
23
+ const color = Utils.hexToRGBA(Utils.getCurrentTheme().contributionBoxColor, adjustedIntensity);
24
+ Store.config.canvas.getContext('2d')!.fillStyle = color;
25
+ } else {
26
+ Store.config.canvas.getContext('2d')!.fillStyle = Utils.getCurrentTheme().emptyContributionBoxColor;
27
+ }
28
+ Store.config.canvas.getContext('2d')!.beginPath();
29
+ Store.config.canvas
30
+ .getContext('2d')!
31
+ .roundRect(y * (CELL_SIZE + GAP_SIZE), x * (CELL_SIZE + GAP_SIZE) + 15, CELL_SIZE, CELL_SIZE, 5);
32
+ Store.config.canvas.getContext('2d')!.fill();
33
+ }
34
+ }
35
+
36
+ Store.config.canvas.getContext('2d')!.fillStyle = Utils.getCurrentTheme().textColor;
37
+ Store.config.canvas.getContext('2d')!.font = '10px Arial';
38
+ Store.config.canvas.getContext('2d')!.textAlign = 'center';
39
+
40
+ let lastMonth = '';
41
+ for (let y = 0; y < GRID_WIDTH; y++) {
42
+ if (Store.monthLabels[y] !== lastMonth) {
43
+ const xPos = y * (CELL_SIZE + GAP_SIZE) + CELL_SIZE / 2;
44
+ Store.config.canvas.getContext('2d')!.fillText(Store.monthLabels[y], xPos, 10);
45
+ lastMonth = Store.monthLabels[y];
46
+ }
47
+ }
48
+ };
49
+
50
+ const drawPacman = () => {
51
+ const x = Store.pacman.y * (CELL_SIZE + GAP_SIZE) + CELL_SIZE / 2;
52
+ const y = Store.pacman.x * (CELL_SIZE + GAP_SIZE) + CELL_SIZE / 2 + 15;
53
+ const radius = CELL_SIZE / 2;
54
+
55
+ // Change Pac-Man's color to red if he's on power-up, dead, else yellow
56
+ if (Store.pacman.deadReaminingDuration) {
57
+ Store.config.canvas.getContext('2d')!.fillStyle = PACMAN_COLOR_DEAD;
58
+ } else if (Store.pacman.powerupReaminingDuration) {
59
+ Store.config.canvas.getContext('2d')!.fillStyle = PACMAN_COLOR_POWERUP;
60
+ } else {
61
+ Store.config.canvas.getContext('2d')!.fillStyle = PACMAN_COLOR;
62
+ }
63
+
64
+ const mouthAngle = Store.pacmanMouthOpen ? 0.35 * Math.PI : 0.1 * Math.PI;
65
+
66
+ let startAngle, endAngle;
67
+ switch (Store.pacman.direction) {
68
+ case 'up':
69
+ startAngle = 1.5 * Math.PI + mouthAngle;
70
+ endAngle = 1.5 * Math.PI - mouthAngle;
71
+ break;
72
+ case 'down':
73
+ startAngle = 0.5 * Math.PI + mouthAngle;
74
+ endAngle = 0.5 * Math.PI - mouthAngle;
75
+ break;
76
+ case 'left':
77
+ startAngle = Math.PI + mouthAngle;
78
+ endAngle = Math.PI - mouthAngle;
79
+ break;
80
+ case 'right':
81
+ default:
82
+ startAngle = 0 + mouthAngle;
83
+ endAngle = 2 * Math.PI - mouthAngle;
84
+ break;
85
+ }
86
+
87
+ Store.config.canvas.getContext('2d')!.beginPath();
88
+ Store.config.canvas.getContext('2d')!.arc(x, y, radius, startAngle, endAngle);
89
+ Store.config.canvas.getContext('2d')!.lineTo(x, y);
90
+ Store.config.canvas.getContext('2d')!.fill();
91
+ };
92
+
93
+ const drawGhosts = () => {
94
+ Store.ghosts.forEach((ghost) => {
95
+ const x = ghost.y * (CELL_SIZE + GAP_SIZE) + CELL_SIZE / 2;
96
+ const y = ghost.x * (CELL_SIZE + GAP_SIZE) + CELL_SIZE / 2 + 15;
97
+ const radius = CELL_SIZE / 2;
98
+
99
+ Store.config.canvas.getContext('2d')!.fillStyle = ghost.scared ? 'blue' : ghost.color;
100
+ Store.config.canvas.getContext('2d')!.beginPath();
101
+ Store.config.canvas.getContext('2d')!.arc(x, y, radius, 0, Math.PI);
102
+ Store.config.canvas.getContext('2d')!.rect(x - radius, y, radius * 2, radius);
103
+ Store.config.canvas.getContext('2d')!.fill();
104
+
105
+ Store.config.canvas.getContext('2d')!.fillStyle = 'white';
106
+ Store.config.canvas.getContext('2d')!.beginPath();
107
+ Store.config.canvas.getContext('2d')!.arc(x - radius / 3, y - radius / 3, radius / 4, 0, Math.PI * 2);
108
+ Store.config.canvas.getContext('2d')!.arc(x + radius / 3, y - radius / 3, radius / 4, 0, Math.PI * 2);
109
+ Store.config.canvas.getContext('2d')!.fill();
110
+
111
+ Store.config.canvas.getContext('2d')!.fillStyle = 'black';
112
+ Store.config.canvas.getContext('2d')!.beginPath();
113
+ Store.config.canvas.getContext('2d')!.arc(x - radius / 3, y - radius / 3, radius / 8, 0, Math.PI * 2);
114
+ Store.config.canvas.getContext('2d')!.arc(x + radius / 3, y - radius / 3, radius / 8, 0, Math.PI * 2);
115
+ Store.config.canvas.getContext('2d')!.fill();
116
+ });
117
+ };
118
+
119
+ const renderGameOver = () => {
120
+ Store.config.canvas.getContext('2d')!.fillStyle = 'black';
121
+ Store.config.canvas.getContext('2d')!.font = '20px Arial';
122
+ Store.config.canvas.getContext('2d')!.textAlign = 'center';
123
+ Store.config.canvas.getContext('2d')!.fillText('Game Over', Store.config.canvas.width / 2, Store.config.canvas.height / 2);
124
+ };
125
+
126
+ const drawSoundController = () => {
127
+ if (!Store.config.enableSounds) {
128
+ console.log('vvvv');
129
+ return;
130
+ }
131
+
132
+ const width = 30,
133
+ height = 30,
134
+ left = Store.config.canvas.width - width - 10,
135
+ top = 10;
136
+ Store.config.canvas.getContext('2d')!.fillStyle = `rgba(0, 0, 0, ${MusicPlayer.getInstance().isMuted ? 0.3 : 0.5})`;
137
+ Store.config.canvas.getContext('2d')!.beginPath();
138
+ Store.config.canvas.getContext('2d')!.moveTo(left + 10, top + 10);
139
+ Store.config.canvas.getContext('2d')!.lineTo(left + 20, top + 5);
140
+ Store.config.canvas.getContext('2d')!.lineTo(left + 20, top + 25);
141
+ Store.config.canvas.getContext('2d')!.lineTo(left + 10, top + 20);
142
+ Store.config.canvas.getContext('2d')!.closePath();
143
+ Store.config.canvas.getContext('2d')!.fill();
144
+
145
+ if (!MusicPlayer.getInstance().isMuted) {
146
+ Store.config.canvas.getContext('2d')!.strokeStyle = `rgba(0, 0, 0, 0.4)`;
147
+ Store.config.canvas.getContext('2d')!.lineWidth = 2;
148
+
149
+ // First wave
150
+ Store.config.canvas.getContext('2d')!.beginPath();
151
+ Store.config.canvas.getContext('2d')!.arc(left + 25, top + 15, 5, 0, Math.PI * 2);
152
+ Store.config.canvas.getContext('2d')!.stroke();
153
+
154
+ // Second wave
155
+ Store.config.canvas.getContext('2d')!.beginPath();
156
+ Store.config.canvas.getContext('2d')!.arc(left + 25, top + 15, 8, 0, Math.PI * 2);
157
+ Store.config.canvas.getContext('2d')!.stroke();
158
+ } else {
159
+ // Mute line
160
+ Store.config.canvas.getContext('2d')!.strokeStyle = 'rgba(255, 0, 0, 0.6)';
161
+ Store.config.canvas.getContext('2d')!.lineWidth = 3;
162
+ Store.config.canvas.getContext('2d')!.beginPath();
163
+ Store.config.canvas.getContext('2d')!.moveTo(left + 25, top + 5);
164
+ Store.config.canvas.getContext('2d')!.lineTo(left + 5, top + 25);
165
+ Store.config.canvas.getContext('2d')!.stroke();
166
+ }
167
+ };
168
+
169
+ const listenToSoundController = () => {
170
+ if (!Store.config.enableSounds) {
171
+ return;
172
+ }
173
+ Store.config.canvas.addEventListener('click', function (event) {
174
+ const rect = Store.config.canvas.getBoundingClientRect();
175
+ const x = event.clientX - rect.left,
176
+ y = event.clientY - rect.top;
177
+ const width = 30,
178
+ height = 30,
179
+ left = Store.config.canvas.width - width - 10,
180
+ top = 10;
181
+
182
+ if (x >= left && x <= left + this.width && y >= top && y <= top + this.height) {
183
+ if (MusicPlayer.getInstance().isMuted) {
184
+ MusicPlayer.getInstance().unmute();
185
+ } else {
186
+ MusicPlayer.getInstance().mute();
187
+ }
188
+ }
189
+ });
190
+ };
191
+
192
+ export const Canvas = {
193
+ resizeCanvas,
194
+ drawGrid,
195
+ drawPacman,
196
+ drawGhosts,
197
+ renderGameOver,
198
+ drawSoundController,
199
+ listenToSoundController
200
+ };
@@ -0,0 +1,40 @@
1
+ import { GameTheme, ThemeKeys } from './types';
2
+
3
+ export const CELL_SIZE = 20;
4
+ export const GAP_SIZE = 2;
5
+ export const GRID_WIDTH = 52;
6
+ export const GRID_HEIGHT = 7;
7
+ export const PACMAN_COLOR = 'yellow';
8
+ export const PACMAN_COLOR_POWERUP = 'red';
9
+ export const PACMAN_COLOR_DEAD = '#80808064';
10
+ export const GHOST_COLORS = ['red', 'pink', 'cyan', 'orange'];
11
+ export const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
12
+ export const DELTA_TIME = 250;
13
+ export const PACMAN_DEATH_DURATION = 10;
14
+ export const PACMAN_POWERUP_DURATION = 15;
15
+ export const GAME_THEMES: { [key in ThemeKeys]: GameTheme } = {
16
+ github: {
17
+ textColor: '#586069',
18
+ gridBackground: '#ffffff',
19
+ contributionBoxColor: '#9be9a8',
20
+ emptyContributionBoxColor: '#ebedf0'
21
+ },
22
+ 'github-dark': {
23
+ textColor: '#8b949e',
24
+ gridBackground: '#0d1117',
25
+ contributionBoxColor: '#26a641',
26
+ emptyContributionBoxColor: '#161b22'
27
+ },
28
+ gitlab: {
29
+ textColor: '#626167',
30
+ gridBackground: '#ffffff',
31
+ contributionBoxColor: '#7992f5',
32
+ emptyContributionBoxColor: '#ececef'
33
+ },
34
+ 'gitlab-dark': {
35
+ textColor: '#999999',
36
+ gridBackground: '#1f1f1f',
37
+ contributionBoxColor: '#2e7db1',
38
+ emptyContributionBoxColor: '#2d2d2d'
39
+ }
40
+ };
package/src/game.ts ADDED
@@ -0,0 +1,344 @@
1
+ import { Canvas } from './canvas';
2
+ import { DELTA_TIME, GHOST_COLORS, GRID_HEIGHT, GRID_WIDTH, MONTHS, PACMAN_DEATH_DURATION, PACMAN_POWERUP_DURATION } from './constants';
3
+ import { MusicPlayer, Sound } from './music-player';
4
+ import { Store } from './store';
5
+ import { SVG } from './svg';
6
+
7
+ const initializeGrid = () => {
8
+ Store.grid = Array.from({ length: GRID_HEIGHT }, () => Array.from({ length: GRID_WIDTH }, () => 0));
9
+ Store.monthLabels = Array(GRID_WIDTH).fill('');
10
+ let maxCommits = 1;
11
+
12
+ const now = new Date();
13
+ const startOfCurrentWeek = new Date(now);
14
+ startOfCurrentWeek.setDate(now.getDate() - now.getDay());
15
+
16
+ Store.contributions.forEach((contribution) => {
17
+ const contributionDate = new Date(contribution.date);
18
+ const dayOfWeek = contributionDate.getDay();
19
+ const weeksAgo = Math.floor((+startOfCurrentWeek - +contributionDate) / (1000 * 60 * 60 * 24 * 7));
20
+
21
+ if (weeksAgo >= 0 && weeksAgo < GRID_WIDTH && dayOfWeek >= 0 && dayOfWeek < GRID_HEIGHT) {
22
+ Store.grid[dayOfWeek][GRID_WIDTH - 1 - weeksAgo] = contribution.count;
23
+ if (contribution.count > maxCommits) maxCommits = contribution.count;
24
+ }
25
+ });
26
+
27
+ for (let x = 0; x < GRID_HEIGHT; x++) {
28
+ for (let y = 0; y < GRID_WIDTH; y++) {
29
+ if (Store.grid[x][y] > 0) {
30
+ Store.grid[x][y] = Store.grid[x][y] / maxCommits;
31
+ }
32
+ }
33
+ }
34
+
35
+ for (let y = 0; y < GRID_WIDTH; y++) {
36
+ const weeksAgo = GRID_WIDTH - 1 - y;
37
+ const columnDate = new Date(startOfCurrentWeek);
38
+ columnDate.setDate(columnDate.getDate() - weeksAgo * 7);
39
+ Store.monthLabels[y] = MONTHS[columnDate.getMonth()];
40
+ }
41
+ };
42
+
43
+ const placePacman = () => {
44
+ let validCells = [];
45
+ for (let x = 0; x < GRID_HEIGHT; x++) {
46
+ for (let y = 0; y < GRID_WIDTH; y++) {
47
+ if (Store.grid[x][y] > 0) validCells.push({ x, y });
48
+ }
49
+ }
50
+ if (validCells.length > 0) {
51
+ const randomCell = validCells[Math.floor(Math.random() * validCells.length)];
52
+ Store.pacman = {
53
+ x: randomCell.x,
54
+ y: randomCell.y,
55
+ direction: 'right',
56
+ points: 0,
57
+ deadReaminingDuration: 0,
58
+ powerupReaminingDuration: 0
59
+ };
60
+ }
61
+ if (Store.config.outputFormat == 'canvas') Canvas.drawPacman();
62
+ };
63
+
64
+ const placeGhosts = () => {
65
+ Store.ghosts = [];
66
+ Store.scaredGhostsDestinations = [];
67
+ // Create 4 ghosts
68
+ for (let i = 0; i < 4; i++) {
69
+ const color = GHOST_COLORS[i % GHOST_COLORS.length];
70
+ let x, y;
71
+ do {
72
+ x = Math.floor(Math.random() * GRID_HEIGHT);
73
+ y = Math.floor(Math.random() * GRID_WIDTH);
74
+ } while (Store.grid[x][y] === 0);
75
+ Store.ghosts.push({ x, y, color, scared: false, target: undefined });
76
+ Store.scaredGhostsDestinations.push({ x: 0, y: 0 });
77
+ }
78
+ if (Store.config.outputFormat == 'canvas') Canvas.drawGhosts();
79
+ };
80
+
81
+ const startGame = async () => {
82
+ if (Store.config.outputFormat == 'canvas') {
83
+ Store.config.canvas = Store.config.canvas;
84
+ Canvas.resizeCanvas();
85
+ Canvas.listenToSoundController();
86
+ }
87
+
88
+ Store.frameCount = 0;
89
+ Store.ghosts.forEach((ghost) => (ghost.scared = false));
90
+
91
+ initializeGrid();
92
+ if (Store.config.outputFormat == 'canvas') Canvas.drawGrid();
93
+
94
+ if (Store.config.outputFormat == 'canvas') {
95
+ if (!Store.config.enableSounds) {
96
+ MusicPlayer.getInstance().mute();
97
+ }
98
+ await MusicPlayer.getInstance().preloadSounds();
99
+ MusicPlayer.getInstance().startDefaultSound();
100
+ await MusicPlayer.getInstance().play(Sound.BEGINNING);
101
+ }
102
+
103
+ placePacman();
104
+ placeGhosts();
105
+
106
+ if (Store.config.outputFormat == 'svg') {
107
+ const remainingCells = () => Store.grid.some((row) => row.some((cell) => cell > 0));
108
+ while (remainingCells()) {
109
+ await updateGame();
110
+ }
111
+ // One more time to generate svg
112
+ await updateGame();
113
+ } else {
114
+ clearInterval(Store.gameInterval);
115
+ Store.gameInterval = setInterval(async () => await updateGame(), DELTA_TIME);
116
+ }
117
+ };
118
+
119
+ const updateGame = async () => {
120
+ Store.frameCount++;
121
+ if (Store.frameCount % Store.config.gameSpeed !== 0) {
122
+ Store.gameHistory.push({
123
+ pacman: { ...Store.pacman },
124
+ ghosts: Store.ghosts.map((ghost) => ({ ...ghost })),
125
+ grid: Store.grid.map((row) => [...row])
126
+ });
127
+ return;
128
+ }
129
+
130
+ if (Store.pacman.deadReaminingDuration) {
131
+ Store.pacman.deadReaminingDuration--;
132
+ if (!Store.pacman.deadReaminingDuration) {
133
+ // IT'S ALIVE!
134
+ if (Store.config.outputFormat == 'canvas')
135
+ MusicPlayer.getInstance()
136
+ .play(Sound.GAME_OVER)
137
+ .then(() => MusicPlayer.getInstance().startDefaultSound());
138
+ }
139
+ }
140
+
141
+ if (Store.pacman.powerupReaminingDuration) {
142
+ Store.pacman.powerupReaminingDuration--;
143
+ if (!Store.pacman.powerupReaminingDuration) {
144
+ Store.ghosts.forEach((ghost) => (ghost.scared = false));
145
+ Store.pacman.points = 0;
146
+ }
147
+ }
148
+
149
+ const remainingCells = Store.grid.some((row) => row.some((cell) => cell > 0));
150
+ if (!remainingCells) {
151
+ if (Store.config.outputFormat == 'canvas') {
152
+ clearInterval(Store.gameInterval);
153
+ if (Store.config.outputFormat == 'canvas') {
154
+ Canvas.renderGameOver();
155
+ MusicPlayer.getInstance()
156
+ .play(Sound.BEGINNING)
157
+ .then(() => MusicPlayer.getInstance().stopDefaultSound());
158
+ }
159
+ }
160
+
161
+ if (Store.config.outputFormat == 'svg') {
162
+ const animatedSVG = SVG.generateAnimatedSVG();
163
+ const svgBlob = new Blob([animatedSVG], {
164
+ type: 'image/svg+xml;charset=utf-8'
165
+ });
166
+ const svgUrl = URL.createObjectURL(svgBlob);
167
+ Store.config.svgCallback(svgUrl);
168
+ }
169
+
170
+ Store.config.gameOverCallback();
171
+ return;
172
+ }
173
+
174
+ movePacman();
175
+ moveGhosts();
176
+ checkCollisions();
177
+
178
+ Store.pacmanMouthOpen = !Store.pacmanMouthOpen;
179
+
180
+ Store.gameHistory.push({
181
+ pacman: { ...Store.pacman },
182
+ ghosts: Store.ghosts.map((ghost) => ({ ...ghost })),
183
+ grid: Store.grid.map((row) => [...row])
184
+ });
185
+
186
+ if (Store.config.outputFormat == 'canvas') Canvas.drawGrid();
187
+ if (Store.config.outputFormat == 'canvas') Canvas.drawPacman();
188
+ if (Store.config.outputFormat == 'canvas') Canvas.drawGhosts();
189
+ if (Store.config.outputFormat == 'canvas') Canvas.drawSoundController();
190
+ };
191
+
192
+ const movePacman = () => {
193
+ if (Store.pacman.deadReaminingDuration) {
194
+ return;
195
+ }
196
+ let targetCells: { x: number; y: number; distance: number }[] = [];
197
+
198
+ if (Store.pacman.powerupReaminingDuration) {
199
+ targetCells = Store.ghosts.map((ghost) => ({
200
+ x: ghost.x,
201
+ y: ghost.y,
202
+ distance: Infinity
203
+ }));
204
+ } else {
205
+ for (let x = 0; x < GRID_HEIGHT; x++) {
206
+ for (let y = 0; y < GRID_WIDTH; y++) {
207
+ if (Store.grid[x][y] > 0) targetCells.push({ x, y, distance: Infinity });
208
+ }
209
+ }
210
+ }
211
+
212
+ if (targetCells.length === 0) return;
213
+
214
+ const closest = targetCells.reduce(
215
+ (closest, cell) => {
216
+ const distance = Math.abs(cell.x - Store.pacman.x) + Math.abs(cell.y - Store.pacman.y);
217
+ return distance < closest.distance ? { ...cell, distance } : closest;
218
+ },
219
+ { x: Store.pacman.x, y: Store.pacman.y, distance: Infinity }
220
+ );
221
+
222
+ const dx = closest.x - Store.pacman.x;
223
+ const dy = closest.y - Store.pacman.y;
224
+
225
+ if (Math.abs(dx) > Math.abs(dy)) {
226
+ Store.pacman.x += Math.sign(dx);
227
+ Store.pacman.direction = dx > 0 ? 'down' : 'up';
228
+ } else {
229
+ Store.pacman.y += Math.sign(dy);
230
+ Store.pacman.direction = dy > 0 ? 'right' : 'left';
231
+ }
232
+
233
+ if (Store.grid[Store.pacman.x][Store.pacman.y] > 0) {
234
+ Store.pacman.points += 1;
235
+ Store.grid[Store.pacman.x][Store.pacman.y] = 0;
236
+
237
+ if (Store.pacman.points >= 30) activatePowerUp();
238
+ }
239
+ };
240
+
241
+ const moveGhosts = () => {
242
+ Store.ghosts.forEach((ghost, index) => {
243
+ if (ghost.scared) {
244
+ if (!ghost.target) {
245
+ ghost.target = getRandomDestination(ghost.x, ghost.y);
246
+ }
247
+
248
+ const dx = ghost.target.x - ghost.x;
249
+ const dy = ghost.target.y - ghost.y;
250
+ const moveX = Math.abs(dx) > Math.abs(dy) ? Math.sign(dx) : 0;
251
+ const moveY = Math.abs(dy) >= Math.abs(dx) ? Math.sign(dy) : 0;
252
+
253
+ const newX = ghost.x + moveX;
254
+ const newY = ghost.y + moveY;
255
+
256
+ if (newX >= 0 && newX < GRID_HEIGHT && newY >= 0 && newY < GRID_WIDTH) {
257
+ ghost.x = newX;
258
+ ghost.y = newY;
259
+ }
260
+
261
+ if (ghost.x === ghost.target.x && ghost.y === ghost.target.y) {
262
+ ghost.target = getRandomDestination(ghost.x, ghost.y);
263
+ }
264
+ } else {
265
+ const directions = [
266
+ [-1, 0],
267
+ [1, 0],
268
+ [0, -1],
269
+ [0, 1]
270
+ ];
271
+ const [dx, dy] = directions[Math.floor(Math.random() * directions.length)];
272
+
273
+ // If Pacman has the power-up, ghosts move slower (move every other frame)
274
+ if (Store.pacman.powerupReaminingDuration && Math.random() < 0.5) return;
275
+
276
+ const newX = ghost.x + dx;
277
+ const newY = ghost.y + dy;
278
+
279
+ if (newX >= 0 && newX < GRID_HEIGHT && newY >= 0 && newY < GRID_WIDTH) {
280
+ ghost.x = newX;
281
+ ghost.y = newY;
282
+ }
283
+ }
284
+ });
285
+ };
286
+
287
+ const getRandomDestination = (x: number, y: number) => {
288
+ const maxDistance = 10;
289
+ const randomX = x + Math.floor(Math.random() * (2 * maxDistance + 1)) - maxDistance;
290
+ const randomY = y + Math.floor(Math.random() * (2 * maxDistance + 1)) - maxDistance;
291
+ return {
292
+ x: Math.max(0, Math.min(randomX, GRID_HEIGHT - 1)),
293
+ y: Math.max(0, Math.min(randomY, GRID_WIDTH - 1))
294
+ };
295
+ };
296
+
297
+ const checkCollisions = () => {
298
+ if (Store.pacman.deadReaminingDuration) return;
299
+
300
+ Store.ghosts.forEach((ghost, index) => {
301
+ if (ghost.x === Store.pacman.x && ghost.y === Store.pacman.y) {
302
+ if (Store.pacman.powerupReaminingDuration && ghost.scared) {
303
+ respawnGhost(index);
304
+ Store.pacman.points += 10;
305
+ if (Store.config.outputFormat == 'canvas') {
306
+ MusicPlayer.getInstance().play(Sound.EAT_GHOST);
307
+ }
308
+ } else {
309
+ Store.pacman.points = 0;
310
+ Store.pacman.powerupReaminingDuration = 0;
311
+ Store.pacman.deadReaminingDuration = PACMAN_DEATH_DURATION;
312
+ if (Store.config.outputFormat == 'canvas') {
313
+ MusicPlayer.getInstance()
314
+ .play(Sound.GAME_OVER)
315
+ .then(() => MusicPlayer.getInstance().stopDefaultSound());
316
+ }
317
+ }
318
+ }
319
+ });
320
+ };
321
+
322
+ const respawnGhost = (ghostIndex: number) => {
323
+ let x, y;
324
+ do {
325
+ x = Math.floor(Math.random() * GRID_HEIGHT);
326
+ y = Math.floor(Math.random() * GRID_WIDTH);
327
+ } while ((Math.abs(x - Store.pacman.x) <= 2 && Math.abs(y - Store.pacman.y) <= 2) || Store.grid[x][y] === 0);
328
+ Store.ghosts[ghostIndex] = {
329
+ x,
330
+ y,
331
+ color: GHOST_COLORS[ghostIndex % GHOST_COLORS.length],
332
+ scared: false,
333
+ target: undefined
334
+ };
335
+ };
336
+
337
+ const activatePowerUp = () => {
338
+ Store.pacman.powerupReaminingDuration = PACMAN_POWERUP_DURATION;
339
+ Store.ghosts.forEach((ghost) => (ghost.scared = true));
340
+ };
341
+
342
+ export const Game = {
343
+ startGame
344
+ };
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { Game } from './game';
2
+ import { Store } from './store';
3
+ import { Config } from './types';
4
+ import { Utils } from './utils';
5
+
6
+ export const renderContributions = async (conf: Config) => {
7
+ const defaultConfing: Config = {
8
+ platform: 'github',
9
+ username: '',
10
+ canvas: undefined as unknown as HTMLCanvasElement,
11
+ outputFormat: 'svg',
12
+ svgCallback: (_: string) => {},
13
+ gameOverCallback: () => () => {},
14
+ gameTheme: 'github',
15
+ gameSpeed: 1,
16
+ enableSounds: true
17
+ };
18
+ Store.config = { ...defaultConfing, ...conf };
19
+
20
+ switch (conf.platform) {
21
+ case 'gitlab':
22
+ Store.contributions = await Utils.getGitlabContribution(conf.username);
23
+ break;
24
+
25
+ case 'github':
26
+ Store.contributions = await Utils.getGithubContribution(conf.username);
27
+ break;
28
+ }
29
+
30
+ Game.startGame();
31
+ };