@@ -2,6 +2,54 @@ import React, { useState, useEffect } from "react";
22import { getChains , stringToBlobUrl } from "common" ;
33import { TChain } from "common" ;
44
5+ // Game Configuration
6+ const GAME_CONFIG = {
7+ // Player settings
8+ player : {
9+ radius : 20 ,
10+ initialX : 50 ,
11+ jumpForce : - 15 ,
12+ gravity : 0.6 ,
13+ } ,
14+
15+ // Obstacle settings
16+ obstacles : {
17+ width : 40 ,
18+ minHeight : 40 ,
19+ maxNormalHeight : 60 ,
20+ minHighHeight : 100 ,
21+ maxHighHeight : 140 ,
22+ highObstacleChance : 0.4 ,
23+ minSpacing : 700 ,
24+ randomSpacingRange : 300 ,
25+ } ,
26+
27+ // Game physics
28+ physics : {
29+ initialSpeed : 3 ,
30+ speedIncrease : 4 ,
31+ speedIncreaseRate : 1000 , // Lower = faster speed increase
32+ } ,
33+
34+ // Scoring
35+ scoring : {
36+ obstaclePoints : 100 ,
37+ survivalPointsPerSecond : 1 ,
38+ } ,
39+
40+ // Canvas settings
41+ canvas : {
42+ width : 400 ,
43+ height : 300 ,
44+ groundOffset : 30 ,
45+ } ,
46+
47+ // Cooldown
48+ cooldown : {
49+ duration : 3 , // seconds
50+ } ,
51+ } ;
52+
553type Obstacle = {
654 x : number ;
755 width : number ;
@@ -19,14 +67,14 @@ export function GitcoinRunner() {
1967 const [ cooldown , setCooldown ] = useState ( false ) ;
2068 const [ cooldownTime , setCooldownTime ] = useState ( 3 ) ;
2169 const playerRef = React . useRef ( {
22- x : 50 ,
23- y : 250 , // Default position, will be updated in useEffect
70+ x : GAME_CONFIG . player . initialX ,
71+ y : 250 ,
2472 dy : 0 ,
25- radius : 20 ,
73+ radius : GAME_CONFIG . player . radius ,
2674 isJumping : false ,
2775 } ) ;
2876 const gameStateRef = React . useRef ( {
29- gameSpeed : 2 ,
77+ gameSpeed : GAME_CONFIG . physics . initialSpeed ,
3078 frameCount : 0 ,
3179 } ) ;
3280 const obstaclesRef = React . useRef < Obstacle [ ] > ( [ ] ) ;
@@ -36,9 +84,8 @@ export function GitcoinRunner() {
3684
3785 function startJump ( ) {
3886 const player = playerRef . current ;
39-
4087 if ( ! player . isJumping ) {
41- player . dy = - 10 ;
88+ player . dy = GAME_CONFIG . player . jumpForce ;
4289 player . isJumping = true ;
4390 }
4491 }
@@ -55,12 +102,36 @@ export function GitcoinRunner() {
55102 : 250 ;
56103 player . dy = 0 ;
57104 player . isJumping = false ;
58- gameState . gameSpeed = 2 ;
105+ gameState . gameSpeed = GAME_CONFIG . physics . initialSpeed ;
59106 gameState . frameCount = 0 ;
60107 obstaclesRef . current = [ ] ;
61108 setGameStarted ( true ) ;
62109 }
63110
111+ function createObstacle ( ) {
112+ const width = GAME_CONFIG . obstacles . width ;
113+ const isHighObstacle =
114+ Math . random ( ) < GAME_CONFIG . obstacles . highObstacleChance ;
115+ const height = isHighObstacle
116+ ? GAME_CONFIG . obstacles . minHighHeight +
117+ Math . random ( ) *
118+ ( GAME_CONFIG . obstacles . maxHighHeight -
119+ GAME_CONFIG . obstacles . minHighHeight )
120+ : GAME_CONFIG . obstacles . minHeight +
121+ Math . random ( ) *
122+ ( GAME_CONFIG . obstacles . maxNormalHeight -
123+ GAME_CONFIG . obstacles . minHeight ) ;
124+ const iconIndex = Math . floor ( Math . random ( ) * chainIconsRef . current . length ) ;
125+
126+ obstaclesRef . current . push ( {
127+ x : canvasRef . current ?. width || 0 ,
128+ width,
129+ height,
130+ scored : false ,
131+ iconIndex,
132+ } ) ;
133+ }
134+
64135 useEffect ( ( ) => {
65136 if ( ! canvasRef . current || ! imagesLoaded ) return ;
66137
@@ -72,28 +143,8 @@ export function GitcoinRunner() {
72143 const obstacles = obstaclesRef . current ;
73144 const gameState = gameStateRef . current ;
74145 let animationId : number ;
75- const gravity = 0.25 ;
76- const groundLevel = canvas . height - 30 ;
77-
78- function createObstacle ( ) {
79- const width = 40 ;
80- // Sometimes create higher obstacles that can be passed under
81- const isHighObstacle = Math . random ( ) < 0.3 ; // 30% chance for high obstacle
82- const height = isHighObstacle
83- ? 80 + Math . random ( ) * 40 // High obstacle: 80-120px
84- : 40 + Math . random ( ) * 20 ; // Normal obstacle: 40-60px
85- const iconIndex = Math . floor (
86- Math . random ( ) * chainIconsRef . current . length
87- ) ;
88-
89- obstacles . push ( {
90- x : canvas . width ,
91- width,
92- height,
93- scored : false ,
94- iconIndex,
95- } ) ;
96- }
146+ const gravity = GAME_CONFIG . player . gravity ;
147+ const groundLevel = canvas . height - GAME_CONFIG . canvas . groundOffset ;
97148
98149 function drawGround ( context : CanvasRenderingContext2D ) {
99150 context . strokeStyle = "#666" ;
@@ -134,21 +185,38 @@ export function GitcoinRunner() {
134185
135186 function checkCollision ( ) {
136187 return obstacles . some ( ( obstacle ) => {
137- // Get player bounds
138- const playerTop = player . y - player . radius ;
139- const playerBottom = player . y + player . radius ;
140- const playerLeft = player . x - player . radius ;
141- const playerRight = player . x + player . radius ;
142-
143- // Get obstacle bounds
144- const obstacleLeft = obstacle . x ;
145- const obstacleRight = obstacle . x + obstacle . width ;
146- const obstacleTop = groundLevel - obstacle . height ;
147-
148- // Check if we're horizontally aligned with the obstacle
149- if ( playerRight > obstacleLeft && playerLeft < obstacleRight ) {
150- // Only collide if we hit from above or the sides
151- return playerBottom > obstacleTop && playerTop < obstacleTop ;
188+ const playerBox = {
189+ left : player . x - player . radius ,
190+ right : player . x + player . radius ,
191+ top : player . y - player . radius ,
192+ bottom : player . y + player . radius ,
193+ } ;
194+
195+ const obstacleBox = {
196+ left : obstacle . x ,
197+ right : obstacle . x + obstacle . width ,
198+ top : groundLevel - obstacle . height ,
199+ bottom : groundLevel ,
200+ } ;
201+
202+ // Check for horizontal overlap
203+ const horizontalOverlap =
204+ playerBox . right > obstacleBox . left &&
205+ playerBox . left < obstacleBox . right ;
206+
207+ // If there's horizontal overlap, check for collision
208+ if ( horizontalOverlap ) {
209+ // For high obstacles (that can be passed under)
210+ if ( obstacle . height > GAME_CONFIG . obstacles . maxNormalHeight ) {
211+ // Only collide if we hit the top portion
212+ return (
213+ playerBox . bottom > obstacleBox . top &&
214+ playerBox . top < obstacleBox . top
215+ ) ;
216+ } else {
217+ // For normal obstacles, collide if there's any vertical overlap
218+ return playerBox . bottom > obstacleBox . top ;
219+ }
152220 }
153221
154222 return false ;
@@ -159,17 +227,17 @@ export function GitcoinRunner() {
159227 obstacles . forEach ( ( obstacle ) => {
160228 if ( ! obstacle . scored && player . x > obstacle . x + obstacle . width ) {
161229 obstacle . scored = true ;
162- setScore ( ( prev ) => prev + 100 ) ; // More points for obstacles
230+ setScore ( ( prev ) => prev + GAME_CONFIG . scoring . obstaclePoints ) ;
163231 }
164232 } ) ;
165233
166- // Update survival time based on frame count (60 frames = 1 second)
167234 if ( gameStarted && ! gameOver ) {
168235 gameState . frameCount ++ ;
169- // Add 1 point every 60 frames (1 second)
170236 if ( gameState . frameCount % 60 === 0 ) {
171237 setSurvivalTime ( gameState . frameCount / 60 ) ;
172- setScore ( ( prev ) => prev + 1 ) ;
238+ setScore (
239+ ( prev ) => prev + GAME_CONFIG . scoring . survivalPointsPerSecond
240+ ) ;
173241 }
174242 }
175243 }
@@ -193,26 +261,28 @@ export function GitcoinRunner() {
193261
194262 // Draw start screen
195263 if ( ! gameStarted && ! gameOver ) {
196- ctx . fillStyle = "rgba(0, 0, 0, 0.7) " ;
264+ ctx . fillStyle = "#ffffff " ;
197265 ctx . fillRect ( 0 , 0 , canvas . width , canvas . height ) ;
198266
199267 // Title with glow
200- ctx . fillStyle = "white " ;
268+ ctx . fillStyle = "#000000 " ;
201269 ctx . textAlign = "center" ;
202- ctx . shadowColor = "rgba(0, 255 , 255, 0.5 )" ;
270+ ctx . shadowColor = "rgba(0, 200 , 255, 0.3 )" ;
203271 ctx . shadowBlur = 15 ;
204272 ctx . font = "bold 36px Arial" ;
205273 ctx . fillText ( "Chain Hopper" , canvas . width / 2 , canvas . height / 2 - 20 ) ;
206274
207275 // Instructions
208276 ctx . shadowBlur = 5 ;
209277 ctx . font = "20px Arial" ;
278+ ctx . fillStyle = "#333333" ;
210279 ctx . fillText (
211280 "Jump across the chains!" ,
212281 canvas . width / 2 ,
213282 canvas . height / 2 + 10
214283 ) ;
215284 ctx . font = "16px Arial" ;
285+ ctx . fillStyle = "#666666" ;
216286 ctx . fillText (
217287 "Press spacebar or tap to play" ,
218288 canvas . width / 2 ,
@@ -225,19 +295,19 @@ export function GitcoinRunner() {
225295
226296 // Draw game over screen
227297 if ( gameOver ) {
228- // Semi-transparent overlay
229- ctx . fillStyle = "rgba(0, 0, 0, 0.85) " ;
298+ // Light background
299+ ctx . fillStyle = "#ffffff " ;
230300 ctx . fillRect ( 0 , 0 , canvas . width , canvas . height ) ;
231301
232- // Draw fancy score box with gradient
302+ // Draw score box with subtle gradient
233303 const gradient = ctx . createLinearGradient (
234304 0 ,
235305 canvas . height / 2 - 100 ,
236306 0 ,
237307 canvas . height / 2 + 100
238308 ) ;
239- gradient . addColorStop ( 0 , "rgba(255, 255 , 255, 0.15 )" ) ;
240- gradient . addColorStop ( 1 , "rgba(255, 255 , 255, 0.05)" ) ;
309+ gradient . addColorStop ( 0 , "rgba(0, 200 , 255, 0.1 )" ) ;
310+ gradient . addColorStop ( 1 , "rgba(0, 200 , 255, 0.05)" ) ;
241311 ctx . fillStyle = gradient ;
242312 ctx . beginPath ( ) ;
243313 ctx . roundRect (
@@ -250,16 +320,17 @@ export function GitcoinRunner() {
250320 ctx . fill ( ) ;
251321
252322 // Game over text with shadow
253- ctx . fillStyle = "white " ;
254- ctx . shadowColor = "rgba(0, 0, 0 , 0.5 )" ;
323+ ctx . fillStyle = "#000000 " ;
324+ ctx . shadowColor = "rgba(0, 200, 255 , 0.3 )" ;
255325 ctx . shadowBlur = 10 ;
256326 ctx . font = "bold 36px Arial" ;
257327 ctx . textAlign = "center" ;
258328 ctx . fillText ( "Game Over!" , canvas . width / 2 , canvas . height / 2 - 30 ) ;
259329
260- // Score and time with subtle glow
330+ // Score with subtle glow
261331 ctx . shadowBlur = 5 ;
262332 ctx . font = "28px Arial" ;
333+ ctx . fillStyle = "#333333" ;
263334 ctx . fillText (
264335 `Score: ${ score } ` ,
265336 canvas . width / 2 ,
@@ -271,15 +342,15 @@ export function GitcoinRunner() {
271342
272343 if ( cooldown ) {
273344 ctx . font = "16px Arial" ;
274- ctx . fillStyle = "rgba(255, 255, 255, 0.7) " ;
345+ ctx . fillStyle = "#666666 " ;
275346 ctx . fillText (
276347 `Wait ${ cooldownTime } seconds...` ,
277348 canvas . width / 2 ,
278349 canvas . height / 2 + 50
279350 ) ;
280351 } else {
281352 ctx . font = "18px Arial" ;
282- ctx . fillStyle = "rgba(255, 255, 255, 0.9) " ;
353+ ctx . fillStyle = "#666666 " ;
283354 ctx . fillText (
284355 "Press space to play again" ,
285356 canvas . width / 2 ,
@@ -299,8 +370,13 @@ export function GitcoinRunner() {
299370 player . isJumping = false ;
300371 }
301372
302- // Remove speed cap and make progression slower
303- gameState . gameSpeed = 2 + ( gameState . frameCount / 6000 ) * 3 ;
373+ // Update game speed
374+ gameState . gameSpeed =
375+ GAME_CONFIG . physics . initialSpeed +
376+ Math . min (
377+ gameState . frameCount / GAME_CONFIG . physics . speedIncreaseRate ,
378+ GAME_CONFIG . physics . speedIncrease
379+ ) ;
304380
305381 // Update obstacles
306382 obstacles . forEach ( ( obstacle ) => {
@@ -312,10 +388,13 @@ export function GitcoinRunner() {
312388 obstacles . shift ( ) ;
313389 }
314390
315- // Add new obstacles with much more space between them
391+ // Add new obstacles with randomized spacing
316392 if (
317393 obstacles . length === 0 ||
318- obstacles [ obstacles . length - 1 ] . x < canvas . width - 800
394+ obstacles [ obstacles . length - 1 ] . x <
395+ canvas . width -
396+ ( GAME_CONFIG . obstacles . minSpacing +
397+ Math . random ( ) * GAME_CONFIG . obstacles . randomSpacingRange )
319398 ) {
320399 createObstacle ( ) ;
321400 }
@@ -488,8 +567,8 @@ export function GitcoinRunner() {
488567 < div className = "text-center" >
489568 < canvas
490569 ref = { canvasRef }
491- width = { 400 }
492- height = { 300 }
570+ width = { GAME_CONFIG . canvas . width }
571+ height = { GAME_CONFIG . canvas . height }
493572 className = "mx-auto border rounded-lg cursor-pointer"
494573 />
495574 </ div >
0 commit comments