[003] Tetris

- game rules are based on the Tetris Guideline [tetris.wiki/Tetris_Guideline]
- Soft Drop, DAS & ARE are based on NES Tetris [tetris.wiki/Tetris_(NES,_Nintendo)]
- game updates at 60 frames per second [gamedev.stackexchange.com/q/159835]
- uses marathon speed for each level [tetris.wiki/Marathon]

TODO: sounds and effects (+ BG)
TODO: score multipliers (i.e. combos, T-spin, etc.)
TODO: not sure if I-piece "kick" is correct


Game Mechanics

[rules]

- playfield (matrix) is 10 cells wide and 20 cells tall
- pieces can be manipulated at the top of the playfield (on spawn)
- each of the 7 pieces are randomized in a 'bag' and are dealt out one by one
- can hold pieces, once taken out, piece must lock down first before hold can be used again
- there is a ghost piece (preview) to show where a piece will go to


Scoring

[rules]

single = 100 * level
double = 300 * level
triple = 500 * level
tetris = 800 * level

+1 score on each grid cell a piece is soft dropped
+2 score on each grid cell a piece is hard dropped
level advances by one for each 10 lines cleared


Movement

[rules]

- on L/R key press, move the piece one cell
- [DAS] stop 16 frames (0.26667), then move again every 6 frames (0.1)
- tapping L/R key instantly moves the piece one cell
- the piece automatically moves down at intervals based on level
- on each level, has a 'Frames per Gridcell' (converted to Godot delta)
- soft drop (pressing down) is 1/2 G

- ARE (appearance delay) depending on where piece is locked
- 10 frames at index [0 - 1] rows (0.16667)
- 12 frames at index [2 - 5] rows (0.20000)
- 14 frames at index [6 - 9] rows (0.23334)
- 16 frames at index [10 - 13] rows (0.26667)
- 18 frames at index [14 - 19] rows (0.30000)
- additional 20 frames on line clear (0.33333)


Drop Speed

[rules]

- based on the Tetris Guide [tetris.wiki/Marathon]
- each level has a 'Cell per Frame'
- due to how _process() is in Godot, i had to convert this to 'Time per Cell'
- keeping in mind that each second is 60 frames

frames_per_cell = 1 / cell_per_frame
second_per_cell = frames_per_cell / 60
_cell_per_frame = [0.01667, 0.021017, 0.026977, 0.035256, 0.04693,
                    0.06361, 0.0879, 0.1236, 0.1775, 0.2598,
                    0.388, 0.59, 0.92, 1.46, 2.36,
                    3.91, 6.61, 11.43, 20]
_frames_per_cell = [59.98800, 47.58053, 37.06861, 28.36397, 21.30833,
                    15.72080, 11.37656, 8.09061, 5.63380, 3.84911,
                    2.57732, 1.69491, 1.08696, 0.68493, 0.42372,
                    0.25575, 0.15129, 0.08749, 0.05]
_second_per_cell = [1.0, 0.79300, 0.61781, 0.47273, 0.35514,
                    0.26201, 0.18961, 0.13484, 0.09390, 0.06415,
                    0.04296, 0.02825, 0.01812, 0.01142, 0.00706,
                    0.00426, 0.00252, 0.00146, 0.00083]


Rotation & Kicks

[rules]

- uses guideline Super Rotation System [tetris.wiki/Super_Rotation_System]
- unobstructed basic rotation are unaffected
- if basic rotation is blocked, 4 other positions are tested
- if the 4 other positions are all blocked, the rotation fails
- tested position are based on initial state and desired final rotation state
- adjust Y position for Godot Grid system (Y is reversed)

- piece O have no rotation
- piece I have a different rotation offset

    Test 1        Test 2        Test 3        Test 4        Test 5
0     ( 0, 0)     (-1, 0)     (+2, 0)     (-1, 0)     (+2, 0)
L     ( 0,-1)     ( 0,-1)     ( 0,-1)     ( 0,+1)     ( 0,-2)
2     (-1,-1)     (+1,-1)     (-2,-1)     (+1, 0)     (-2, 0)
R     (-1, 0)     ( 0, 0)     ( 0, 0)     ( 0,-1)     ( 0,+2)
- all other pieces share the same rotation offset
    Test 1        Test 2        Test 3        Test 4        Test 5
0     ( 0, 0)     ( 0, 0)     ( 0, 0)     ( 0, 0)     ( 0, 0)
L     ( 0, 0)     (-1, 0)     (-1,+1)     ( 0,-2)     (-1,-2)
2     ( 0, 0)     ( 0, 0)     ( 0, 0)     ( 0, 0)     ( 0, 0)
R     ( 0, 0)     (+1, 0)     (+1,+1)     ( 0,-2)     (+1,-2)

- values are subtracted based on rotation
- 0 > R = 0 - R
- Test 1 = ( 0, 0) - ( 0, 0) = ( 0, 0)
- Test 2 = ( 0, 0) - (+1, 0) = (-1, 0)
- Test 3 = ( 0, 0) - (+1,-1) = (-1,+1)
- Test 4 = ( 0, 0) - ( 0,+2) = ( 0,-2)
- Test 5 = ( 0, 0) - (+1,+2) = (-1,-2)


Manual Tilemap Detection

[pseudocode]

- did not use any Collision systems
- manually track a 'core' tile within a Tetromino piece
- using tilemap logic, checks the neighboring tiles
- left = index - 1, right = index + 1, bottom = index + map_width, top = index - map_width

- when moving left or right, stop if a tile is hit
- when moving down, check if a tile is hit
- if a tile is hit, build the tetromino using the 'core' tile
- on each row being built on, check if it is 'full'
- if full, clear row and use logic to repack the map


Ghost Piece

[pseudocode]

- use of sprite outlines for each type of Tetromino
- rotate and reposition each ghost sprite when rotating the Tetromino

- ghost.position_x = tetromino.position_x
- starting at tetromino.position_y
- check each row to see if any blocks is hit (until outside map)
if row is empty, ghost.position_y + 1
once a block is hit, stop the checks


Piece Generation

[pseudocode]
var pieces: Array[int] = [0, 1, 2, 3, 4, 5, 6]
var next_index: int
var next_value: int
on piece lock
    next_index += 1
    if next_index == 7:
        pieces.shuffle()
        next_index = 0
    piece = next_value
    next_value = pieces[next_idx]
StatusReleased
PlatformsHTML5
AuthorQuietGodot
Made withGodot

Leave a comment

Log in with itch.io to leave a comment.