Take you through the complete development of the classic game [Tetris] from scratch, and the entire logic only takes less than 200 lines of code.

Classic Tetris is an unforgettable game, which can be played with computer keyboard shortcuts or mouse clicks, or with the touch screen of mobile phones; the initial number of rows and difficulty level can be set; the scene played last time will be automatically saved/restored.

Take you through the complete development of the classic game [Tetris] from scratch, and the entire logic only takes less than 200 lines of code.
The whole process is carried out on the ZhongTouch low-code application platform, using expressions to describe the game logic (a highly simplified version of JS).
This course has high requirements for mathematical abstraction, and is suitable for developers who have basically mastered Crowdtouch.

Demonstration of the final effect

Play it first (no registration): Zhongtouch low-code application platform editing mode

For detailed teaching, please go to the Bilibili video: [Crowd Touch Course] Tetris_哔哩哔哩_bilibili

game rules

  1. There is a field for small squares: 10 wide by 20 high.

  2. A set of regular figures composed of 4 small squares, 7 types in total, named after the shapes of the 7 letters S, Z, L, J, I, O, T.

  3. Blocks are randomly generated on the top of the field, moved, rotated, dropped and placed according to certain rules, locked and filled into the field. If one or more rows of the venue are completely filled with each placement, all the small squares that make up these rows will be eliminated and a certain amount of points will be awarded. The blocks that have not been eliminated will always accumulate and have various effects on the subsequent placement of the blocks.

  4. As the points increase, the falling speed of the cubes will gradually increase, increasing the difficulty and fun of the game.

  5. When the block moves to the bottom of the area or falls on other blocks and cannot move, it will be fixed there. When the stacked height of uneliminated cubes exceeds the maximum height specified by the field, the game ends.

4 states

Ready (ready), whereabouts (fall), end (over), paused (paused).

scoring rules

The higher the difficulty level, the faster the movement speed, and the higher the score of falling and eliminating.
drop score: 1 * level;
elimination score: Math.pow(2, number of lines eliminated) * level * 10;

Control the rendering of the field with a two-dimensional array matrix

Use a two-dimensional array to represent the entire area where the block is located, and $v.matrix = array(20, array(10)) you will get a 10*20 two-dimensional array matrix with all 0s. 0 means no blocks, 1 means blocks.
Use two nested data components to render, the outer data component uses $v.matrixas the data source, and the inner layer uses the data item provided by the outer layer $xas the data source (itself a one-dimensional array), from top to bottom, from left to right Right output into the zone.

For example, this array of matrices:

