Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
patorjk
GitHub Repository: patorjk/JavaScript-Snake
Path: blob/main/src/js/snake.js
163 views
1
/*
2
JavaScript Snake
3
First version by Patrick Gillespie - I've since merged in a good number of github pull requests
4
http://patorjk.com/games/snake
5
*/
6
7
/**
8
* @module Snake
9
* @class SNAKE
10
*/
11
12
// this will allow us to access the game in other JS files when the app is loaded up in a codesandbox.com sandbox, that's the only reason it's here
13
if (!window.SNAKE) {
14
window.SNAKE = {};
15
}
16
17
/*
18
Direction explained (0 = up, etc etc)
19
0
20
3 1
21
2
22
*/
23
const MOVE_NONE = -1;
24
const MOVE_UP = 0;
25
const MOVE_LEFT = 3;
26
const MOVE_DOWN = 2;
27
const MOVE_RIGHT = 1;
28
29
const MIN_SNAKE_SPEED = 25;
30
const RUSH_INCR = 5;
31
32
const DEFAULT_SNAKE_SPEED = 80;
33
34
const BOARD_NOT_READY = 0;
35
const BOARD_READY = 1;
36
const BOARD_IN_PLAY = 2;
37
38
const HIGH_SCORE_KEY = "jsSnakeHighScore";
39
40
/**
41
* @method addEventListener
42
* @param {Object} obj The object to add an event listener to.
43
* @param {String} event The event to listen for.
44
* @param {Function} funct The function to execute when the event is triggered.
45
* @param {Boolean} evtCapturing True to do event capturing, false to do event bubbling.
46
*/
47
SNAKE.addEventListener = (function () {
48
if (window.addEventListener) {
49
return function (obj, event, funct, evtCapturing) {
50
obj.addEventListener(event, funct, evtCapturing);
51
};
52
} else if (window.attachEvent) {
53
return function (obj, event, funct) {
54
obj.attachEvent("on" + event, funct);
55
};
56
}
57
})();
58
59
/**
60
* @method removeEventListener
61
* @param {Object} obj The object to remove an event listener from.
62
* @param {String} event The event that was listened for.
63
* @param {Function} funct The function that was executed when the event is triggered.
64
* @param {Boolean} evtCapturing True if event capturing was done, false otherwise.
65
*/
66
67
SNAKE.removeEventListener = (function () {
68
if (window.removeEventListener) {
69
return function (obj, event, funct, evtCapturing) {
70
obj.removeEventListener(event, funct, evtCapturing);
71
};
72
} else if (window.detachEvent) {
73
return function (obj, event, funct) {
74
obj.detachEvent("on" + event, funct);
75
};
76
}
77
})();
78
79
/**
80
* This class manages the snake which will reside inside of a SNAKE.Board object.
81
* @class Snake
82
* @constructor
83
* @namespace SNAKE
84
* @param {Object} config The configuration object for the class. Contains playingBoard (the SNAKE.Board that this snake resides in), startRow and startCol.
85
*/
86
SNAKE.Snake =
87
SNAKE.Snake ||
88
(function () {
89
// -------------------------------------------------------------------------
90
// Private static variables and methods
91
// -------------------------------------------------------------------------
92
93
const blockPool = [];
94
95
const SnakeBlock = function () {
96
this.elm = null;
97
this.elmStyle = null;
98
this.row = -1;
99
this.col = -1;
100
this.next = null;
101
this.prev = null;
102
};
103
104
// this function is adapted from the example at http://greengeckodesign.com/blog/2007/07/get-highest-z-index-in-javascript.html
105
function getNextHighestZIndex(myObj) {
106
let highestIndex = 0,
107
currentIndex = 0,
108
ii;
109
for (ii in myObj) {
110
if (myObj[ii].elm.currentStyle) {
111
currentIndex = parseFloat(myObj[ii].elm.style["z-index"], 10);
112
} else if (window.getComputedStyle) {
113
currentIndex = parseFloat(
114
document.defaultView
115
.getComputedStyle(myObj[ii].elm, null)
116
.getPropertyValue("z-index"),
117
10,
118
);
119
}
120
if (!isNaN(currentIndex) && currentIndex > highestIndex) {
121
highestIndex = currentIndex;
122
}
123
}
124
return highestIndex + 1;
125
}
126
127
// -------------------------------------------------------------------------
128
// Contructor + public and private definitions
129
// -------------------------------------------------------------------------
130
131
/*
132
config options:
133
playingBoard - the SnakeBoard that this snake belongs too.
134
startRow - The row the snake should start on.
135
startCol - The column the snake should start on.
136
moveSnakeWithAI - function to move the snake with AI
137
*/
138
return function (config) {
139
if (!config || !config.playingBoard) {
140
return;
141
}
142
if (localStorage[HIGH_SCORE_KEY] === undefined)
143
localStorage.setItem(HIGH_SCORE_KEY, 0);
144
145
// ----- private variables -----
146
147
const me = this;
148
const playingBoard = config.playingBoard;
149
const growthIncr = 5;
150
const columnShift = [0, 1, 0, -1];
151
const rowShift = [-1, 0, 1, 0];
152
let prevNode;
153
154
let lastMove = 1,
155
preMove = MOVE_NONE,
156
isFirstGameMove = true,
157
currentDirection = MOVE_NONE, // 0: up, 1: left, 2: down, 3: right
158
snakeSpeed = DEFAULT_SNAKE_SPEED,
159
isDead = false,
160
isPaused = false;
161
162
const modeDropdown = document.getElementById("selectMode");
163
if (modeDropdown) {
164
modeDropdown.addEventListener("change", function (evt) {
165
evt = evt || {};
166
let val = evt.target
167
? parseInt(evt.target.value)
168
: DEFAULT_SNAKE_SPEED;
169
170
if (isNaN(val)) {
171
val = DEFAULT_SNAKE_SPEED;
172
} else if (val < MIN_SNAKE_SPEED) {
173
val = DEFAULT_SNAKE_SPEED;
174
}
175
176
snakeSpeed = val;
177
178
setTimeout(function () {
179
document.getElementById("game-area").focus();
180
}, 10);
181
});
182
}
183
184
// ----- public variables -----
185
me.snakeBody = {};
186
me.snakeBody["b0"] = new SnakeBlock(); // create snake head
187
me.snakeBody["b0"].row = config.startRow || 1;
188
me.snakeBody["b0"].col = config.startCol || 1;
189
me.snakeBody["b0"].elm = createSnakeElement();
190
me.snakeBody["b0"].elmStyle = me.snakeBody["b0"].elm.style;
191
playingBoard.getBoardContainer().appendChild(me.snakeBody["b0"].elm);
192
me.snakeBody["b0"].elm.style.left = getLeftPosition(me.snakeBody["b0"]);
193
me.snakeBody["b0"].elm.style.top = getTopPosition(me.snakeBody["b0"]);
194
me.snakeBody["b0"].next = me.snakeBody["b0"];
195
me.snakeBody["b0"].prev = me.snakeBody["b0"];
196
197
me.snakeLength = 1;
198
me.snakeHead = me.snakeBody["b0"];
199
me.snakeTail = me.snakeBody["b0"];
200
me.snakeHead.elm.className = me.snakeHead.elm.className.replace(
201
/\bsnake-snakebody-dead\b/,
202
"",
203
);
204
me.snakeHead.elm.id = "snake-snakehead-alive";
205
me.snakeHead.elm.className += " snake-snakebody-alive";
206
207
// ----- private methods -----
208
209
function getTopPosition(block) {
210
const num = block.row * playingBoard.getBlockHeight();
211
return `${num}px`;
212
}
213
214
function getLeftPosition(block) {
215
const num = block.col * playingBoard.getBlockWidth();
216
return `${num}px`;
217
}
218
219
function createSnakeElement() {
220
const tempNode = document.createElement("div");
221
tempNode.className = "snake-snakebody-block";
222
tempNode.style.left = "-1000px";
223
tempNode.style.top = "-1000px";
224
tempNode.style.width = playingBoard.getBlockWidth() + "px";
225
tempNode.style.height = playingBoard.getBlockHeight() + "px";
226
return tempNode;
227
}
228
229
function createBlocks(num) {
230
let tempBlock;
231
const tempNode = createSnakeElement();
232
233
for (let ii = 1; ii < num; ii++) {
234
tempBlock = new SnakeBlock();
235
tempBlock.elm = tempNode.cloneNode(true);
236
tempBlock.elmStyle = tempBlock.elm.style;
237
playingBoard.getBoardContainer().appendChild(tempBlock.elm);
238
blockPool[blockPool.length] = tempBlock;
239
}
240
241
tempBlock = new SnakeBlock();
242
tempBlock.elm = tempNode;
243
playingBoard.getBoardContainer().appendChild(tempBlock.elm);
244
blockPool[blockPool.length] = tempBlock;
245
}
246
247
function recordScore() {
248
const highScore = localStorage[HIGH_SCORE_KEY];
249
if (me.snakeLength > highScore) {
250
alert(
251
"Congratulations! You have beaten your previous high score, which was " +
252
highScore +
253
".",
254
);
255
localStorage.setItem(HIGH_SCORE_KEY, me.snakeLength);
256
}
257
}
258
259
function handleEndCondition(handleFunc) {
260
recordScore();
261
me.snakeHead.elm.style.zIndex = getNextHighestZIndex(me.snakeBody);
262
me.snakeHead.elm.className = me.snakeHead.elm.className.replace(
263
/\bsnake-snakebody-alive\b/,
264
"",
265
);
266
me.snakeHead.elm.className += " snake-snakebody-dead";
267
268
isDead = true;
269
handleFunc();
270
}
271
272
// ----- public methods -----
273
274
me.setPaused = function (val) {
275
isPaused = val;
276
};
277
me.getPaused = function () {
278
return isPaused;
279
};
280
281
/**
282
* This method sets the snake direction
283
* @param direction
284
*/
285
me.setDirection = (direction) => {
286
if (currentDirection !== lastMove) {
287
// Allow a queue of 1 premove so you can turn again before the first turn registers
288
preMove = direction;
289
}
290
if (Math.abs(direction - lastMove) !== 2 || isFirstGameMove) {
291
// Prevent snake from turning 180 degrees
292
currentDirection = direction;
293
isFirstGameMove = false;
294
}
295
};
296
297
/**
298
* This method is called when a user presses a key. It logs arrow key presses in "currentDirection", which is used when the snake needs to make its next move.
299
* @method handleArrowKeys
300
* @param {Number} keyNum A number representing the key that was pressed.
301
*/
302
/*
303
Handles what happens when an arrow key is pressed.
304
Direction explained (0 = up, etc etc)
305
0
306
3 1
307
2
308
*/
309
me.handleArrowKeys = function (keyNum) {
310
if (isDead || (isPaused && !config.premoveOnPause)) {
311
return;
312
}
313
314
let directionFound = MOVE_NONE;
315
316
switch (keyNum) {
317
case 37:
318
case 65:
319
directionFound = MOVE_LEFT;
320
break;
321
case 38:
322
case 87:
323
directionFound = MOVE_UP;
324
break;
325
case 39:
326
case 68:
327
directionFound = MOVE_RIGHT;
328
break;
329
case 40:
330
case 83:
331
directionFound = MOVE_DOWN;
332
break;
333
}
334
me.setDirection(directionFound);
335
};
336
337
/**
338
* This method is executed for each move of the snake. It determines where the snake will go and what will happen to it. This method needs to run quickly.
339
* @method go
340
*/
341
me.go = function () {
342
const oldHead = me.snakeHead,
343
newHead = me.snakeTail,
344
grid = playingBoard.grid; // cache grid for quicker lookup
345
346
if (isPaused === true) {
347
setTimeout(function () {
348
me.go();
349
}, snakeSpeed);
350
return;
351
}
352
353
// code to execute if snake is being moved by AI
354
if (config.moveSnakeWithAI) {
355
config.moveSnakeWithAI({
356
grid,
357
snakeHead: me.snakeHead,
358
currentDirection,
359
isFirstGameMove,
360
setDirection: me.setDirection,
361
});
362
}
363
364
me.snakeTail = newHead.prev;
365
me.snakeHead = newHead;
366
367
// clear the old board position
368
if (grid[newHead.row] && grid[newHead.row][newHead.col]) {
369
grid[newHead.row][newHead.col] = 0;
370
}
371
372
if (currentDirection !== MOVE_NONE) {
373
lastMove = currentDirection;
374
if (preMove !== MOVE_NONE) {
375
// If the user queued up another move after the current one
376
currentDirection = preMove; // Execute that move next time (unless overwritten)
377
preMove = MOVE_NONE;
378
}
379
}
380
381
newHead.col = oldHead.col + columnShift[lastMove];
382
newHead.row = oldHead.row + rowShift[lastMove];
383
384
if (!newHead.elmStyle) {
385
newHead.elmStyle = newHead.elm.style;
386
}
387
388
newHead.elmStyle.left = getLeftPosition(newHead);
389
newHead.elmStyle.top = getTopPosition(newHead);
390
if (me.snakeLength > 1) {
391
newHead.elm.id = "snake-snakehead-alive";
392
oldHead.elm.id = "";
393
}
394
395
// check the new spot the snake moved into
396
397
if (grid[newHead.row][newHead.col] === 0) {
398
grid[newHead.row][newHead.col] = 1;
399
setTimeout(function () {
400
me.go();
401
}, snakeSpeed);
402
} else if (grid[newHead.row][newHead.col] > 0) {
403
me.handleDeath();
404
} else if (
405
grid[newHead.row][newHead.col] === playingBoard.getGridFoodValue()
406
) {
407
grid[newHead.row][newHead.col] = 1;
408
if (!me.eatFood()) {
409
me.handleWin();
410
return;
411
}
412
setTimeout(function () {
413
me.go();
414
}, snakeSpeed);
415
}
416
};
417
418
/**
419
* This method is called when it is determined that the snake has eaten some food.
420
* @method eatFood
421
* @return {bool} Whether a new food was able to spawn (true)
422
* or not (false) after the snake eats food.
423
*/
424
me.eatFood = function () {
425
if (blockPool.length <= growthIncr) {
426
createBlocks(growthIncr * 2);
427
}
428
const blocks = blockPool.splice(0, growthIncr);
429
430
let ii = blocks.length,
431
index;
432
prevNode = me.snakeTail;
433
while (ii--) {
434
index = "b" + me.snakeLength++;
435
me.snakeBody[index] = blocks[ii];
436
me.snakeBody[index].prev = prevNode;
437
me.snakeBody[index].elm.className =
438
me.snakeHead.elm.className.replace(/\bsnake-snakebody-dead\b/, "");
439
me.snakeBody[index].elm.className += " snake-snakebody-alive";
440
prevNode.next = me.snakeBody[index];
441
prevNode = me.snakeBody[index];
442
}
443
me.snakeTail = me.snakeBody[index];
444
me.snakeTail.next = me.snakeHead;
445
me.snakeHead.prev = me.snakeTail;
446
447
if (!playingBoard.foodEaten()) {
448
return false;
449
}
450
451
//Checks if the current selected option is that of "Rush"
452
//If so, "increase" the snake speed
453
const selectDropDown = document.getElementById("selectMode");
454
const selectedOption =
455
selectDropDown.options[selectDropDown.selectedIndex];
456
457
if (selectedOption.text.localeCompare("Rush") == 0) {
458
if (snakeSpeed > MIN_SNAKE_SPEED + RUSH_INCR) {
459
snakeSpeed -= RUSH_INCR;
460
}
461
}
462
463
return true;
464
};
465
466
/**
467
* This method handles what happens when the snake dies.
468
* @method handleDeath
469
*/
470
me.handleDeath = function () {
471
//Reset speed
472
const selectedSpeed = document.getElementById("selectMode").value;
473
snakeSpeed = parseInt(selectedSpeed);
474
475
handleEndCondition(playingBoard.handleDeath);
476
};
477
478
/**
479
* This method handles what happens when the snake wins.
480
* @method handleDeath
481
*/
482
me.handleWin = function () {
483
handleEndCondition(playingBoard.handleWin);
484
};
485
486
/**
487
* This method sets a flag that lets the snake be alive again.
488
* @method rebirth
489
*/
490
me.rebirth = function () {
491
isDead = false;
492
isFirstGameMove = true;
493
preMove = MOVE_NONE;
494
};
495
496
/**
497
* This method reset the snake so it is ready for a new game.
498
* @method reset
499
*/
500
me.reset = function () {
501
if (isDead === false) {
502
return;
503
}
504
505
const blocks = [];
506
let curNode = me.snakeHead.next;
507
let nextNode;
508
509
while (curNode !== me.snakeHead) {
510
nextNode = curNode.next;
511
curNode.prev = null;
512
curNode.next = null;
513
blocks.push(curNode);
514
curNode = nextNode;
515
}
516
me.snakeHead.next = me.snakeHead;
517
me.snakeHead.prev = me.snakeHead;
518
me.snakeTail = me.snakeHead;
519
me.snakeLength = 1;
520
521
for (let ii = 0; ii < blocks.length; ii++) {
522
blocks[ii].elm.style.left = "-1000px";
523
blocks[ii].elm.style.top = "-1000px";
524
blocks[ii].elm.className = me.snakeHead.elm.className.replace(
525
/\bsnake-snakebody-dead\b/,
526
"",
527
);
528
blocks[ii].elm.className += " snake-snakebody-alive";
529
}
530
531
blockPool.concat(blocks);
532
me.snakeHead.elm.className = me.snakeHead.elm.className.replace(
533
/\bsnake-snakebody-dead\b/,
534
"",
535
);
536
me.snakeHead.elm.className += " snake-snakebody-alive";
537
me.snakeHead.elm.id = "snake-snakehead-alive";
538
me.snakeHead.row = config.startRow || 1;
539
me.snakeHead.col = config.startCol || 1;
540
me.snakeHead.elm.style.left = getLeftPosition(me.snakeHead);
541
me.snakeHead.elm.style.top = getTopPosition(me.snakeHead);
542
};
543
544
me.getSpeed = () => {
545
return snakeSpeed;
546
};
547
me.setSpeed = (speed) => {
548
snakeSpeed = speed;
549
};
550
551
// ---------------------------------------------------------------------
552
// Initialize
553
// ---------------------------------------------------------------------
554
createBlocks(growthIncr * 2);
555
};
556
})();
557
558
/**
559
* This class manages the food which the snake will eat.
560
* @class Food
561
* @constructor
562
* @namespace SNAKE
563
* @param {Object} config The configuration object for the class. Contains playingBoard (the SNAKE.Board that this food resides in).
564
*/
565
566
SNAKE.Food =
567
SNAKE.Food ||
568
(function () {
569
// -------------------------------------------------------------------------
570
// Private static variables and methods
571
// -------------------------------------------------------------------------
572
573
let instanceNumber = 0;
574
575
function getRandomPosition(x, y) {
576
return Math.floor(Math.random() * (y + 1 - x)) + x;
577
}
578
579
// -------------------------------------------------------------------------
580
// Contructor + public and private definitions
581
// -------------------------------------------------------------------------
582
583
/*
584
config options:
585
playingBoard - the SnakeBoard that this object belongs too.
586
*/
587
return function (config) {
588
if (!config || !config.playingBoard) {
589
return;
590
}
591
592
// ----- private variables -----
593
594
const me = this;
595
const playingBoard = config.playingBoard;
596
let fRow, fColumn;
597
const myId = instanceNumber++;
598
599
const elmFood = document.createElement("div");
600
elmFood.setAttribute("id", "snake-food-" + myId);
601
elmFood.className = "snake-food-block";
602
elmFood.style.width = playingBoard.getBlockWidth() + "px";
603
elmFood.style.height = playingBoard.getBlockHeight() + "px";
604
elmFood.style.left = "-1000px";
605
elmFood.style.top = "-1000px";
606
playingBoard.getBoardContainer().appendChild(elmFood);
607
608
// ----- public methods -----
609
610
/**
611
* @method getFoodElement
612
* @return {DOM Element} The div the represents the food.
613
*/
614
me.getFoodElement = function () {
615
return elmFood;
616
};
617
618
/**
619
* Randomly places the food onto an available location on the playing board.
620
* @method randomlyPlaceFood
621
* @return {bool} Whether a food was able to spawn (true) or not (false).
622
*/
623
me.randomlyPlaceFood = function () {
624
// if there exist some food, clear its presence from the board
625
if (
626
playingBoard.grid[fRow] &&
627
playingBoard.grid[fRow][fColumn] === playingBoard.getGridFoodValue()
628
) {
629
playingBoard.grid[fRow][fColumn] = 0;
630
}
631
632
let row = 0,
633
col = 0,
634
numTries = 0;
635
636
const maxRows = playingBoard.grid.length - 1;
637
const maxCols = playingBoard.grid[0].length - 1;
638
639
while (playingBoard.grid[row][col] !== 0) {
640
row = getRandomPosition(1, maxRows);
641
col = getRandomPosition(1, maxCols);
642
643
// in some cases there may not be any room to put food anywhere
644
// instead of freezing, exit out (and return false to indicate
645
// that the player beat the game)
646
numTries++;
647
if (numTries > 20000) {
648
return false;
649
}
650
}
651
652
playingBoard.grid[row][col] = playingBoard.getGridFoodValue();
653
fRow = row;
654
fColumn = col;
655
elmFood.style.top = row * playingBoard.getBlockHeight() + "px";
656
elmFood.style.left = col * playingBoard.getBlockWidth() + "px";
657
return true;
658
};
659
};
660
})();
661
662
/**
663
* This class manages playing board for the game.
664
* @class Board
665
* @constructor
666
* @namespace SNAKE
667
* @param {Object} config The configuration object for the class. Set fullScreen equal to true if you want the game to take up the full screen, otherwise, set the top, left, width and height parameters.
668
*/
669
670
SNAKE.Board =
671
SNAKE.Board ||
672
(function () {
673
// -------------------------------------------------------------------------
674
// Private static variables and methods
675
// -------------------------------------------------------------------------
676
677
let instanceNumber = 0;
678
679
// this function is adapted from the example at http://greengeckodesign.com/blog/2007/07/get-highest-z-index-in-javascript.html
680
function getNextHighestZIndex(myObj) {
681
let highestIndex = 0,
682
currentIndex = 0,
683
ii;
684
for (ii in myObj) {
685
if (myObj[ii].elm.currentStyle) {
686
currentIndex = parseFloat(myObj[ii].elm.style["z-index"], 10);
687
} else if (window.getComputedStyle) {
688
currentIndex = parseFloat(
689
document.defaultView
690
.getComputedStyle(myObj[ii].elm, null)
691
.getPropertyValue("z-index"),
692
10,
693
);
694
}
695
if (!isNaN(currentIndex) && currentIndex > highestIndex) {
696
highestIndex = currentIndex;
697
}
698
}
699
return highestIndex + 1;
700
}
701
702
/*
703
This function returns the width of the available screen real estate that we have
704
*/
705
function getClientWidth() {
706
let myWidth = 0;
707
if (typeof window.innerWidth === "number") {
708
myWidth = window.innerWidth; //Non-IE
709
} else if (
710
document.documentElement &&
711
(document.documentElement.clientWidth ||
712
document.documentElement.clientHeight)
713
) {
714
myWidth = document.documentElement.clientWidth; //IE 6+ in 'standards compliant mode'
715
} else if (
716
document.body &&
717
(document.body.clientWidth || document.body.clientHeight)
718
) {
719
myWidth = document.body.clientWidth; //IE 4 compatible
720
}
721
return myWidth;
722
}
723
724
/*
725
This function returns the height of the available screen real estate that we have
726
*/
727
function getClientHeight() {
728
let myHeight = 0;
729
if (typeof window.innerHeight === "number") {
730
myHeight = window.innerHeight; //Non-IE
731
} else if (
732
document.documentElement &&
733
(document.documentElement.clientWidth ||
734
document.documentElement.clientHeight)
735
) {
736
myHeight = document.documentElement.clientHeight; //IE 6+ in 'standards compliant mode'
737
} else if (
738
document.body &&
739
(document.body.clientWidth || document.body.clientHeight)
740
) {
741
myHeight = document.body.clientHeight; //IE 4 compatible
742
}
743
return myHeight;
744
}
745
746
// -------------------------------------------------------------------------
747
// Contructor + public and private definitions
748
// -------------------------------------------------------------------------
749
750
return function (inputConfig) {
751
// --- private variables ---
752
const me = this;
753
const myId = instanceNumber++;
754
const config = inputConfig || {};
755
const MAX_BOARD_COLS = 250;
756
const MAX_BOARD_ROWS = 250;
757
const blockWidth = 20;
758
const blockHeight = 20;
759
const GRID_FOOD_VALUE = -1; // the value of a spot on the board that represents snake food; MUST BE NEGATIVE
760
761
// defaults
762
if (!config.onLengthUpdate) {
763
config.onLengthUpdate = () => {};
764
}
765
766
if (!config.onPauseToggle) {
767
config.onPauseToggle = () => {};
768
}
769
if (!config.onWin) {
770
config.onWin = () => {};
771
}
772
if (!config.onDeath) {
773
config.onDeath = () => {};
774
}
775
776
let myFood,
777
mySnake,
778
boardState = BOARD_READY, // 0: in active, 1: awaiting game start, 2: playing game
779
myKeyListener,
780
myWindowListener,
781
isPaused = false; //note: both the board and the snake can be paused
782
783
// Board components
784
let elmContainer,
785
elmPlayingField,
786
elmAboutPanel,
787
elmLengthPanel,
788
elmHighscorePanel,
789
elmWelcome,
790
elmTryAgain,
791
elmWin,
792
elmPauseScreen;
793
794
// --- public variables ---
795
me.grid = [];
796
797
// ---------------------------------------------------------------------
798
// private functions
799
// ---------------------------------------------------------------------
800
801
function getStartRow() {
802
return config.startRow || 2;
803
}
804
805
function getStartCol() {
806
return config.startCol || 2;
807
}
808
809
function createBoardElements() {
810
elmPlayingField = document.createElement("div");
811
elmPlayingField.setAttribute("id", "playingField");
812
elmPlayingField.className = "snake-playing-field";
813
814
SNAKE.addEventListener(
815
elmPlayingField,
816
"click",
817
function () {
818
elmContainer.focus();
819
},
820
false,
821
);
822
823
elmPauseScreen = document.createElement("div");
824
elmPauseScreen.className = "snake-pause-screen";
825
elmPauseScreen.innerHTML =
826
"<div style='padding:10px;'>[Paused]<p/>Press [space] to unpause.</div>";
827
828
elmAboutPanel = document.createElement("div");
829
elmAboutPanel.className = "snake-panel-component";
830
elmAboutPanel.innerHTML =
831
"<a href='http://patorjk.com/blog/software/' class='snake-link'>more patorjk.com apps</a> - <a href='https://github.com/patorjk/JavaScript-Snake' class='snake-link'>source code</a> - <a href='https://www.youtube.com/channel/UCpcCLm9y6CsjHUrCvJHYHUA' class='snake-link'>pat's youtube</a>";
832
833
elmLengthPanel = document.createElement("div");
834
elmLengthPanel.className = "snake-panel-component";
835
elmLengthPanel.innerHTML = "Length: 1";
836
837
elmHighscorePanel = document.createElement("div");
838
elmHighscorePanel.className = "snake-panel-component";
839
elmHighscorePanel.innerHTML =
840
"Highscore: " + (localStorage[HIGH_SCORE_KEY] || 0);
841
842
// if it's not AI, show the dialogs
843
if (!config.moveSnakeWithAI) {
844
elmWelcome = createWelcomeElement();
845
elmTryAgain = createTryAgainElement();
846
elmWin = createWinElement();
847
}
848
849
SNAKE.addEventListener(
850
elmContainer,
851
"keyup",
852
function (evt) {
853
if (!evt) evt = window.event;
854
evt.cancelBubble = true;
855
if (evt.stopPropagation) {
856
evt.stopPropagation();
857
}
858
if (evt.preventDefault) {
859
evt.preventDefault();
860
}
861
return false;
862
},
863
false,
864
);
865
866
elmContainer.className = "snake-game-container";
867
868
elmPauseScreen.style.zIndex = 10000;
869
elmContainer.appendChild(elmPauseScreen);
870
elmContainer.appendChild(elmPlayingField);
871
elmContainer.appendChild(elmAboutPanel);
872
elmContainer.appendChild(elmLengthPanel);
873
elmContainer.appendChild(elmHighscorePanel);
874
875
// nothing to attach if using AI
876
if (!config.moveSnakeWithAI) {
877
elmContainer.appendChild(elmWelcome);
878
elmContainer.appendChild(elmTryAgain);
879
elmContainer.appendChild(elmWin);
880
}
881
882
mySnake = new SNAKE.Snake({
883
playingBoard: me,
884
startRow: getStartRow(),
885
startCol: getStartCol(),
886
premoveOnPause: config.premoveOnPause,
887
moveSnakeWithAI: config.moveSnakeWithAI,
888
});
889
myFood = new SNAKE.Food({ playingBoard: me });
890
891
if (elmWelcome) {
892
elmWelcome.style.zIndex = 1000;
893
}
894
}
895
896
function maxBoardWidth() {
897
return MAX_BOARD_COLS * me.getBlockWidth();
898
}
899
900
function maxBoardHeight() {
901
return MAX_BOARD_ROWS * me.getBlockHeight();
902
}
903
904
function createWelcomeElement() {
905
const tmpElm = document.createElement("div");
906
tmpElm.id = "sbWelcome" + myId;
907
tmpElm.className = "snake-welcome-dialog";
908
909
const welcomeTxt = document.createElement("div");
910
let fullScreenText = "";
911
if (config.fullScreen) {
912
fullScreenText = "On Windows, press F11 to play in Full Screen mode.";
913
}
914
welcomeTxt.innerHTML =
915
"JavaScript Snake<p></p>Use the <strong>arrow keys</strong> on your keyboard to play the game. " +
916
fullScreenText +
917
"<p></p>";
918
const welcomeStart = document.createElement("button");
919
welcomeStart.appendChild(document.createTextNode("Play Game"));
920
921
const loadGame = function () {
922
SNAKE.removeEventListener(window, "keyup", kbShortcut, false);
923
tmpElm.style.display = "none";
924
me.setBoardState(BOARD_READY);
925
me.getBoardContainer().focus();
926
};
927
928
const kbShortcut = function (evt) {
929
if (!evt) evt = window.event;
930
const keyNum = evt.which ? evt.which : evt.keyCode;
931
if (keyNum === 32 || keyNum === 13) {
932
loadGame();
933
}
934
};
935
936
SNAKE.addEventListener(window, "keyup", kbShortcut, false);
937
SNAKE.addEventListener(welcomeStart, "click", loadGame, false);
938
939
tmpElm.appendChild(welcomeTxt);
940
tmpElm.appendChild(welcomeStart);
941
return tmpElm;
942
}
943
944
function createGameEndElement(message, elmId, elmClassName) {
945
const tmpElm = document.createElement("div");
946
tmpElm.id = elmId + myId;
947
tmpElm.className = elmClassName;
948
949
const gameEndTxt = document.createElement("div");
950
gameEndTxt.innerHTML = "JavaScript Snake<p></p>" + message + "<p></p>";
951
const gameEndStart = document.createElement("button");
952
gameEndStart.appendChild(document.createTextNode("Play Again?"));
953
954
const reloadGame = function () {
955
tmpElm.style.display = "none";
956
me.resetBoard();
957
me.setBoardState(BOARD_READY);
958
me.getBoardContainer().focus();
959
};
960
961
const kbGameEndShortcut = function (evt) {
962
if (boardState !== 0 || tmpElm.style.display !== "block") {
963
return;
964
}
965
if (!evt) evt = window.event;
966
const keyNum = evt.which ? evt.which : evt.keyCode;
967
if (keyNum === 32 || keyNum === 13) {
968
reloadGame();
969
}
970
};
971
SNAKE.addEventListener(window, "keyup", kbGameEndShortcut, true);
972
973
SNAKE.addEventListener(gameEndStart, "click", reloadGame, false);
974
tmpElm.appendChild(gameEndTxt);
975
tmpElm.appendChild(gameEndStart);
976
return tmpElm;
977
}
978
979
function createTryAgainElement() {
980
return createGameEndElement(
981
"You died :(",
982
"sbTryAgain",
983
"snake-try-again-dialog",
984
);
985
}
986
987
function createWinElement() {
988
return createGameEndElement("You win! :D", "sbWin", "snake-win-dialog");
989
}
990
991
function handleEndCondition(elmDialog) {
992
const index = Math.max(
993
getNextHighestZIndex(mySnake.snakeBody),
994
getNextHighestZIndex({ tmp: { elm: myFood.getFoodElement() } }),
995
);
996
if (elmDialog) {
997
elmContainer.removeChild(elmDialog);
998
elmContainer.appendChild(elmDialog);
999
elmDialog.style.zIndex = index;
1000
elmDialog.style.display = "block";
1001
}
1002
me.setBoardState(BOARD_NOT_READY);
1003
}
1004
1005
// ---------------------------------------------------------------------
1006
// public functions
1007
// ---------------------------------------------------------------------
1008
1009
me.setPaused = function (val) {
1010
isPaused = val;
1011
mySnake.setPaused(val);
1012
if (isPaused) {
1013
elmPauseScreen.style.display = "block";
1014
} else {
1015
elmPauseScreen.style.display = "none";
1016
}
1017
config.onPauseToggle(isPaused);
1018
};
1019
me.getPaused = function () {
1020
return isPaused;
1021
};
1022
1023
/**
1024
* Resets the playing board for a new game.
1025
* @method resetBoard
1026
*/
1027
me.resetBoard = function () {
1028
SNAKE.removeEventListener(
1029
elmContainer,
1030
"keydown",
1031
myKeyListener,
1032
false,
1033
);
1034
SNAKE.removeEventListener(
1035
elmContainer,
1036
"visibilitychange",
1037
myWindowListener,
1038
false,
1039
);
1040
mySnake.reset();
1041
config.onLengthUpdate(1);
1042
elmLengthPanel.innerHTML = "Length: 1";
1043
me.setupPlayingField();
1044
me.grid[getStartRow()][getStartCol()] = 1; // snake head
1045
};
1046
/**
1047
* Gets the current state of the playing board. There are 3 states: 0 - Welcome or Try Again dialog is present. 1 - User has pressed "Start Game" on the Welcome or Try Again dialog but has not pressed an arrow key to move the snake. 2 - The game is in progress and the snake is moving.
1048
* @method getBoardState
1049
* @return {Number} The state of the board.
1050
*/
1051
me.getBoardState = function () {
1052
return boardState;
1053
};
1054
/**
1055
* Sets the current state of the playing board. There are 3 states: 0 - Welcome or Try Again dialog is present. 1 - User has pressed "Start Game" on the Welcome or Try Again dialog but has not pressed an arrow key to move the snake. 2 - The game is in progress and the snake is moving.
1056
* @method setBoardState
1057
* @param {Number} state The state of the board.
1058
*/
1059
me.setBoardState = function (state) {
1060
boardState = state;
1061
};
1062
/**
1063
* @method getGridFoodValue
1064
* @return {Number} A number that represents food on a number representation of the playing board.
1065
*/
1066
me.getGridFoodValue = function () {
1067
return GRID_FOOD_VALUE;
1068
};
1069
/**
1070
* @method getPlayingFieldElement
1071
* @return {DOM Element} The div representing the playing field (this is where the snake can move).
1072
*/
1073
me.getPlayingFieldElement = function () {
1074
return elmPlayingField;
1075
};
1076
/**
1077
* @method setBoardContainer
1078
* @param {DOM Element or String} myContainer Sets the container element for the game.
1079
*/
1080
me.setBoardContainer = function (myContainer) {
1081
if (typeof myContainer === "string") {
1082
myContainer = document.getElementById(myContainer);
1083
}
1084
if (myContainer === elmContainer) {
1085
return;
1086
}
1087
elmContainer = myContainer;
1088
elmPlayingField = null;
1089
me.setupPlayingField();
1090
me.grid[getStartRow()][getStartCol()] = 1; // snake head
1091
};
1092
/**
1093
* @method getBoardContainer
1094
* @return {DOM Element}
1095
*/
1096
me.getBoardContainer = function () {
1097
return elmContainer;
1098
};
1099
/**
1100
* @method getBlockWidth
1101
* @return {Number}
1102
*/
1103
me.getBlockWidth = function () {
1104
return blockWidth;
1105
};
1106
/**
1107
* @method getBlockHeight
1108
* @return {Number}
1109
*/
1110
me.getBlockHeight = function () {
1111
return blockHeight;
1112
};
1113
/**
1114
* Sets up the playing field.
1115
* @method setupPlayingField
1116
*/
1117
me.setupPlayingField = function () {
1118
if (!elmPlayingField) {
1119
createBoardElements();
1120
} // create playing field
1121
1122
// calculate width of our game container
1123
let cWidth, cHeight;
1124
let cTop, cLeft;
1125
if (config.fullScreen === true) {
1126
cTop = 0;
1127
cLeft = 0;
1128
cWidth = getClientWidth() - 20;
1129
cHeight = getClientHeight() - 20;
1130
} else {
1131
cTop = config.top;
1132
cLeft = config.left;
1133
cWidth = config.width;
1134
cHeight = config.height;
1135
}
1136
1137
// define the dimensions of the board and playing field
1138
const wEdgeSpace =
1139
me.getBlockWidth() * 2 + (cWidth % me.getBlockWidth());
1140
const fWidth = Math.min(
1141
maxBoardWidth() - wEdgeSpace,
1142
cWidth - wEdgeSpace,
1143
);
1144
const hEdgeSpace =
1145
me.getBlockHeight() * 3 + (cHeight % me.getBlockHeight());
1146
const fHeight = Math.min(
1147
maxBoardHeight() - hEdgeSpace,
1148
cHeight - hEdgeSpace,
1149
);
1150
1151
elmContainer.style.left = cLeft + "px";
1152
elmContainer.style.top = cTop + "px";
1153
elmContainer.style.width = cWidth + "px";
1154
elmContainer.style.height = cHeight + "px";
1155
elmPlayingField.style.left = me.getBlockWidth() + "px";
1156
elmPlayingField.style.top = me.getBlockHeight() + "px";
1157
elmPlayingField.style.width = fWidth + "px";
1158
elmPlayingField.style.height = fHeight + "px";
1159
1160
// the math for this will need to change depending on font size, padding, etc
1161
// assuming height of 14 (font size) + 8 (padding)
1162
const bottomPanelHeight = hEdgeSpace - me.getBlockHeight();
1163
const pLabelTop =
1164
me.getBlockHeight() +
1165
fHeight +
1166
Math.round((bottomPanelHeight - 30) / 2) +
1167
"px";
1168
1169
elmAboutPanel.style.top = pLabelTop;
1170
elmAboutPanel.style.width = "450px";
1171
elmAboutPanel.style.left =
1172
Math.round(cWidth / 2) - Math.round(450 / 2) + "px";
1173
1174
elmLengthPanel.style.top = pLabelTop;
1175
elmLengthPanel.style.left = 30 + "px";
1176
1177
elmHighscorePanel.style.top = pLabelTop;
1178
elmHighscorePanel.style.left = cWidth - 140 + "px";
1179
1180
// if width is too narrow, hide the about panel
1181
if (cWidth < 700) {
1182
elmAboutPanel.style.display = "none";
1183
} else {
1184
elmAboutPanel.style.display = "block";
1185
}
1186
1187
me.grid = [];
1188
const numBoardCols = fWidth / me.getBlockWidth() + 2;
1189
const numBoardRows = fHeight / me.getBlockHeight() + 2;
1190
1191
for (let row = 0; row < numBoardRows; row++) {
1192
me.grid[row] = [];
1193
for (let col = 0; col < numBoardCols; col++) {
1194
if (
1195
col === 0 ||
1196
row === 0 ||
1197
col === numBoardCols - 1 ||
1198
row === numBoardRows - 1
1199
) {
1200
me.grid[row][col] = 1; // an edge
1201
} else {
1202
me.grid[row][col] = 0; // empty space
1203
}
1204
}
1205
}
1206
1207
myFood.randomlyPlaceFood();
1208
config.onLengthUpdate(1);
1209
1210
myKeyListener = function (evt) {
1211
if (!evt) evt = window.event;
1212
const keyNum = evt.which ? evt.which : evt.keyCode;
1213
1214
if (me.getBoardState() === BOARD_READY) {
1215
if (
1216
!(keyNum >= 37 && keyNum <= 40) &&
1217
!(
1218
keyNum === 87 ||
1219
keyNum === 65 ||
1220
keyNum === 83 ||
1221
keyNum === 68
1222
)
1223
) {
1224
return;
1225
} // if not an arrow key, leave
1226
1227
// This removes the listener added at the #listenerX line
1228
SNAKE.removeEventListener(
1229
elmContainer,
1230
"keydown",
1231
myKeyListener,
1232
false,
1233
);
1234
SNAKE.removeEventListener(
1235
elmContainer,
1236
"visibilitychange",
1237
myWindowListener,
1238
false,
1239
);
1240
1241
myKeyListener = function (evt) {
1242
if (!evt) evt = window.event;
1243
const keyNum = evt.which ? evt.which : evt.keyCode;
1244
1245
if (keyNum === 32) {
1246
if (me.getBoardState() != BOARD_NOT_READY)
1247
me.setPaused(!me.getPaused());
1248
}
1249
1250
mySnake.handleArrowKeys(keyNum);
1251
1252
evt.cancelBubble = true;
1253
if (evt.stopPropagation) {
1254
evt.stopPropagation();
1255
}
1256
if (evt.preventDefault) {
1257
evt.preventDefault();
1258
}
1259
return false;
1260
};
1261
1262
//listener for pausing the game if user change tab or minimize the browser window
1263
document.addEventListener("visibilitychange", () => {
1264
if (document.visibilityState === "hidden") {
1265
if (me.getBoardState() != BOARD_NOT_READY && !me.getPaused())
1266
me.setPaused(true);
1267
}
1268
});
1269
1270
SNAKE.addEventListener(
1271
elmContainer,
1272
"keydown",
1273
myKeyListener,
1274
false,
1275
);
1276
SNAKE.addEventListener(
1277
elmContainer,
1278
"visibilitychange",
1279
myWindowListener,
1280
false,
1281
);
1282
1283
mySnake.rebirth();
1284
mySnake.handleArrowKeys(keyNum);
1285
me.setBoardState(BOARD_IN_PLAY); // start the game!
1286
mySnake.go();
1287
}
1288
1289
evt.cancelBubble = true;
1290
if (evt.stopPropagation) {
1291
evt.stopPropagation();
1292
}
1293
if (evt.preventDefault) {
1294
evt.preventDefault();
1295
}
1296
return false;
1297
};
1298
1299
// Search for #listenerX to see where this is removed
1300
if (!config.moveSnakeWithAI) {
1301
SNAKE.addEventListener(elmContainer, "keydown", myKeyListener, false);
1302
SNAKE.addEventListener(
1303
elmContainer,
1304
"visibilitychange",
1305
myWindowListener,
1306
false,
1307
);
1308
}
1309
};
1310
1311
/**
1312
* This method is called when the snake has eaten some food.
1313
* @method foodEaten
1314
* @return {bool} Whether a new food was able to spawn (true)
1315
* or not (false) after the snake eats food.
1316
*/
1317
me.foodEaten = function () {
1318
config.onLengthUpdate(mySnake.snakeLength);
1319
elmLengthPanel.innerHTML = "Length: " + mySnake.snakeLength;
1320
if (mySnake.snakeLength > localStorage[HIGH_SCORE_KEY]) {
1321
localStorage.setItem(HIGH_SCORE_KEY, mySnake.snakeLength);
1322
elmHighscorePanel.innerHTML =
1323
"Highscore: " + localStorage[HIGH_SCORE_KEY];
1324
}
1325
if (!myFood.randomlyPlaceFood()) {
1326
return false;
1327
}
1328
return true;
1329
};
1330
1331
/**
1332
* This method is called when the snake dies.
1333
* @method handleDeath
1334
*/
1335
me.handleDeath = function () {
1336
handleEndCondition(elmTryAgain);
1337
config.onDeath({ startAIGame: me.startAIGame });
1338
};
1339
1340
/**
1341
* This method is called when the snake wins.
1342
* @method handleWin
1343
*/
1344
me.handleWin = function () {
1345
handleEndCondition(elmWin);
1346
config.onWin({ startAIGame: me.startAIGame });
1347
};
1348
1349
me.setSpeed = (speed) => {
1350
mySnake.setSpeed(speed);
1351
};
1352
me.getSpeed = () => {
1353
return mySnake.getSpeed();
1354
};
1355
1356
me.startAIGame = () => {
1357
me.resetBoard();
1358
mySnake.rebirth();
1359
me.setBoardState(BOARD_IN_PLAY); // start the game!
1360
mySnake.go();
1361
};
1362
1363
// ---------------------------------------------------------------------
1364
// Initialize
1365
// ---------------------------------------------------------------------
1366
1367
config.fullScreen =
1368
typeof config.fullScreen === "undefined" ? false : config.fullScreen;
1369
config.top = typeof config.top === "undefined" ? 0 : config.top;
1370
config.left = typeof config.left === "undefined" ? 0 : config.left;
1371
config.width = typeof config.width === "undefined" ? 400 : config.width;
1372
config.height =
1373
typeof config.height === "undefined" ? 400 : config.height;
1374
config.premoveOnPause =
1375
typeof config.premoveOnPause === "undefined"
1376
? false
1377
: config.premoveOnPause;
1378
1379
if (config.fullScreen) {
1380
SNAKE.addEventListener(
1381
window,
1382
"resize",
1383
function () {
1384
me.setupPlayingField();
1385
},
1386
false,
1387
);
1388
}
1389
1390
me.setBoardState(BOARD_NOT_READY);
1391
1392
if (config.boardContainer) {
1393
me.setBoardContainer(config.boardContainer);
1394
}
1395
1396
const reloadGame = function () {
1397
me.resetBoard();
1398
me.setBoardState(BOARD_READY);
1399
me.getBoardContainer().focus();
1400
};
1401
1402
if (config.onInit) {
1403
config.onInit({
1404
reloadGame,
1405
getSpeed: me.getSpeed,
1406
setSpeed: me.setSpeed,
1407
startAIGame: me.startAIGame,
1408
});
1409
}
1410
}; // end return function
1411
})();
1412
1413