There are two classic ways to play snake:
- Integral recruit
- A to eat what
The first is the one I first experienced on the handheld game console when I was a child (accidentally revealing my age). The specific gameplay is that the snake finishes the game after eating a certain amount of food, and then speeds up after finishing the game. The second is a game that Nokia installed on its phones in 1997, where the game is played by eating until there is no food left. The author wants to implement the second play.
MVC Design pattern
Based on the snake classic, the author also uses a classic design Model: MVC (model-view-Control) when implementing it. The various states and data structures of the game are managed by Model; View is used to show changes to the Model; The user’s interaction with the game is done by Control (Control provides various game apis).
The Model is the core of the game and the main content of this article; View has some performance implications; Control is responsible for the business logic. The advantage of this design is that the Model is completely independent, the View is the Model’s state machine, and the Model and View are both driven by Control.
Model
Look at a classic picture of a snake.
The snake has four key participants:
- Serpent (snake)
- Food
- Wall (bounds)
- Stage (Zone)
The stage is an M by N matrix (a two-dimensional array), the index boundary of the matrix is the stage wall, and the members of the matrix are used to mark the location of food and snakes.
The empty stage is as follows:
,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 [[0], [0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0]. ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 [0], [0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0],]Copy the code
Food (F) and snakes (S) appear on stage:
,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 [[0], [0], [0, 0, F, 0,0,0,0,0,0,0], [0, 0, S, S, S, S, 0, 0], [0,0,0,0,0,0, S, 0, 0]. [0,0,0,0, S, S, S, 0, 0], [0,0,0,0, S, 0,0,0,0,0], [0,0,0,0, S, 0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0],]Copy the code
Since the operation of two-dimensional array is not as convenient as one-dimensional array, the author uses one-dimensional array as follows:
,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 [0, 0, 0, 0, F,,0,0,0,0,0,0 0, 0, 0, S, S, S, S, 0, 0, 0,0,0,0,0,0, S, 0, 0, 0,0,0,0, S, S, S, 0, 0, 0,0,0,0, S, 0,0,0,0,0, 0,0,0,0, S, 0,0,0,0,0,,0,0,0,0,0,0,0,0,0 0, 0,0,0,0,0,0,0,0,0,0,]Copy the code
The snake and food in the stage matrix are just a mapping of the stage to them, and they have independent data structures for each other:
- The snake is a linked list of coordinate indexes;
- Food is an index to stage coordinates.
The activities of the snake
There are three kinds of snake activities, as follows:
- Move
- Eat (eat)
- Collision
mobile
What happens inside the snake as it moves?
Snake lists do two things in one move: insert a new node to the top of the list, and remove an old node from the bottom of the list. Using an array to represent the snake list, the snake movement would be the following pseudocode:
function move(next) {
snake.pop() & snake.unshift(next);
}Copy the code
Is an array a good list of snakes? This is what I started thinking about, because the unshift & POP of the array represents the snake’s movement seamlessly. However, convenience does not mean good performance. Unshift inserts elements into the array in O(n) time, and POP removes elements from the end of the array in O(1) time.
Snake movement is a high frequency action, and if the algorithmic complexity of a single action is O(n) and the length of the snake is large, then the performance of the game will be problematic. The snake I want to implement is theoretically a long snake, so my response in this article is that arrays are not suitable as snake lists.
The snake list must be a true list structure. The time complexity of removing or inserting a node in a list is O(1), and using a list as a data structure for a snake list improves game performance. Javascript does not have a ready-made list structure, so I wrote a linked list class called Chain, which provides unshfit & POP. The following pseudocode creates a list of snakes:
let snake = new Chain();Copy the code
I won’t cover it here for lack of spaceChain
If you are interested, you can go to:Github.com/leeenx/es6-…
Eating & bumping
The difference between eating and colliding is that eating collides with food and colliding with a wall. The author thinks that “eating” and “collision” belong to two branches of three possible results of a snake “movement”. The three possible outcomes of snake movement are “forward”, “feeding” and “collision”.
Look back at the pseudo-code for snake movement:
function move(next) {
snake.pop() & snake.unshift(next);
}Copy the code
Next in the code represents the index of the cell the snake head is about to enter. The snake can only “advance” if the cell is 0, “collide” itself when the cell is S, and eat when the cell is F.
I don’t think I hit the wall? In the design process, the author did not design the wall in the stage matrix, but through the index out of the way to represent the wall. Next === -1 means out of bounds and into the wall.
The following pseudocode represents the entire movement of the snake:
// let cell = -1 === next? B : zone[next]; Switch (cell) {// eat case F: eat(); break; Case S: Collision (S); break; Case B: Collision (B): break; // forward default: move; }Copy the code
Random hurl food
Random feeding means that an index value of the stage is randomly selected to map the location of the food. This seems simple enough to write like this:
// pseudo-code food = math.random (zone.length) >> 0;Copy the code
If you take into account the premise of feeding without overlapping with the snake, you can see that the random code above does not guarantee that feeding does not overlap with the snake. Because of the security of this algorithm with gambling nature, and it is called “gambling algorithm”. In order to ensure the safety of feeding, the author extended the algorithm:
Function feed() {let index = math.random (zone.length) >> 0; // Is the current location occupied return zone[index] === S? feed() : index; } food = feed();Copy the code
Although the above code can guarantee the absolute safety of feeding in theory, but the author called this algorithm “desperate gambler algorithm”, because the above algorithm has a fatal BUG — super-long recursion or infinite loop.
In order to solve the above fatal problem, the author designed the following algorithm to do random feeding:
Function feed() {let len = zone.length - snake. Length; If (len === 0) return; RND = math.random () * count >> 0 + 1; // Count Spaces while(count! Zone [index++] === 0 && ++count; } return index - 1; } food = feed();Copy the code
The average complexity of this algorithm is O(n/2). Since feeding is a low-frequency operation, O(n/2) complexity does not present any performance problems. However, I think the complexity of this algorithm is a little too high. If you go back to the original “gambling algorithm,” it was a pretty unreliable idea, but it had one advantage — time complexity of O(1).
Probability of “gambling algorithm” = (zone.length – snake. Length)/zone.length. Snake. Length is a dynamic value that ranges from 0 to zone.length. The average reliable probability of “gambling algorithm” is deduced as follows:
The average probability of “gambling algorithm” being correct = 50%
It seems that “gambling algorithms” can be exploited. So the author redesigned an algorithm:
Function bet() {let RND = math.random () * zone.length >> 0; return zone[rnd] === 0 ? rnd : -1; } function feed() { ... } food = bet(); if(food === -1) food = feed();Copy the code
The average complexity of the new algorithm can be effectively reduced to O(n/4), life sometimes needs a bit of luck:).
View
In View, you can choose a game rendering engine according to your preferences. The author chose PIXI as the game rendering engine in View layer.
View has two main tasks:
- Draw the interface of the game;
- Render various data structures in the Model
In other words, a View is the process of restoring the design using a rendering engine. The purpose of this article is to introduce the “snake” implementation ideas, how to use a rendering engine is not the scope of this article, the author would like to introduce is: “how to improve the efficiency of rendering”.
Displaying a Model snake in a View can be as simple as this pseudocode:
View.snake. Clean (); Model.snake. ForEach ((node) => {let viewNode = createViewNode(node); // Combine a new snake with view.snake. Push (viewNode); });Copy the code
The time complexity of the above code is O(n). As mentioned above, snake movement is a high-frequency activity, so we should avoid running O(n) code at high frequencies. To analyze the snake’s three activities: “moving”, “feeding” and “bumping”. First, the Model has a “crash”, and the View should simply pause rendering the state in the Model, the game is dead, and Control takes care of the rest. The snake in the Model does two things in a single “move” : inserts a new node into the top of the table and removes an old node from the bottom of the table; Snakes only do one thing at a time: insert a new node into the head of the list.
If differentiation check is performed on Model’s snake list in View and the View only incrementally updates the difference part, the time complexity of the algorithm can be reduced to O(1) ~ O(2). Here is the optimized pseudocode:
let snakeA = model.snake, snakeB = view.snake; While (snakeb.length <= snakea.length) {headA = snakea.next (); // The head node matches if(heada.data === headb.data) break; If (snakea. HEAD === heada.index) {snakeb. unshift(heada.data); } // Insert a second node into snakeB else snakeb. insertAfter(0, heada.data); }} // Let tailA = snakea. last(), tailB; while(snakeB.length ! == 0) { tailB = snakeB.last(); // If (taila. data === tailb. data) break; // Do not match else snakeb.pop (); }Copy the code
Control
Control does three things:
- The interaction between the game and the user
- Driver Model
- Synchronize View and Model
“Game user interaction” refers to the APIs and events that are available externally to the game process. The APIs planned by the authors are as follows:
name | type | deltail |
---|---|---|
init | method | Initialize the game |
start | method | Start the game |
restart | method | Restart the game |
pause | method | suspended |
resume | method | restore |
turn | method | Control the snake’s turn. Such as: turn (” left “) |
destroy | method | Destruction of the game |
speed | property | The speed at which the snake moves |
The events are as follows:
name | detail |
---|---|
countdown | Pour hour meter |
eat | Eat the food |
before-eat | Trigger before food is eaten |
gameover | Game over |
Events are mounted under the Event object of the game instance.
On ("countdown", (time) => console.log(" countdown: ", time));Copy the code
“Driving Model” does only one thing — updates the orientation of the Model’s snake to that specified by the user. “Synchronize View with Model” is also relatively simple. Check whether the Model is updated, and notify the View to update the game interface if there is an update.
conclusion
Here is the qr code of the online DEMO of snake introduced by this article:
The source code of the game is hosted at: github.com/leeenx/snak…
Thank you for reading this article patiently. This article only represents the author’s personal views, if there is anything inappropriate, please feel free to comment.
Convex laboratory
Aotu. IO/notes / 2017 /…