[
    [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, 1, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
    [1, 0, 0, 0, 1, 1, 0, 0, 0, 0],
    [1, 0, 0, 0, 1, 1, 1, 1, 0, 0],
    [1, 0, 0, 1, 1, 1, 1, 1, 0, 0],
    [1, 1, 0, 1, 1, 1, 1, 1, 0, 1]
]

will render as:

Pay attention to the corresponding relationship between the 1 in the matrix and the position of the black square in the figure. When the number is 1, give the small square a class name ($x ? "c" : ""), where c means black color.

7 Shapes of Tetris

The player manipulates a block with a certain shape, and uses a 4 * 2 two-dimensional array to render the shape of the block:

[
    [
        [0, 0, 0, 0],
        [1, 1, 1, 1]
    ],
    [
        [0, 0, 1, 0],
        [1, 1, 1, 0]
    ],
    [
        [1, 0, 0, 0],
        [1, 1, 1, 0]
    ],
    [
        [1, 1, 0, 0],
        [0, 1, 1, 0]
    ],
    [
        [0, 1, 1, 0],
        [1, 1, 0, 0]
    ],
    [
        [0, 1, 1, 0],
        [0, 1, 1, 0]
    ],
    [
        [0, 1, 0, 0],
        [1, 1, 1, 0]
    ]
]

Save it to $v.blocksa variable. Note the shape of the 1s in the array (I, L, J, Z, S, O, T).

Keyboard and click controls

Players can control the game through keyboard shortcuts or mouse clicks (mobile phone touch), so on the one hand, to capture key events, to bind click or touch events, they execute the same function. We write these logical expressions in $c.expglobal variables.
We associate the key code ($event.keyCode) with the action name and put it in $v.KEYS:

{
    "32": "drop",
    "37": "moveLeft",
    "38": "rotate",
    "39": "moveRight",
    "40": "down",
    "80": "pause",
    "82": "replay",
    "83": "sound"
}

Executed when a key is pressed (onKeyDown) or the mouse is pressed (onMouseDown):

$v.activeKey = $event.keyCode
$c.exp[$v.KEYS[$v.activeKey]].exc()

activeKey indicates the currently pressed key, and $v.KEYS[$v.activeKey] is the name of the action to be executed bound above. The definitions of these actions are placed in $c.exp and will be explained one by one in the following pages .

Executed when the key is pressed (onKeyUp) or the mouse is released (onMouseUp):

$v.activeKey = undefined
$(".key > .active").removeClass("active")

Game start: start

$v.score = 0
$v.status = "fall"
$v.startline ? array($v.startline).forEach($c.exp.startline) : ""
$c.exp.next.exc()

The score is set to 0, the state is set to fall, and if the start line is set then initialize the start line and generate the next Tetris.

Initialize the start line: startline

$v.matrix[19 - $index] = array(10,1)
array(1 + $w.Math.floor($w.Math.random() * 5)).forEach('$v.matrix[19 - $ext.$index][$w.Math.floor($w.Math.random() * 9)] = 0')

Set the small squares in each row of the starting row to 1 first, and then randomly hollow out some squares (set to 0)

Generate the next Tetris: next

$v.currB = $v.nextB
$v.nextB = $v.blocks[$w.Math.floor($w.Math.random() * $v.blocks.length)]
$v.xy = [3, -1]
$c.exp.fall.exc()

Randomly generate a Tetris block from $v.blocksit (currB represents the current Block of the current block, nextB represents the next block next Block), and set its position at the top (the starting point of x is 3, the middle position, and the starting point of y is -1, that is Still outside the matrix, invisible).
With $v.currB[$parent.$index - $v.xy[1]][$index - $v.xy[0]]) ? " c" : ""dynamic class names, newly generated blocks are also rendered in the matrix.

Start falling: fall

stopIf($v.status !== "fall" || ($l.fall && $l.fall !== $v.fall))
$c.exp.meetIf.exc()
$v.xy[1] = $v.xy[1] + 1
render()
timeout((10 - $v.level) * ($v.activeKey ? 200 : 100))
$c.exp.fall.exc()

If it is not in a falling state or the player is performing a downhill operation, stop the falling action.
Check the collision first. If there is no problem, add one to the Y axis and render(), so that you can see that a line of Tetris has fallen.
The pause time (timeout) is determined according to the player level ($v.level). The shorter the pause time, the faster the falling speed, and then recursively execute the fall (self).

Collision check: meetIf

stopIf($v.status != "fall" || !$v.currB.length)
stopIf($v.currB.some('$x.some("$x && $v.matrix[$ext.$index + $v.xy[1] + 1][$index + $v.xy[0]] !== 0")'), $c.exp.meet)

If the state is no longer fall (the game is over or the player pressed replay), or a collision has occurred ($v.currB = []) do not continue to check.
Assuming that there is a small box at the position where any small box of Tetris (that is, $x == 1) moves down one line, it means that there will be a collision when it falls this time.

There was a collision: meet

stopIf($v.xy[1] === -1, $c.exp.replay)
$v.currB.forEach('$x.forEach("$x ? $c.exp.meet2.exc() : null")')
$v.currB = []
render()
timeout(300)
$$(".matrix .red").removeClass("red")
timeout(300)
$v.score = ($v.score || 0) + $v.level
$l.clearY = []
$v.matrix.forEach('$x.includes(0) ? "" : $l.clearY.push($index)')
$l.clearY.length ? $c.exp.clearline.exc() : timeout(100)
$c.exp.next.exc()

