-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgame.js
377 lines (322 loc) · 10.1 KB
/
game.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
/*
I don't have enough experience with javascript. So let's make a simple
snake game that will be set in a 3D environment. The game will be
first have a 2D version and then we will add the 3D environment, if
I can figure that out.
*/
var DEBUG = false;
/*
Let us define the values in the board that will represent different things:
0 - Empty Space
1 - Snake Body
2 - Apple
And that's all the parts there are to snake, as I remember it.
*/
// returns a random value in range [min, max)
function randomValueInRange(min, max){
return Math.random() * (max - min) + min;
}
// compute the euclidean distance between two points
function euclideanDistance(cell1, cell2){
return Math.sqrt((cell1.row - cell2.row)**2 + (cell1.col - cell2.col)**2);
}
export class SnakeGame {
constructor(gridWidth, gridHeight){
this.board; // store game state
this.direction; // store the directions snake cells should move in
this.snakeBody; // easy access to snake cells
this.head;
this.gridWidth = gridWidth;
this.gridHeight = gridHeight;
// all apples must spawn at least alpha blocks away from the head
this.alpha = 5;
if (DEBUG) { console.log(`Board size: (W,H) = (${this.gridWidth},${this.gridHeight})`)}
// init board and direction matrix
this.clearBoard();
this.clearDirection();
this.clearSnake();
// init snake position
this.initSnakeHead();
if (DEBUG) { console.log(`Head at (x,y) = (${this.head.col},${this.head.row})`)}
// generate apple
this.genApple();
}
// initialize a new game state
resetGameState(){
// init board and direction matrix
this.clearBoard();
this.clearDirection();
this.clearSnake();
// init snake position
this.initSnakeHead();
// generate apple
this.genApple();
}
// initialize the snake head
initSnakeHead(){
this.head = this.getRandomBoardCell();
this.board[this.head.row][this.head.col] = 1;
this.snakeBody.push(this.head)
}
// expect cell to be Object[int,int]
// returns true if given cell coordinate is in any part of the snakeBody
// otherwise return false
isCellInBody(cell){
for (const v of this.snakeBody) {
if (cell.row === v.row && cell.col === v.col){
return true;
}
}
return false;
}
// return true if cell is not in playspace
// else return false
isCellInWallOrOutOfBounds(cell){
// in or jumped beyond any of the walls
if(cell.row < 0 || cell.col < 0 || cell.row >= this.gridHeight || cell.col >= this.gridWidth){
return true;
}
return false;
}
// check if player won snake
// could check all cells, but that is slower
// instead, just check the length of the snake body
// if the len of the snake body is equal to the total numbe of
// cells in the board, then they have covered the entire board.
isWin(){
if(this.snakeBody.length == this.gridWidth*this.gridHeight){
return true;
}
return false;
}
// check collision
// if given cell coincides with either a snake body or a wall
// return true if no collision, else false
isNoCollision(cell){
// snake body
if( this.isCellInBody(cell) ){
if (DEBUG) {
console.log(`Collision detected: cell in snake body. Snake Body:`);
for(const x of this.snakeBody){
console.log(`(x,y) = (${x.col},${x.row})`)
}
}
return false;
}
// a wall
if ( this.isCellInWallOrOutOfBounds(cell) ){
if (DEBUG) {console.log(`Collision detected: cell in a wall`)}
return false;
}
return true;
}
// need to generate an apple at a position that is at least
// alpha blocks away from the snake's head AND
// isn't in the snake's body in any way
// updates the object's apple property in place
genApple(){
let x = this.getRandomBoardCell();
// while random board cell is in the body
// or if the cell is too close to the head, generate a new cell
while(this.isCellInBody(x) || (Math.trunc(euclideanDistance(this.head, x)) < this.alpha)){
x = this.getRandomBoardCell();
}
this.board[x.row][x.col] = 2;
if (DEBUG) { console.log(`Apple spawn: (x,y) = (${x.col}, ${x.row})`)}
this.apple = x;
}
growSnake(moveDirection){
/* This function is called when the snake head eats an apple.
Just replace the apple with a new snake body (head),
update the direction matrix, and respawn an apple.
*/
if (DEBUG) { console.log('growSnake'); }
// update direction matrix for old head
this.direction[this.head.row][this.head.col] = moveDirection;
// spawn a new head (the apple)
this.snakeBody.unshift(this.apple);
this.head = this.snakeBody[0];
// update board
this.board[this.head.row][this.head.col] = 1;
// update direction matrix for new head
this.direction[this.head.row][this.head.col] = moveDirection;
// spawn a new apple
this.genApple();
}
moveSnake(moveDirection){
/* This function is called if the snake hasn't eaten an apple
and thus we need to simulate moving the entire snake forward.
*/
// update the direction matrix with the direction
// current head is moving
this.direction[this.head.row][this.head.col] = moveDirection;
// move snake by using the direction in the direction matrix
var row, col, currDir;
for(const i in this.snakeBody){
row = this.snakeBody[i].row;
col = this.snakeBody[i].col;
currDir = this.direction[row][col];
if (DEBUG) { console.log(`currDir = ${currDir}`)}
this.board[row][col] = 0; // clear curr
switch(currDir){
case 0: // up
row--;
this.snakeBody[i] = {row: row, col: col};
this.board[row][col] = 1;
break;
case 1: // right
col++;
this.snakeBody[i] = {row: row, col: col};
this.board[row][col] = 1;
break;
case 2: // down
row++;
this.snakeBody[i] = {row: row, col: col};
this.board[row][col] = 1;
break;
case 3: // left
col--;
this.snakeBody[i] = {row: row, col: col};
this.board[row][col] = 1;
break;
}
}
// update head pointer (first block in list)
this.head = this.snakeBody[0];
}
updateSnake(moveDirection){
/* movedirection is an int indicating the direction to move in
input
moveDirection
0 - up
1 - right
2 - down
3 - left
output
0 - ok, updated, continue game.
1 - win, game over - no more cells to move in!
-1 - loss, game over - collision detected.
*/
if (DEBUG) { console.log('updateSnake'); }
// want a shallow copy of head
let nextCell = { ...this.head };
// row is y (up and down) , col is x (left and right)
switch(moveDirection){
case 0: // up
nextCell.row--;
break;
case 1: // right
nextCell.col++;
break;
case 2: // down
nextCell.row++;
break;
case 3: // left
nextCell.col--;
break;
}
if (DEBUG) { console.log(`nextCell: (x,y) = (${nextCell.col}, ${nextCell.row})`)}
// check if game over -> hit wall or hit self
if ( !this.isNoCollision(nextCell) ) {
return -1;
}
// grow the snake if ate the apple
if (nextCell.row === this.apple.row && nextCell.col === this.apple.col) {
this.growSnake(moveDirection);
} else {
// just move the snake in moveDirection
this.moveSnake(moveDirection);
}
// check win condition (there exists no space left on board)
// i believe the easiest way to win is if you try hard and
// only move your snake in a hamiltonian cycle.
if (this.isWin()){
return 1;
}
// otherwise, continue the game!
return 0;
}
// clears/resets board
clearBoard(){
this.board = [];
for(let i = 0; i < this.gridHeight; i++){
let tmp = [];
for(let j = 0; j < this.gridWidth; j++){
tmp.push(0);
}
this.board.push(tmp);
}
}
// clears the direction matrix
clearDirection(){
this.direction = [];
for(let i = 0; i < this.gridHeight; i++){
let tmp = [];
for(let j = 0; j < this.gridWidth; j++){
tmp.push(-1);
}
this.direction.push(tmp);
}
}
// clear the snakeBody
clearSnake(){
this.snakeBody = [];
}
// returns the current game state's user score
// which is just the length of the snake body
getScore(){
return this.snakeBody.length;
}
// get a random valid board position
// will be used to spawn in apples
// returns: Object with 2 properties - row and col (both ints)
// oh i want types, but we have to walk (js) before we run (ts).
getRandomBoardCell(){
let row = Math.trunc(randomValueInRange(0, this.gridHeight));
let col = Math.trunc(randomValueInRange(0, this.gridWidth));
return {row:row, col:col};
}
// main update entrance function to move from game state n to n+1
nextGameState(inputKeystroke){
/* movedirection is an int indicating the direction to move in
input
inputKeystroke
0 - up
1 - right
2 - down
3 - left
-1 - restart game
*/
if (DEBUG) {
console.log(`Before Board:`);
printBoard(this.board);
console.log(`Before Direction`)
printBoard(this.direction);
}
// reset the game state if input is -1
if (inputKeystroke === -1){
// restart the game
this.resetGameState();
return 0;
}
// Expected output from updateSnake
// 0 - ok, updated, continue game.
// 1 - win, game over - no more cells to move in!
// -1 - loss, game over - collision detected.
let updateSnakeResult = this.updateSnake(inputKeystroke);
if (DEBUG) {
console.log(`updateSnake returned: ${updateSnakeResult}`)
console.log(`After Board:`);
printBoard(this.board);
console.log(`After Direction`)
printBoard(this.direction);
}
return updateSnakeResult;
}
}
function printBoard(board) {
board.forEach(row => {
console.log(row.join(' '));
});
}
export default SnakeGame;