1
1
import dynamic from "next/dynamic" ;
2
- import { useEffect , useState , useCallback } from "react" ;
2
+ import { useEffect , useState , useCallback , useRef } from "react" ;
3
3
import styles from "../styles/Snake.module.css" ;
4
4
5
5
const Config = {
@@ -21,7 +21,7 @@ const Direction = {
21
21
Bottom : { x : 0 , y : 1 } ,
22
22
} ;
23
23
24
- const Cell = ( { x, y, type } ) => {
24
+ const Cell = ( { x, y, type, remaining } ) => {
25
25
const getStyles = ( ) => {
26
26
switch ( type ) {
27
27
case CellType . Snake :
@@ -33,18 +33,21 @@ const Cell = ({ x, y, type }) => {
33
33
34
34
case CellType . Food :
35
35
return {
36
- backgroundColor : "darkorange " ,
36
+ backgroundColor : "tomato " ,
37
37
borderRadius : 20 ,
38
38
width : 32 ,
39
39
height : 32 ,
40
+ transform : `scale(${ 0.5 + remaining / 20 } )` ,
40
41
} ;
41
42
42
43
default :
43
44
return { } ;
44
45
}
45
46
} ;
47
+
46
48
return (
47
49
< div
50
+ key = { `${ x } -${ y } ` }
48
51
className = { styles . cellContainer }
49
52
style = { {
50
53
left : x * Config . cellSize ,
@@ -53,7 +56,9 @@ const Cell = ({ x, y, type }) => {
53
56
height : Config . cellSize ,
54
57
} }
55
58
>
56
- < div className = { styles . cell } style = { getStyles ( ) } > </ div >
59
+ < div className = { styles . cell } style = { getStyles ( ) } >
60
+ { remaining }
61
+ </ div >
57
62
</ div >
58
63
) ;
59
64
} ;
@@ -64,10 +69,25 @@ const getRandomCell = () => ({
64
69
createdAt : Date . now ( ) ,
65
70
} ) ;
66
71
67
- const getInitialFoods = ( ) => [ { x : 4 , y : 10 , createdAt : Date . now ( ) } ] ;
68
-
69
72
const getInitialDirection = ( ) => Direction . Right ;
70
73
74
+ const useInterval = ( callback , duration ) => {
75
+ const time = useRef ( 0 ) ;
76
+
77
+ const wrappedCallback = useCallback ( ( ) => {
78
+ // don't call callback() more than once within `duration`
79
+ if ( Date . now ( ) - time . current >= duration ) {
80
+ time . current = Date . now ( ) ;
81
+ callback ( ) ;
82
+ }
83
+ } , [ callback , duration ] ) ;
84
+
85
+ useEffect ( ( ) => {
86
+ const interval = setInterval ( wrappedCallback , 1000 / 60 ) ;
87
+ return ( ) => clearInterval ( interval ) ;
88
+ } , [ wrappedCallback , duration ] ) ;
89
+ } ;
90
+
71
91
const useSnake = ( ) => {
72
92
const getDefaultSnake = ( ) => [
73
93
{ x : 8 , y : 12 } ,
@@ -79,7 +99,7 @@ const useSnake = () => {
79
99
const [ snake , setSnake ] = useState ( getDefaultSnake ( ) ) ;
80
100
const [ direction , setDirection ] = useState ( getInitialDirection ( ) ) ;
81
101
82
- const [ foods , setFoods ] = useState ( getInitialFoods ( ) ) ;
102
+ const [ foods , setFoods ] = useState ( [ ] ) ;
83
103
84
104
const score = snake . length - 3 ;
85
105
@@ -88,7 +108,7 @@ const useSnake = () => {
88
108
89
109
// resets the snake ,foods, direction to initial values
90
110
const resetGame = useCallback ( ( ) => {
91
- setFoods ( getInitialFoods ( ) ) ;
111
+ setFoods ( [ ] ) ;
92
112
setDirection ( getInitialDirection ( ) ) ;
93
113
} , [ ] ) ;
94
114
@@ -99,6 +119,19 @@ const useSnake = () => {
99
119
) ;
100
120
} , [ ] ) ;
101
121
122
+ // ?. is called optional chaining
123
+ // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
124
+ const isFood = useCallback (
125
+ ( { x, y } ) => foods . some ( ( food ) => food . x === x && food . y === y ) ,
126
+ [ foods ]
127
+ ) ;
128
+
129
+ const isSnake = useCallback (
130
+ ( { x, y } ) =>
131
+ snake . find ( ( position ) => position . x === x && position . y === y ) ,
132
+ [ snake ]
133
+ ) ;
134
+
102
135
const addFood = useCallback ( ( ) => {
103
136
let newFood = getRandomCell ( ) ;
104
137
while ( isSnake ( newFood ) || isFood ( newFood ) ) {
@@ -108,68 +141,46 @@ const useSnake = () => {
108
141
} , [ isFood , isSnake ] ) ;
109
142
110
143
// move the snake
111
- useEffect ( ( ) => {
112
- const runSingleStep = ( ) => {
113
- setSnake ( ( snake ) => {
114
- const head = snake [ 0 ] ;
115
-
116
- // 0 <= a % b < b
117
- // so new x will always be inside the grid
118
- const newHead = {
119
- x : ( head . x + direction . x + Config . height ) % Config . height ,
120
- y : ( head . y + direction . y + Config . width ) % Config . width ,
121
- } ;
122
-
123
- // make a new snake by extending head
124
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
125
- const newSnake = [ newHead , ...snake ] ;
126
-
127
- // reset the game if the snake hit itself
128
- if ( isSnake ( newHead ) ) {
129
- resetGame ( ) ;
130
- return getDefaultSnake ( ) ;
131
- }
132
-
133
- // remove tail from the increased size snake
134
- // only if the newHead isn't a food
135
- if ( ! isFood ( newHead ) ) {
136
- newSnake . pop ( ) ;
137
- } else {
138
- setFoods ( ( currentFoods ) =>
139
- currentFoods . filter (
140
- ( food ) => ! ( food . x === newHead . x && food . y === newHead . y )
141
- )
142
- ) ;
143
- }
144
-
145
- return newSnake ;
146
- } ) ;
147
- } ;
144
+ const runSingleStep = useCallback ( ( ) => {
145
+ setSnake ( ( snake ) => {
146
+ const head = snake [ 0 ] ;
147
+
148
+ // 0 <= a % b < b
149
+ // so new x will always be inside the grid
150
+ const newHead = {
151
+ x : ( head . x + direction . x + Config . height ) % Config . height ,
152
+ y : ( head . y + direction . y + Config . width ) % Config . width ,
153
+ } ;
154
+
155
+ // make a new snake by extending head
156
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
157
+ const newSnake = [ newHead , ...snake ] ;
158
+
159
+ // reset the game if the snake hit itself
160
+ if ( isSnake ( newHead ) ) {
161
+ resetGame ( ) ;
162
+ return getDefaultSnake ( ) ;
163
+ }
148
164
149
- runSingleStep ( ) ;
150
- const timer = setInterval ( runSingleStep , 500 ) ;
165
+ // remove tail from the increased size snake
166
+ // only if the newHead isn't a food
167
+ if ( ! isFood ( newHead ) ) {
168
+ newSnake . pop ( ) ;
169
+ } else {
170
+ setFoods ( ( currentFoods ) =>
171
+ currentFoods . filter (
172
+ ( food ) => ! ( food . x === newHead . x && food . y === newHead . y )
173
+ )
174
+ ) ;
175
+ }
151
176
152
- return ( ) => clearInterval ( timer ) ;
153
- } , [ direction , foods , isFood , resetGame , isSnake ] ) ;
177
+ return newSnake ;
178
+ } ) ;
179
+ } , [ direction , isFood , isSnake , resetGame ] ) ;
154
180
155
- useEffect ( ( ) => {
156
- // add a food in a 3s interval
157
- const createFoodIntervalId = setInterval ( ( ) => {
158
- addFood ( ) ;
159
- } , 3000 ) ;
160
-
161
- // run the remove function each second,
162
- // but the function will decide which foods are
163
- // older than 10s and delete them
164
- const removeFoodIntervalId = setInterval ( ( ) => {
165
- removeFoods ( ) ;
166
- } , 1000 ) ;
167
-
168
- return ( ) => {
169
- clearInterval ( createFoodIntervalId ) ;
170
- clearInterval ( removeFoodIntervalId ) ;
171
- } ;
172
- } , [ addFood , removeFoods ] ) ;
181
+ useInterval ( runSingleStep , 200 ) ;
182
+ useInterval ( addFood , 3000 ) ;
183
+ useInterval ( removeFoods , 100 ) ;
173
184
174
185
useEffect ( ( ) => {
175
186
const handleDirection = ( direction , oppositeDirection ) => {
@@ -204,29 +215,26 @@ const useSnake = () => {
204
215
return ( ) => window . removeEventListener ( "keydown" , handleNavigation ) ;
205
216
} , [ ] ) ;
206
217
207
- // ?. is called optional chaining
208
- // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
209
- const isFood = useCallback (
210
- ( { x, y } ) => foods . some ( ( food ) => food . x === x && food . y === y ) ,
211
- [ foods ]
212
- ) ;
213
-
214
- const isSnake = useCallback (
215
- ( { x, y } ) =>
216
- snake . find ( ( position ) => position . x === x && position . y === y ) ,
217
- [ snake ]
218
- ) ;
219
-
220
218
const cells = [ ] ;
221
219
for ( let x = 0 ; x < Config . width ; x ++ ) {
222
220
for ( let y = 0 ; y < Config . height ; y ++ ) {
223
- let type = CellType . Empty ;
221
+ let type = CellType . Empty ,
222
+ remaining = undefined ;
224
223
if ( isFood ( { x, y } ) ) {
225
224
type = CellType . Food ;
225
+ remaining =
226
+ 10 -
227
+ Math . round (
228
+ ( Date . now ( ) -
229
+ foods . find ( ( food ) => food . x === x && food . y === y ) . createdAt ) /
230
+ 1000
231
+ ) ;
226
232
} else if ( isSnake ( { x, y } ) ) {
227
233
type = CellType . Snake ;
228
234
}
229
- cells . push ( < Cell key = { `${ x } -${ y } ` } x = { x } y = { y } type = { type } /> ) ;
235
+ cells . push (
236
+ < Cell key = { `${ x } -${ y } ` } x = { x } y = { y } type = { type } remaining = { remaining } />
237
+ ) ;
230
238
}
231
239
}
232
240
0 commit comments