If the Y-axis of the collision position is -1, it means that the Russian Cube just hit it, then the game is over, and the replay logic replay is executed.
Perform collision action

Collision action: meet2

$v.matrix[$ext.$index + $v.xy[1]][$index + $v.xy[0]] = 1
$v.el.matrix.children[$ext.$index + $v.xy[1]].children[$index + $v.xy[0]].addClass("red")

Put the current position of Tetris into the field matrix, and then add the red class name to these colliding small squares to make their colors red and highlighted.

Continue to execute the previous "collision occurred" logic, clear $v.currB, render it, pause for 300 microseconds, remove the red highlight of the colliding small squares, and pause for another 300 microseconds to add points according to the player's level, because The higher the player's level, the higher the falling speed, and the higher the points earned.

Check which rows (rows that do not contain 0) can be eliminated: loop through each row of the matrix, if there is no small square of 0 in the array of a row, it means that this row is all 1, and a row is filled and can be eliminated. Then continue to execute the next logic to generate the next block and continue to fall.

Clearline: clearline

$l.clearY.forEach('$v.el.matrix.children[$x].addClass("red")')
timeout(300)
$l.clearY.forEach('$v.el.matrix.children[$x].removeClass("red")')
timeout(310)
$l.clearY.forEach('$v.matrix.splice($x, 1); $v.matrix.splice(0, 0, array(10, 0))')
$v.score = ($v.score || 0) + $w.Math.pow(2, $l.clearY.length) * $v.level * 10
$v.lines = ($v.lines || 0) + $l.clearY.length

Give the line to be deleted the red class name, and the class name will be canceled after 300 microseconds, and there will be a red flashing effect visually.
Remove pending rows from the matrix and add a blank row from the top of the matrix.
Increase the score according to the number of lines to be eliminated and the player level. The more lines eliminated at one time, the higher the score, which is the increase of the geometric level (2 power).
Accumulates the number of eliminated rows.

During the falling process, the player can control the movement of Tetris: move left, move right, rotate, drop down, and fall.

Move left: moveLeft

$l.lr = -1
$c.exp.moveLR.exc()

Move right: moveRight

$l.lr = 1
$c.exp.moveLR.exc()

Move logic: moveLR

stopIf(!$v.activeKey || $v.currB.some('$x.some("$x && $v.matrix[$ext.$index + $v.xy[1]][$index + $v.xy[0] + $l.lr] !== 0")'))
stopIf($v.status === "paused", '$v.status = "fall"; ' + $c.exp.fall)
$v.xy[0] = $v.xy[0] + $l.lr
render()
timeout(150 - $v.level * 10)
$c.exp.moveLR.exc()

Boundary checking is required when moving left and right, and the left and right boundaries cannot be slipped: If there is a small square of 1 on the X axis of any small square in the current Tetris plus one step in the moving direction (ie $l.lr), it means To cross the line, stop moving logic.
Really move one step to the left or right, render it, and wait for a while. The better the player level, the less waiting time, that is, the faster the speed will be.
If the user still presses the button or the mouse (that is, !$v.activeKey in the first line), continue to execute this movement logic until the player releases his hand or crosses the boundary or collides.

Rotation: rotate

$l.currB = []
$v.currB.length === 2 ? $c.exp.rotate2to4.exc() :  $c.exp.rotate4to2.exc()
stopIf($l.currB.some('$x.some("$x && $v.matrix[$ext.$index + $l.xy[1]][$index + $l.xy[0]] !== 0")'))
$v.currB = $l.currB
$v.xy = $l.xy

If the current Tetris is horizontal (only 2 rows), then it is vertical (rotate2to4), otherwise it is horizontal.
Note $lthat it is a temporary variable, which is used to test whether it can be rotated during the rotation process. First rotate it (temporary row and column swap, convert the coordinates and put it in $l.currB), and then adjust the position (change the temporary position) $l.xy.

Turn it up: rotate2to4

