zeroed-some/sprong / 5fd18f3

Browse files

one player mode with enemy 'AI'

Authored by espadonne
SHA
5fd18f301bf131e67488f112aa63b5473b9c4b43
Parents
0fb0b91
Tree
6472e93

1 changed file

StatusFile+-
M sprong.js 413 50
sprong.jsmodified
@@ -20,6 +20,27 @@ let rightSupport, rightPaddle, rightSpring;
2020
 let leftScore = 0;
2121
 let rightScore = 0;
2222
 let gameStarted = false;
23
+let gameState = 'menu'; // 'menu', 'playing', 'paused'
24
+let gameMode = 'vs-cpu'; // 'vs-cpu' or 'vs-human'
25
+let aiEnabled = true;
26
+
27
+// Menu state
28
+let menuState = {
29
+    selectedOption: 0, // 0 = 1 Player, 1 = 2 Player
30
+    options: ['1 Player vs CPU', '2 Player'],
31
+    difficultySelected: 1, // 0 = Easy, 1 = Medium, 2 = Hard
32
+    difficulties: ['Easy', 'Medium', 'Hard'],
33
+    showDifficulty: true
34
+};
35
+
36
+// AI system
37
+let aiState = {
38
+    targetY: 200,
39
+    reactionDelay: 0,
40
+    difficulty: 'medium', // 'easy', 'medium', 'hard'
41
+    lastBallX: 0,
42
+    lastUpdateTime: 0
43
+};
2344
 
2445
 // Player input
2546
 let keys = {};
@@ -60,11 +81,33 @@ const MOUSE_SPEED_LIMIT = 4; // Max speed for mouse movement
6081
 const MOUSE_LAG_FACTOR  = 0.12; // How much lag in mouse following
6182
 const TOUCH_SENSITIVITY = 1.2;  // Touch movement multiplier
6283
 
63
-// Spring physics constants (tuned for bounciness!)
64
-const PADDLE_MASS       = 0.6;  // Lighter for more bounce
84
+// Spring physics constants (toned down for stability)
85
+const PADDLE_MASS       = 0.8;  // Back to more stable value
6586
 const SPRING_LENGTH     = 40;
66
-const SPRING_DAMPING    = 0.4;  // Much less damping = more bounce!
67
-const SPRING_STIFFNESS  = 0.035; // Higher stiffness = snappier
87
+const SPRING_DAMPING    = 0.6;  // More damping = less erratic
88
+const SPRING_STIFFNESS  = 0.025; // Slightly lower for smoother motion
89
+
90
+// AI difficulty settings
91
+const AI_SETTINGS = {
92
+    easy: {
93
+        reactionTime: 400,    // ms delay
94
+        accuracy: 0.7,        // 70% accuracy
95
+        speed: 0.6,           // 60% of normal speed
96
+        prediction: 0.3       // 30% prediction vs reaction
97
+    },
98
+    medium: {
99
+        reactionTime: 250,
100
+        accuracy: 0.85,
101
+        speed: 0.8,
102
+        prediction: 0.6
103
+    },
104
+    hard: {
105
+        reactionTime: 150,
106
+        accuracy: 0.95,
107
+        speed: 1.0,
108
+        prediction: 0.8
109
+    }
110
+};
68111
 
69112
 // Visual enhancement constants
70113
 const TRAIL_SEGMENTS        = 8;
