Overview

Tetris (Russian: «Тетрис», pronounced [ˈtɛtrʲɪs]) is a tile-matching puzzle video game, originally designed and programmed by Russian game designer Alexey Pajitnov.[https://en.wikipedia.org/wiki/Alexey_Pajitnov] It was released on June 6, 1984, while he was working for the Dorodnitsyn Computing Centre of the Academy of Science of the Soviet Union in Moscow. He derived its name from the Greek numerical prefix tetra- (all of the game's pieces contain four segments) and tennis, Pajitnov's favorite sport.

This is a .Net version of Tetris, that uses two extended DataGridViews, one as the falling blocks game board and one for the shapes preview, alongside the game board.

During game play, you can rotate the falling blocks clock-wise by pressing the UP arrow button, or anti clock-wise by pressing Shift and the UP arrow. LEFT and RIGHT arrow buttons move the current shape left or right, and the DOWN arrow button increases the speed of descent. 
As you form complete rows on the bottom of the game board, those rows flash several times before disappearing, and your score (displayed in a label to the right of the game board) is incremented. As you progress in the game, the speed of descent of the falling shapes increases as you make more full rows.







The GameGrid Class

The GameGrid Class is an extended DataGridView, which allows no user input other than the arrow key game commands and the Shift key. All of the game play is coordinated by the GameGrid class which raises custom events to pass score change information and shape changed information to the Form, which renders these changes in a Label for the score, and another extended DataGridView, which shows the current shape.


''' <summary>
''' Extended DataGridView
''' DoubleBuffered. Restricts user selection of cells.
''' </summary>
''' <remarks></remarks>
Public Class GameGrid
    Inherits DataGridView
 
    'constants used with Keypresses
    Const WM_LBUTTONDOWN As Integer = &H201
    Const WM_LBUTTONDBLCLK As Integer = &H203
    Const WM_KEYDOWN As Integer = &H100
    Const VK_LEFT As Integer = &H25
    Const VK_RIGHT As Integer = &H27
    Const VK_DOWN As Integer = &H28
    Const VK_UP As Integer = &H26
 
    'custom events
    Public Event IncrementScore(newPoints As Integer)
    Public Event ShapeChanged(shapePoints() As Point, shapeColor As String)
 
    Private rowCounter As Integer = 0
 
    Public Sub New()
        Me.DoubleBuffered = True
    End Sub
 
    ''' <summary>
    ''' OnRowPrePaint
    ''' Avoid DGV cell focussing
    ''' </summary>
    ''' <param name="e"></param>
    Protected Overrides Sub OnRowPrePaint(ByVal e As System.Windows.Forms.DataGridViewRowPrePaintEventArgs)
        e.PaintParts = e.PaintParts And Not DataGridViewPaintParts.Focus
        MyBase.OnRowPrePaint(e)
    End Sub
 
    ''' <summary>
    ''' WndProc
    ''' Avoid DGV focussing, and catch Keypresses
    ''' </summary>
    ''' <param name="m"></param>
    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
        If m.Msg = WM_LBUTTONDBLCLK OrElse m.Msg = WM_LBUTTONDOWN Then
            Return
        ElseIf m.Msg = WM_KEYDOWN Then
            If m.WParam.ToInt32 = VK_LEFT Then
                moveLeft()
            ElseIf m.WParam.ToInt32 = VK_RIGHT Then
                moveRight()
            ElseIf m.WParam.ToInt32 = VK_DOWN Then
                moveDown()
            ElseIf m.WParam.ToInt32 = VK_UP Then
                rotateShape()
            End If
            Return
        End If
        MyBase.WndProc(m)
    End Sub
 
    'timers used in game play
    Private WithEvents tmr As New Timer With {.Interval = 500}
    Private WithEvents flashtmr As New Timer With {.Interval = 125}
 
    'variables used with flashtmr tick event
    Private flashCounter As Integer = 1
    Private flashRow As Integer
    Private missATick As Boolean = False
 
    'variables holding cell colors and shapes
    Private gameGrid()() As String
    Private currentShape As Shape
    Private listShapes As New List(Of Shape)
 
    Private r As New Random
    Private moveCounter As Integer = 0
 
    'clears the game board and score and initiates a new game
    Public Sub newGame()
        tmr.Stop()
        tmr.Interval = 500
        listShapes.Clear()
        moveCounter = 0
 
        ReDim gameGrid(29)
        For x As Integer = 1 To 30
            Dim row(19) As String
            gameGrid(x - 1) = DirectCast(row.Clone, String())
        Next
        newShape()
        currentShape = listShapes(0)
        RaiseEvent ShapeChanged(currentShape.CurrentPoints, currentShape.ShapeColor)
        rowCounter = 0
        tmr.Start()
        flashtmr.Start()
    End Sub
 
    'creates a new falling shape
    Private Sub newShape()
        Dim sc() As String = {"R", "G", "B", "Y"}
        Dim ns As New Shape(r.Next(0, 7), sc(r.Next(0, 4)))
        listShapes.Add(ns)
        'currentShape = ns
        AddHandler ns.TouchDown, AddressOf currentShape_TouchDown
        HasChanged(gameGrid, False, -1)
    End Sub
 
    'responds to LEFT arrow button keydown
    Public Sub moveLeft()
        If currentShape Is Nothing Then Return
        gameGrid = currentShape.moveLeft(gameGrid)
        HasChanged(gameGrid, False, -1)
    End Sub
 
    'responds to RIGHT arrow button keydown
    Public Sub moveRight()
        If currentShape Is Nothing Then Return
        gameGrid = currentShape.moveRight(gameGrid)
        HasChanged(gameGrid, False, -1)
    End Sub
 
    'responds to DOWN arrow button keydown
    Public Sub moveDown()
        Do
            For x As Integer = 0 To listShapes.Count - 1
                If x > listShapes.Count - 1 Then Continue Do
                gameGrid = listShapes(x).moveDown(gameGrid)
                HasChanged(gameGrid, False, -1)
            Next
            Exit Do
        Loop
        moveCounter += 1
    End Sub
    '
    'responds to UP arrow button keydown
    Public Sub rotateShape()
        If currentShape Is Nothing Then Return
        gameGrid = currentShape.rotateShape(gameGrid)
        HasChanged(gameGrid, False, -1)
        RaiseEvent ShapeChanged(currentShape.CurrentPoints, currentShape.ShapeColor)
    End Sub
 
    'on tick, all shapes move down one row
    Private Sub tmr_Tick(sender As Object, e As EventArgs) Handles tmr.Tick
        If missATick Then Return
        If moveCounter >= 27 Then
            moveCounter = 0
 
            newShape()
            If listShapes.Count = 1 Then
                currentShape = listShapes(0)
                RaiseEvent ShapeChanged(currentShape.CurrentPoints, currentShape.ShapeColor)
            End If
        End If
        moveDown()
    End Sub
 
    'responds to shape touchdown
    Private Sub currentShape_TouchDown(sender As Shape)
        If sender.CurrentPoints.Any(Function(p) p.Y < 0) Then tmr.Stop()
        RemoveHandler currentShape.TouchDown, AddressOf currentShape_TouchDown
        listShapes.Remove(sender)
        If listShapes.Count < 1 Then
            currentShape = Nothing
            moveCounter = 27
        Else
            currentShape = listShapes(0)
            RaiseEvent ShapeChanged(currentShape.CurrentPoints, currentShape.ShapeColor)
        End If
    End Sub
 
    'clears full rows as they occur
    Private Sub flashtmr_Tick(sender As Object, e As EventArgs) Handles flashtmr.Tick
        Select Case flashCounter
            Case 1
                flashRow = findFullRow()
                If flashRow > -1 Then
                    flashCounter = 2
                    HasChanged(gameGrid, True, flashRow)
                End If
            Case 2
                flashCounter = 3
                HasChanged(gameGrid, False, -1)
            Case 3
                flashCounter = 4
                HasChanged(gameGrid, True, flashRow)
            Case 4
                Dim newGrid As New List(Of String())(gameGrid)
                For Each p As Point In listShapes.Last.CurrentPoints
                    If p.Y > -1 Then
                        newGrid(p.Y)(p.X) = ""
                    End If
                Next
                Dim newRow(19) As String
                newGrid.RemoveAt(flashRow)
                newGrid.Insert(0, newRow)
                missATick = True
                gameGrid = newGrid.ToArray
                flashCounter = 1
                moveDown()
                HasChanged(gameGrid, False, -1)
                missATick = False
                rowCounter += 1
                If rowCounter Mod 10 = 0 Then
                    tmr.Interval -= 40
                    RaiseEvent IncrementScore(((1000 - tmr.Interval) * 0.35))
                ElseIf rowCounter Mod 5 = 0 Then
                    tmr.Interval -= 20
                    RaiseEvent IncrementScore(((1000 - tmr.Interval) * 0.25))
                Else
                    RaiseEvent IncrementScore(((1000 - tmr.Interval) * 0.05))
                End If
        End Select
    End Sub
 
    'finds full rows in DGV
    Private Function findFullRow() As Integer
        For x As Integer = 29 To 0 Step -1
            If gameGrid(x).All(Function(s) Not String.IsNullOrEmpty(s)) Then Return x
        Next
        Return -1
    End Function
 
    'renders the colors in the DGV
    Private Sub HasChanged(grid As String()(), flash As Boolean, flashRow As Integer)
        Dim colors As New Dictionary(Of String, Color) From {{"R", Color.Red}, {"G", Color.Green}, {"B", Color.Blue}, {"Y", Color.Yellow}}
        Dim flashColors As New Dictionary(Of String, Color) From {{"R", Color.FromArgb(255, 165, 165)}, {"G", Color.FromArgb(165, 255, 165)}, {"B", Color.FromArgb(165, 165, 255)}, {"Y", Color.FromArgb(255, 255, 230)}}
        For y As Integer = 0 To 29
            For x As Integer = 0 To 19
                If String.IsNullOrEmpty(grid(y)(x)) Then
                    Me.Rows(y).Cells(x).Style.BackColor = Color.Black
                Else
                    If Not flash OrElse (flash And Not flashRow = y) Then
                        Me.Rows(y).Cells(x).Style.BackColor = colors(grid(y)(x))
                    Else
                        Me.Rows(y).Cells(x).Style.BackColor = flashColors(grid(y)(x))
                    End If
                End If
            Next
        Next
    End Sub
 
End Class



The Shapes

The Shapes are defined by a Shape class. Only the lower-most shape - the currentshape - can be moved or rotated. On touchdown, the currentshape ceases to be a Shape and becomes just cell BackGroundColors in the DataGridView.


Conclusion

The .Net programming languages VB.Net and C# can be used, as shown in this example, to create some fairly advanced Graphical Games. Using OOP techniques, it's possible to create concise coding and fairly impressive output, not only in Gaming applications, but in all kinds of application...


Downloads

Vb.Net and C#


See Also

C# version


Articles related to game programming


VB.Net - WordSearch
VB.Net - Vertex
VB.Net - Perspective
VB.Net - MasterMind
VB.Net - OOP BlackJack
VB.Net - Numbers Game
VB.Net - HangMan
Console BlackJack - VB.Net | C#
TicTacToe - VB.Net | C#
OOP Sudoku - VB.Net | C#
OctoWords VB.Net | C#
OOP Buttons Guessing Game VB.Net | C#
OOP Tangram Shapes Game VB.Net | C#