array(4).forEach('$l.currB.push([$v.currB[1][$index], $v.currB[0][$index]])')
$l.xy = [$v.xy[0] + 1, $v.xy[1] - 1]

Recumbent: rotate4to2

array(2).forEach('$l.currB.push([$v.currB[3][$index], $v.currB[2][$index], $v.currB[1][$index], $v.currB[0][$index]])')
$l.xy = [$v.xy[0] - 1, $v.xy[1] + 1]

After the test is over, check whether there will be a collision. After OK, the rotation is actually implemented: assign the temporary variable $lto $v.

Start downhill: down

stopIf($v.status === "paused", '$v.status = "fall"; ' + $c.exp.fall)
$v.fall = date()
$l.fall = $v.fall
$c.exp.downLoop.exc()

If it is currently in the pause state, switch the state to the falling state and start to perform the falling action, but do not perform the downhill action.
Exclusive execution through the two variables of $v.fall and $l.fall, that is, to suspend the natural fall when downhill.

Downhill logic: downLoop

$c.exp.meetIf.exc()
$v.xy[1] = $v.xy[1] + 1
render()
timeout(20 - $v.level)
stopIf($v.activeKey, $c.exp.downLoop)
$c.exp.fall.exc()

Check the collision situation, if it is OK, go down one line, render it, wait for a very short time according to the player's level, if the player does not send water, continue to drop rapidly. If the player lets go, it will fall naturally.

Start to drop: drop

stopIf($v.status === "over")
stopIf($v.status === "ready", $c.exp.start)
stopIf($v.status === "paused", '$v.status = "fall"; ' + $c.exp.fall)
$v.el.top.addClass("dropping")
$v.fall = date()
timeout(9)
$v.el.top.removeClass("dropping")
$c.exp.dropLoop.exc()

If it is over, it will not fall.
If the key is pressed in the ready state, it will not drop, but start the game.
If the key pressed in the pause state does not fall, but starts to fall.

Drop logic: dropLoop

$c.exp.meetIf.exc()
$v.xy[1] = $v.xy[1] + 1
$c.exp.dropLoop.exc()

Falling is very similar to rappelling, they fall in a row until they hit the bottom or hit the bottom, but from the human eye, falling is one step, there is no intermediate falling process, that is because there is no pause during the falling process, and there is no delay. Rendering (even if a line is dropped, it cannot be seen by the human eye without rendering).

In addition to manipulating Tetris blocks, players can also pause the game, switch sound effects, and replay.

Pause the game: pause

stopIf($v.status === "over")
stopIf($v.status === "ready", $c.exp.start)
stopIf($v.status === "fall", '$v.status = "paused"; ' + $c.exp.pauseAnim)
$v.status = "fall"
$c.exp.fall.exc()

There is no need to pause if it is over.
If it is the key pressed in the ready state, it will turn to start the game.
If the key is pressed in the falling state, set the state to pause, and start to pause the animation, otherwise it will change to a falling action (press it again in the paused state).

Pause animation: pauseAnim

toggleClass($v.el.paused, "c")
timeout(300)
$v.status === "paused" ? $c.exp.pauseAnim.exc() : ""

Change the pause icon on the right to blink, every 300 microseconds.

Switch sound effect: sound

$v.music = !$v.music

When the sound effect is turned on, many actions will play sounds, and you can see that there will be the following expressions in those actions:

$v.music && $audio.start(when, offset, duration)

Replay: replay

stopIf($v.status === "ready", $c.exp.start)
$v.currB = []
$v.hiscore = $w.Math.max($v.hiscore, $v.score)
$v.lines = 0
$v.matrix = array(20, array(10))
$v.status = "over"
render()
array(20).forEach('$v.el.matrix.children[19 - $index].addClass("c"); timeout(30)')
timeout(100)
array(20).forEach('$v.el.matrix.children[$index].removeClass("c"); timeout(30)')
$v.status = "ready"
render()
$c.exp.readyAnim.exc()