@@ -154,9 +197,9 @@ function createSpringPaddleSystem(side) {
154197
         // Right paddle (the actual hitting surface)
155198
         rightPaddle = Bodies.rectangle(paddleX, startY, PADDLE_WIDTH, PADDLE_HEIGHT, {
156199
             mass: PADDLE_MASS,
157
-            restitution: 1.3,
200
+            restitution: 1.2,  // Slightly toned down
158201
             friction: 0,
159
-            frictionAir: 0.005
202
+            frictionAir: 0.008 // Bit more air resistance for stability
160203
         });
161204
         
162205
         // Spring constraint connecting support to paddle
@@ -174,34 +217,38 @@ function draw() {
174217
     // Update physics
175218
     Engine.update(engine);
176219
     
177
-    // Handle enhanced player input
178
-    handleEnhancedInput();
179
-    
180
-    // Update particle systems
181
-    updateParticles();
182
-    checkCollisions();
183
-    
184
-    // Check for scoring
185
-    checkBallPosition();
186
-    
187220
     // Clear canvas
188221
     background(10, 10, 10);
189222
     
190
-    // Draw particles behind everything
191
-    drawParticles();
192
-    
193
-    // Draw game objects with enhanced visuals
194
-    drawSpringPaddleSystemsEnhanced();
195
-    drawBallEnhanced();
196
-    drawBoundaries();
197
-    drawCenterLine();
198
-    
199
-    // Draw debug info
200
-    drawDebugInfo();
201
-    
202
-    // Start message
203
-    if (!gameStarted) {
204
-        drawStartMessage();
223
+    if (gameState === 'menu') {
224
+        drawMenu();
225
+    } else {
226
+        // Handle enhanced player input
227
+        handleEnhancedInput();
228
+        
229
+        // Update particle systems
230
+        updateParticles();
231
+        checkCollisions();
232
+        
233
+        // Check for scoring
234
+        checkBallPosition();
235
+        
236
+        // Draw particles behind everything
237
+        drawParticles();
238
+        
239
+        // Draw game objects with enhanced visuals
240
+        drawSpringPaddleSystemsEnhanced();
241
+        drawBallEnhanced();
242
+        drawBoundaries();
243
+        drawCenterLine();
244
+        
245
+        // Draw debug info
246
+        drawDebugInfo();
247
+        
248
+        // Start message
249
+        if (!gameStarted) {
250
+            drawStartMessage();
251
+        }
205252
     }
206253
 }
207254
 
@@ -209,6 +256,11 @@ function handleEnhancedInput() {
209256
     // Handle both keyboard and mouse/touch input
210257
     handleKeyboardInput();
211258
     handleMouseTouchInput();
259
+    
260
+    // Handle AI if enabled
261
+    if (aiEnabled && gameStarted) {
262
+        handleAI();
263
+    }
212264
 }
213265
 
214266
 function handleKeyboardInput() {
@@ -216,24 +268,28 @@ function handleKeyboardInput() {
216268
     let leftInput   = 0;
217269
     let rightInput  = 0;
218270
     
219
-    // Left paddle input (W/S keys)
271
+    // Left paddle input (W/S keys) - always player controlled
220272
     if (keys['w'] || keys['W']) leftInput -= 1;
221273
     if (keys['s'] || keys['S']) leftInput += 1;
222274
     
223
-    // Right paddle input (Arrow keys)
224
-    if (keys['ArrowUp'])    rightInput -= 1;
225
-    if (keys['ArrowDown'])  rightInput += 1;
275
+    // Right paddle input (Arrow keys) - only if AI is disabled
276
+    if (!aiEnabled) {
277
+        if (keys['ArrowUp'])    rightInput -= 1;
278
+        if (keys['ArrowDown'])  rightInput += 1;
279
+    }
226280
     
227281
     // Apply acceleration and smoothing for keyboard
228282
     inputBuffer.left = lerp(inputBuffer.left, leftInput, INPUT_SMOOTHING);
229
-    inputBuffer.right = lerp(inputBuffer.right, rightInput, INPUT_SMOOTHING);
283
+    if (!aiEnabled) {
284
+        inputBuffer.right = lerp(inputBuffer.right, rightInput, INPUT_SMOOTHING);
285
+    }
230286
     
231287
     // Move supports with enhanced physics (only if not using mouse)
232288
     if (!mouseInput.active) {
233289
         if (Math.abs(inputBuffer.left) > 0.01) {
234290
             moveSupportEnhanced(leftSupport, inputBuffer.left * SUPPORT_SPEED);
235291
         }
236
-        if (Math.abs(inputBuffer.right) > 0.01) {
292
+        if (!aiEnabled && Math.abs(inputBuffer.right) > 0.01) {
237293
             moveSupportEnhanced(rightSupport, inputBuffer.right * SUPPORT_SPEED);
238294
         }
239295
     }
@@ -244,6 +300,10 @@ function handleMouseTouchInput() {
244300
     
245301
     // Determine which paddle to control based on mouse X position
246302
     let controllingLeft = mouseX < width / 2;
303
+    
304
+    // Don't allow mouse control of AI paddle
305
+    if (!controllingLeft && aiEnabled) return;
306
+    
247307
     let targetSupport = controllingLeft ? leftSupport : rightSupport;
248308
     
249309
     // Calculate target Y with dead zone
@@ -268,11 +328,59 @@ function handleMouseTouchInput() {
268328
     // Visual feedback - update input buffer for particle effects
269329
     if (controllingLeft) {
270330
         inputBuffer.left = constrain(movement / MOUSE_SPEED_LIMIT, -1, 1);
271
-    } else {
331
+    } else if (!aiEnabled) {
272332
         inputBuffer.right = constrain(movement / MOUSE_SPEED_LIMIT, -1, 1);
273333
     }
274334
 }
275335
 
336
+function handleAI() {
337
+    let currentTime = millis();
338
+    let ballPos = ball.position;
339
+    let ballVel = ball.velocity;
340
+    let aiSettings = AI_SETTINGS[aiState.difficulty];
341
+    
342
+    // Only update AI decision if enough time has passed (reaction delay)
343
+    if (currentTime - aiState.lastUpdateTime > aiSettings.reactionTime) {
344
+        
345
+        // Calculate where ball will be (basic prediction)
346
+        let ballFutureX = ballPos.x + ballVel.x * 30; // Look 30 frames ahead
347
+        let ballFutureY = ballPos.y + ballVel.y * 30;
348
+        
349
+        // Only react if ball is moving toward AI paddle
350
+        if (ballVel.x > 0) {
351
+            // Mix prediction with current position based on AI skill
352
+            let targetY = lerp(ballPos.y, ballFutureY, aiSettings.prediction);
353
+            
354
+            // Add some inaccuracy to make it beatable
355
+            let error = (random() - 0.5) * 50 * (1 - aiSettings.accuracy);
356
+            targetY += error;
357
+            
358
+            // Keep target in bounds
359
+            targetY = constrain(targetY, 80, height - 80);
360
+            
361
+            aiState.targetY = targetY;
362
+            aiState.lastUpdateTime = currentTime;
363
+        }
364
+    }
365
+    
366
+    // Move AI paddle toward target with speed limitation
367
+    let currentY = rightSupport.position.y;
368
+    let deltaY = aiState.targetY - currentY;
369
+    
370
+    if (Math.abs(deltaY) > 5) { // Dead zone
371
+        let movement = deltaY * 0.08 * aiSettings.speed; // Smooth movement
372
+        movement = constrain(movement, -SUPPORT_SPEED * 0.8, SUPPORT_SPEED * 0.8);
373
+        
374
+        moveSupportEnhanced(rightSupport, movement);
375
+        
376
+        // Update input buffer for visual effects
377
+        inputBuffer.right = constrain(movement / (SUPPORT_SPEED * 0.8), -1, 1);
378
+    } else {
379
+        // Gradually reduce input buffer when AI is not moving
380
+        inputBuffer.right *= 0.9;
381
+    }
382
+}
383
+
276384
 function moveSupportEnhanced(support, deltaY) {
277385
     let newY = support.position.y + deltaY;
278386
     
@@ -502,6 +610,9 @@ function drawSinglePaddleEnhanced(paddle, ballDistance) {
502610
     let pos = paddle.position;
503611
     let angle = paddle.angle;
504612
     
613
+    // Check if this is the AI paddle
614
+    let isAI = aiEnabled && paddle === rightPaddle;
615
+    
505616
     // Calculate glow intensity based on ball proximity
506617
     let glowIntensity = map(ballDistance, 0, PADDLE_GLOW_DISTANCE, 150, 0);
507618
     glowIntensity = constrain(glowIntensity, 0, 150);
@@ -510,27 +621,34 @@ function drawSinglePaddleEnhanced(paddle, ballDistance) {
510621
     translate(pos.x, pos.y);
511622
     rotate(angle);
512623
     
624
+    // Different color scheme for AI paddle
625
+    let paddleColor = isAI ? [255, 100, 100] : [0, 255, 136]; // Red for AI, green for player
626
+    
513627
     // Draw enhanced glow effect first
514628
     if (glowIntensity > 0) {
515
-        fill(0, 255, 136, glowIntensity * 0.6);
629
+        fill(paddleColor[0], paddleColor[1], paddleColor[2], glowIntensity * 0.6);
516630
         noStroke();
517631
         rectMode(CENTER);
518632
         rect(0, 0, PADDLE_WIDTH + 12, PADDLE_HEIGHT + 12);
519633
         
520634
         // Add outer glow
521
-        fill(0, 255, 136, glowIntensity * 0.3);
635
+        fill(paddleColor[0], paddleColor[1], paddleColor[2], glowIntensity * 0.3);
522636
         rect(0, 0, PADDLE_WIDTH + 20, PADDLE_HEIGHT + 20);
523637
     }
524638
     
525639
     // Draw main paddle with enhanced visual
526
-    fill(0, 255, 136);
527
-    stroke(0, 255, 136, 220 + glowIntensity * 0.5);
640
+    fill(paddleColor[0], paddleColor[1], paddleColor[2]);
641
+    stroke(paddleColor[0], paddleColor[1], paddleColor[2], 220 + glowIntensity * 0.5);
528642
     strokeWeight(3);
529643
     rectMode(CENTER);
530644
     rect(0, 0, PADDLE_WIDTH, PADDLE_HEIGHT);
531645
     
532646
     // Add core highlight
533
-    fill(150, 255, 200, 100);
647
+    if (isAI) {
648
+        fill(255, 200, 200, 100); // Light red highlight for AI
649
+    } else {
650
+        fill(150, 255, 200, 100); // Light green highlight for player
651
+    }
534652
     noStroke();
535653
     rect(0, 0, PADDLE_WIDTH - 4, PADDLE_HEIGHT - 4);
536654
     
@@ -622,6 +740,7 @@ function drawDebugInfo() {
622740
     text(`FPS: ${Math.round(frameRate())}`, 10, 20);
623741
     text(`Ball Speed: ${Math.round(getBallSpeed())}`, 10, 35);
624742
     text(`Particles: ${particles.length}`, 10, 50);
743
+    text(`Mode: ${aiEnabled ? 'vs CPU' : '2 Player'} | Difficulty: ${aiState.difficulty}`, 10, 65);
625744
     
626745
     // Enhanced spring info
627746
     let leftSpringLength = dist(leftSupport.position.x, leftSupport.position.y, 
@@ -629,26 +748,138 @@ function drawDebugInfo() {
629748
     let rightSpringLength = dist(rightSupport.position.x, rightSupport.position.y, 
630749
                                 rightPaddle.position.x, rightPaddle.position.y);
631750
     
632
-    text(`L Spring: ${Math.round(leftSpringLength)}px (${((SPRING_LENGTH/leftSpringLength - 1) * 100).toFixed(0)}%)`, 10, 65);
633
-    text(`R Spring: ${Math.round(rightSpringLength)}px (${((SPRING_LENGTH/rightSpringLength - 1) * 100).toFixed(0)}%)`, 10, 80);
634
-    text(`Input: L=${inputBuffer.left.toFixed(2)} R=${inputBuffer.right.toFixed(2)}`, 10, 95);
751
+    text(`L Spring: ${Math.round(leftSpringLength)}px (${((SPRING_LENGTH/leftSpringLength - 1) * 100).toFixed(0)}%)`, 10, 80);
752
+    text(`R Spring: ${Math.round(rightSpringLength)}px (${((SPRING_LENGTH/rightSpringLength - 1) * 100).toFixed(0)}%)`, 10, 95);
753
+    text(`Input: L=${inputBuffer.left.toFixed(2)} R=${inputBuffer.right.toFixed(2)}`, 10, 110);
754
+    
755
+    // AI debug info
756
+    if (aiEnabled) {
757
+        text(`AI Target: ${Math.round(aiState.targetY)} | Ball Y: ${Math.round(ball.position.y)}`, 10, 125);
758
+        text(`Ball Vel: X=${ball.velocity.x.toFixed(1)} Y=${ball.velocity.y.toFixed(1)}`, 10, 140);
759
+    }
635760
     
636761
     // Mouse/touch input debug
637762
     if (mouseInput.active) {
638
-        text(`Mouse: ${mouseInput.active ? 'Active' : 'Inactive'} | Side: ${mouseX < width/2 ? 'Left' : 'Right'}`, 10, 110);
639
-        text(`Mouse Y: ${mouseY} | Dead Zone: ${mouseInput.deadZone}px`, 10, 125);
763
+        text(`Mouse: Active | Side: ${mouseX < width/2 ? 'Left' : 'Right'} | Y: ${mouseY}`, 10, 155);
640764
     }
641765
 }
642766
 
767
+function drawMenu() {
768
+    // Draw animated background
769
+    drawMenuBackground();
770
+    
771
+    // Main title
772
+    push();
773
+    let titlePulse = sin(frameCount * 0.05) * 0.2 + 1;
774
+    fill(0, 255, 136);
775
+    textAlign(CENTER);
776
+    textSize(60 * titlePulse);
777
+    text("SPRONG", width/2, 120);
778
+    
779
+    // Subtitle
780
+    fill(0, 255, 136, 150);
781
+    textSize(16);
782
+    text("Physics-based Pong with Spring Paddles", width/2, 150);
783
+    pop();
784
+    
785
+    // Menu options
786
+    let startY = height/2 - 20;
787
+    let spacing = 60;
788
+    
789
+    for (let i = 0; i < menuState.options.length; i++) {
790
+        let y = startY + i * spacing;
791
+        let isSelected = i === menuState.selectedOption;
792
+        
793
+        // Selection indicator
794
+        if (isSelected) {
795
+            push();
796
+            let pulse = sin(frameCount * 0.15) * 0.3 + 1;
797
+            fill(0, 255, 136, 100 * pulse);
798
+            noStroke();
799
+            rectMode(CENTER);
800
+            rect(width/2, y, 300, 45);
801
+            pop();
802
+        }
803
+        
804
+        // Option text
805
+        fill(isSelected ? 255 : 200);
806
+        textAlign(CENTER);
807
+        textSize(isSelected ? 24 : 20);
808
+        text(menuState.options[i], width/2, y + 8);
809
+        
810
+        // Show difficulty selector for 1 Player option
811
+        if (i === 0 && isSelected && menuState.showDifficulty) {
812
+            fill(0, 255, 136, 180);
813
+            textSize(14);
814
+            text(`Difficulty: ${menuState.difficulties[menuState.difficultySelected]}`, width/2, y + 28);
815
+            text("(Use ← → to change)", width/2, y + 45);
816
+        }
817
+    }
818
+    
819
+    // Instructions
820
+    fill(0, 255, 136, 120);
821
+    textAlign(CENTER);
822
+    textSize(14);
823
+    text("Use ↑↓ to select, ENTER to confirm", width/2, height - 80);
824
+    text("or click/touch to select", width/2, height - 60);
825
+    
826
+    // Show controls preview
827
+    textSize(12);
828
+    fill(255, 100);
829
+    if (menuState.selectedOption === 0) {
830
+        text("Controls: W/S keys or Mouse/Touch to move paddle", width/2, height - 30);
831
+    } else {
832
+        text("Controls: Player 1 (W/S) | Player 2 (↑/↓) | Mouse/Touch", width/2, height - 30);
833
+    }
834
+}
835
+
836
+function drawMenuBackground() {
837
+    // Draw subtle animated background elements
838
+    push();
839
+    stroke(0, 255, 136, 30);
840
+    strokeWeight(1);
841
+    
842
+    // Animated grid
843
+    for (let x = 0; x < width; x += 40) {
844
+        let offset = sin(frameCount * 0.01 + x * 0.01) * 5;
845
+        line(x, 0, x, height + offset);
846
+    }
847
+    
848
+    for (let y = 0; y < height; y += 40) {
849
+        let offset = cos(frameCount * 0.01 + y * 0.01) * 5;
850
+        line(0, y, width + offset, y);
851
+    }
852
+    
853
+    // Floating particles
854
+    for (let i = 0; i < 20; i++) {
855
+        let x = (frameCount * 0.5 + i * 137) % width;
856
+        let y = (sin(frameCount * 0.01 + i) * 50 + height/2);
857
+        let alpha = sin(frameCount * 0.02 + i) * 30 + 50;
858
+        
859
+        fill(0, 255, 136, alpha);
860
+        noStroke();
861
+        ellipse(x, y, 3, 3);
862
+    }
863
+    pop();
864
+}
865
+
643866
 function drawStartMessage() {
644867
     fill(0, 255, 136, 200);
645868
     textAlign(CENTER);
646869
     textSize(20);
647870
     text("Press any key to start!", width/2, height/2 + 100);
648871
     textSize(14);
649
-    text("Keyboard: W/S + ↑/↓ | Mouse/Touch: Drag paddles", width/2, height/2 + 125);
872
+    
873
+    if (aiEnabled) {
874
+        text("Player vs CPU | Left paddle: W/S or Mouse/Touch", width/2, height/2 + 125);
875
+        text(`AI Difficulty: ${aiState.difficulty.toUpperCase()}`, width/2, height/2 + 145);
876
+    } else {
877
+        text("2 Player Mode | P1: W/S | P2: ↑/↓ | Mouse/Touch: Drag paddles", width/2, height/2 + 125);
878
+    }
879
+    
650880
     textSize(12);
651
-    text("(Mouse movement has deliberate lag to preserve challenge!)", width/2, height/2 + 145);
881
+    fill(0, 255, 136, 120);
882
+    text("Press ESC to return to menu", width/2, height/2 + 170);
652883
 }
653884
 
654885
 function resetBall() {
@@ -714,10 +945,34 @@ function keyPressed() {
714945
     keys[key] = true;
715946
     keys[keyCode] = true;
716947
     
948
+    if (gameState === 'menu') {
949
+        handleMenuInput();
950
+        return;
951
+    }
952
+    
717953
     if (!gameStarted && key !== ' ') {
718954
         gameStarted = true;
719955
     }
720956
     
957
+    // Toggle game mode (only during gameplay)
958
+    if (key === 'm' || key === 'M') {
959
+        aiEnabled = !aiEnabled;
960
+        gameMode = aiEnabled ? 'vs-cpu' : 'vs-human';
961
+        console.log(`🎮 Switched to ${gameMode} mode`);
962
+    }
963
+    
964
+    // Change AI difficulty (only during gameplay)
965
+    if (key === 'd' || key === 'D') {
966
+        if (aiState.difficulty === 'easy') {
967
+            aiState.difficulty = 'medium';
968
+        } else if (aiState.difficulty === 'medium') {
969
+            aiState.difficulty = 'hard';
970
+        } else {
971
+            aiState.difficulty = 'easy';
972
+        }
973
+        console.log(`🤖 AI difficulty: ${aiState.difficulty}`);
974
+    }
975
+    
721976
     // Reset game with spacebar
722977
     if (key === ' ') {
723978
         leftScore = 0;
@@ -733,11 +988,84 @@ function keyPressed() {
733988
         // Reset mouse input
734989
         mouseInput.active = false;
735990
         
991
+        // Reset AI state
992
+        aiState.targetY = height / 2;
993
+        aiState.lastUpdateTime = 0;
994
+        
736995
         // Clear particles
737996
         particles = [];
738997
         
739998
         console.log("🔄 Game reset!");
740999
     }
1000
+    
1001
+    // Return to menu with ESC
1002
+    if (keyCode === 27) { // ESC key
1003
+        gameState = 'menu';
1004
+        gameStarted = false;
1005
+        particles = [];
1006
+        console.log("📋 Returned to menu");
1007
+    }
1008
+}
1009
+
1010
+function handleMenuInput() {
1011
+    // Navigate menu with arrow keys
1012
+    if (keyCode === UP_ARROW) {
1013
+        menuState.selectedOption = Math.max(0, menuState.selectedOption - 1);
1014
+    } else if (keyCode === DOWN_ARROW) {
1015
+        menuState.selectedOption = Math.min(menuState.options.length - 1, menuState.selectedOption + 1);
1016
+    }
1017
+    
1018
+    // Change difficulty for 1 Player mode
1019
+    if (menuState.selectedOption === 0) {
1020
+        if (keyCode === LEFT_ARROW) {
1021
+            menuState.difficultySelected = Math.max(0, menuState.difficultySelected - 1);
1022
+        } else if (keyCode === RIGHT_ARROW) {
1023
+            menuState.difficultySelected = Math.min(menuState.difficulties.length - 1, menuState.difficultySelected + 1);
1024
+        }
1025
+    }
1026
+    
1027
+    // Confirm selection with ENTER
1028
+    if (keyCode === ENTER || key === ' ') {
1029
+        startGameWithSelection();
1030
+    }
1031
+}
1032
+
1033
+function startGameWithSelection() {
1034
+    // Set game mode based on selection
1035
+    if (menuState.selectedOption === 0) {
1036
+        // 1 Player vs CPU
1037
+        aiEnabled = true;
1038
+        gameMode = 'vs-cpu';
1039
+        aiState.difficulty = menuState.difficulties[menuState.difficultySelected].toLowerCase();
1040
+    } else {
1041
+        // 2 Player
1042
+        aiEnabled = false;
1043
+        gameMode = 'vs-human';
1044
+    }
1045
+    
1046
+    // Start the game
1047
+    gameState = 'playing';
1048
+    gameStarted = false; // Will start when user presses a key
1049
+    
1050
+    // Reset game state
1051
+    leftScore = 0;
1052
+    rightScore = 0;
1053
+    updateScore();
1054
+    resetBall();
1055
+    
1056
+    // Reset input buffers
1057
+    inputBuffer.left = 0;
1058
+    inputBuffer.right = 0;
1059
+    mouseInput.active = false;
1060
+    
1061
+    // Reset AI state
1062
+    aiState.targetY = height / 2;
1063
+    aiState.lastUpdateTime = 0;
1064
+    
1065
+    // Clear particles
1066
+    particles = [];
1067
+    
1068
+    console.log(`🎮 Started ${gameMode} mode${aiEnabled ? ' - Difficulty: ' + aiState.difficulty : ''}`);
7411069
 }
7421070
 
7431071
 function keyReleased() {
@@ -747,6 +1075,11 @@ function keyReleased() {
7471075
 
7481076
 // Mouse/touch input handlers
7491077
 function mousePressed() {
1078
+    if (gameState === 'menu') {
1079
+        handleMenuClick();
1080
+        return false;
1081
+    }
1082
+    
7501083
     // Start mouse/touch input when clicking in game area
7511084
     if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) {
7521085
         mouseInput.active = true;
@@ -760,6 +1093,36 @@ function mousePressed() {
7601093
     }
7611094
 }
7621095
 
1096
+function handleMenuClick() {
1097
+    let startY = height/2 - 20;
1098
+    let spacing = 60;
1099
+    
1100
+    // Check if clicked on menu options
1101
+    for (let i = 0; i < menuState.options.length; i++) {
1102
+        let y = startY + i * spacing;
1103
+        
1104
+        if (mouseY > y - 25 && mouseY < y + 25) {
1105
+            if (menuState.selectedOption === i) {
1106
+                // Double click or click on already selected - start game
1107
+                startGameWithSelection();
1108
+            } else {
1109
+                // Select this option
1110
+                menuState.selectedOption = i;
1111
+            }
1112
+            break;
1113
+        }
1114
+    }
1115
+    
1116
+    // Check difficulty selection area for 1 Player mode
1117
+    if (menuState.selectedOption === 0) {
1118
+        let diffY = startY + 28;
1119
+        if (mouseY > diffY && mouseY < diffY + 20) {
1120
+            // Cycle through difficulties on click
1121
+            menuState.difficultySelected = (menuState.difficultySelected + 1) % menuState.difficulties.length;
1122
+        }
1123
+    }
1124
+}
1125
+
7631126
 function mouseDragged() {
7641127
     // Continue mouse/touch input while dragging
7651128
     if (mouseInput.active) {