If it is the key pressed in the ready state, it will turn to start the game.
Clear the current Tetris building blocks, calculate the highest score, reset the cumulative number of rows to zero, clear the matrix, set the state to over, and render it. At this time, you can see that it is an empty field.
Then make a screen-clearing animation: first turn all the small squares into black one by one from the bottom line, and pause for 30 microseconds after finishing one line; pause for 100 microseconds after finishing all of them, and clear the black from top to bottom , pausing for 30 microseconds each time a row is cleared.

Dinosaur animation in ready state after rendering

Ready animation: readyAnim

timeout(1000)
$(".readyscore").toggleClass("last")
$l.dragon = $(".dragon")
stopIf(!$l.dragon)
$l.dragonClass = "r"
array(40).forEach($c.exp.readyAnim2)
timeout(1000)
$l.dragonClass = "l"
array(40).forEach($c.exp.readyAnim2)
$v.status === "ready" ? $c.exp.readyAnim.exc() : ""

In the ready state, the highest score and the last round score will be alternated every second.
Rotate the class names of dinosaurs from right to left, and from left to right every one second, during which dinosaur animation is performed

Dinosaur animation: readyAnim2

$l.dragon.className = "dragon " + $l.dragonClass + ($index + 1) % 4
timeout(60)

Change the dinosaur's class name every 60 microseconds to animate its glasses and legs.

Scene save/restore: visibilitychange

When the webpage is closed, refreshed, window switched, mobile phone is out of battery, and incoming calls are interrupted, you must ensure that you can continue playing from where you left off when you return to the game next time. These states are actually the hidden states of the visibilityState in the visibilitychange event. Switching to the hidden state is to store all states in localStorage.

$w.document.addEventListener('visibilitychange', func('$w.document.visibilityState === "hidden" ? $exp.save.exc() : $exp.restore.exc()'))

Save status: save

localStorage("tetris", {music: $v.music, startline: $v.startline, level: $v.level, score: $v.score, lines: $v.lines, currB: $v.currB, nextB: $v.nextB, status: $v.status === "over" ? "ready" : $v.status, hiscore: $w.Math.max($v.hiscore, $v.score), matrix: $v.status === "over" ? array(20, array(10)) : $v.matrix, xy: $v.xy[1] > 19 ? [3, -1] : $v.xy})
$v.status === "fall" ? $v.status = "paused" : ""

Hidden also switches the state to paused.

Restoration status: restore

$l.LS = localStorage("tetris")
$v.score = $l.LS.score || 0
...
render()
$v.status === "fall" ? exc('timeout(2000); ' + $c.exp.fall) : ""

Partially assign values ​​to variables after reading state data from localStorage.
The key point is that if it is in the falling state before saving, wait for 2 seconds to wait for the user's response before continuing to fall.

At this point, the whole game logic is finished. You can review the whole process with the following questions:

  • How to get keyboard input?

  • How to control the movement of blocks?

  • How can the various shapes in the game and the entire game space be represented by data?

  • How to judge the possibility of moving left and right and down in the game?

  • How to judge the possibility of rotation of a certain shape in the game?

  • How to accelerate the falling speed of a shape when the down arrow key is pressed?

  • How to judge that a certain shape has reached the end?

  • How to judge that a row has been filled?

  • How to eliminate all rows that can be eliminated after a certain shape falls to the bottom?

  • How to modify the state of the game board?

  • How to count the score?

  • How to deal with the acceleration problem after upgrading?

  • How to judge the end of the game?

Students who are ready to study in depth, please go to the Tetris page, click the [Clone] button on the right, and make a copy of the entire game to play and change at will.

For more teaching videos, please go to the Bilibili space: the personal space of the Zhongtouch application platform_哔哩哔哩_Bilibili , which not only has various front-end visualization case demonstrations and explanations, but also multiple fully functional website application cases Demonstration and explanation of the development process.

This case was imitated from Tetris on June 25, 2021 , and the copyright of relevant materials belongs to the original website. This course is for educational purposes only.

Guess you like

Origin blog.csdn.net/weixin_52095264/article/details/125826839