C# - Tetris.Net
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 gameplay, you can rotate the falling blocks clockwise by pressing the UP arrow button, or anti-clockwise 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 Game Class
The Game 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 Game 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.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace tetris_cs
{
/// <summary>
/// Extended DataGridView
/// DoubleBuffered. Restricts user selection of cells.
/// </summary>
/// <remarks></remarks>
class game : DataGridView
{
//constants used with Keypresses
private const int WM_LBUTTONDOWN = 0x201;
private const int WM_LBUTTONDBLCLK = 0x203;
private const int WM_KEYDOWN = 0x100;
private const int VK_LEFT = 0x25;
private const int VK_RIGHT = 0x27;
private const int VK_DOWN = 0x28;
private const int VK_UP = 0x26;
//custom events
public delegate void IncrementScoreEventHandler(int newPoints);
public event IncrementScoreEventHandler IncrementScore;
public delegate void ShapeChangedEventHandler(Point[] shapePoints, string shapeColor);
public event ShapeChangedEventHandler ShapeChanged;
private int rowCounter = 0;
//set up timer handlers
public game()
{
this.DoubleBuffered = true;
tmr.Tick += tmr_Tick;
flashtmr.Tick += flashtmr_Tick;
}
/// <summary>
/// OnRowPrePaint
/// Avoid DGV cell focussing
/// </summary>
/// <param name="e"></param>
protected override void OnRowPrePaint(DataGridViewRowPrePaintEventArgs e)
{
int p = (int)e.PaintParts;
p -= (int)DataGridViewPaintParts.Focus;
e.PaintParts = (DataGridViewPaintParts)p;
base.OnRowPrePaint(e);
}
/// <summary>
/// WndProc
/// Avoid DGV focussing, and catch Keypresses
/// </summary>
/// <param name="m"></param>
protected override void WndProc(ref System.Windows.Forms.Message m)
{
if (m.Msg == WM_LBUTTONDBLCLK || m.Msg == WM_LBUTTONDOWN)
{
return;
}
else if (m.Msg == WM_KEYDOWN)
{
//System.Diagnostics.Debugger.Break();
if (m.WParam.ToInt32() == VK_LEFT)
{
moveLeft();
}
else if (m.WParam.ToInt32() == VK_RIGHT)
{
moveRight();
}
else if (m.WParam.ToInt32() == VK_DOWN)
{
moveDown();
}
else if (m.WParam.ToInt32() == VK_UP)
{
rotateShape();
}
return;
}
base.WndProc(ref m);
}
//timers used in game play
private Timer tmr = new Timer { Interval = 500 };
private Timer flashtmr = new Timer { Interval = 125 };
//variables used with flashtmr tick event
private int flashCounter = 1;
private int flashRow;
private bool missATick = false;
//variables holding cell colors and shapes
private string[][] gameGrid;
private Shape currentShape;
private List<Shape> listShapes = new List<Shape>();
private Random r = new Random();
private int moveCounter = 0;
//clears the game board and score and initiates a new game
public void newGame()
{
tmr.Stop();
tmr.Interval = 500;
listShapes.Clear();
moveCounter = 0;
gameGrid = new string[30][];
for (int x = 1; x <= 30; x++)
{
string[] row = new string[20];
gameGrid[x - 1] = (string[])row.Clone();
}
newShape();
currentShape = listShapes[0];
if (ShapeChanged != null) {
ShapeChanged(currentShape.CurrentPoints, currentShape.ShapeColor);
}
rowCounter = 0;
tmr.Start();
flashtmr.Start();
}
//creates a new falling shape
private void newShape()
{
string[] sc = { "R", "G", "B", "Y" };
Shape ns = new Shape(r.Next(0, 7), sc[r.Next(0, 4)]);
listShapes.Add(ns);
//currentShape = ns
ns.TouchDown += currentShape_TouchDown;
HasChanged(gameGrid, false, -1);
}
//responds to LEFT arrow button keydown
public void moveLeft()
{
if (currentShape == null)
{
return;
}
gameGrid = currentShape.moveLeft(gameGrid);
HasChanged(gameGrid, false, -1);
}
//responds to RIGHT arrow button keydown
public void moveRight()
{
if (currentShape == null)
{
return;
}
gameGrid = currentShape.moveRight(gameGrid);
HasChanged(gameGrid, false, -1);
}
//responds to DOWN arrow button keydown
public void moveDown()
{
do
{
for (int x = 0; x < listShapes.Count; x++)
{
if (x > listShapes.Count - 1)
{
goto ContinueLabel1;
}
gameGrid = listShapes[x].moveDown(gameGrid);
HasChanged(gameGrid, false, -1);
}
break;
ContinueLabel1:;
} while (true);
moveCounter += 1;
}
//responds to UP arrow button keydown
public void rotateShape()
{
if (currentShape == null)
{
return;
}
gameGrid = currentShape.rotateShape(gameGrid);
HasChanged(gameGrid, false, -1);
if (ShapeChanged != null)
{
ShapeChanged(currentShape.CurrentPoints, currentShape.ShapeColor);
}
}
//on tick, all shapes move down one row
private void tmr_Tick(object sender, EventArgs e)
{
if (missATick)
{
return;
}
if (moveCounter >= 27)
{
moveCounter = 0;
newShape();
if (listShapes.Count == 1)
{
currentShape = listShapes[0];
if (ShapeChanged != null)
{
ShapeChanged(currentShape.CurrentPoints, currentShape.ShapeColor);
}
}
}
moveDown();
}
//responds to shape touchdown
private void currentShape_TouchDown(Shape sender)
{
if (sender.CurrentPoints.Any((p) => p.Y < 0))
{
tmr.Stop();
}
currentShape.TouchDown -= currentShape_TouchDown;
listShapes.Remove(sender);
if (listShapes.Count < 1)
{
currentShape = null;
moveCounter = 27;
}
else
{
currentShape = listShapes[0];
if (ShapeChanged != null)
{
ShapeChanged(currentShape.CurrentPoints, currentShape.ShapeColor);
}
}
}
//clears full rows as they occur
private void flashtmr_Tick(object sender, EventArgs e)
{
switch (flashCounter)
{
case 1:
flashRow = findFullRow();
if (flashRow > -1)
{
flashCounter = 2;
HasChanged(gameGrid, true, flashRow);
}
break;
case 2:
flashCounter = 3;
HasChanged(gameGrid, false, -1);
break;
case 3:
flashCounter = 4;
HasChanged(gameGrid, true, flashRow);
break;
case 4:
List<string[]> newGrid = new List<string[]>(gameGrid);
if (listShapes.Count == 0) { return; }
foreach (Point p in listShapes.Last().CurrentPoints)
{
if (p.Y > -1)
{
newGrid[p.Y][p.X] = "";
}
}
string[] newRow = new string[20];
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 % 10 == 0)
{
tmr.Interval -= 40;
if (IncrementScore != null)
IncrementScore(((int)((1000 - tmr.Interval) * 0.35)));
}
else if (rowCounter % 5 == 0)
{
tmr.Interval -= 20;
if (IncrementScore != null)
IncrementScore(((int)((1000 - tmr.Interval) * 0.25)));
}
else
{
if (IncrementScore != null)
IncrementScore(((int)((1000 - tmr.Interval) * 0.05)));
}
break;
}
}
//finds full rows in DGV
private int findFullRow()
{
for (int x = 29; x >= 0; x--)
{
if (gameGrid[x].All((s) => !string.IsNullOrEmpty(s)))
{
return x;
}
}
return -1;
}
//renders the colors in the DGV
private void HasChanged(string[][] grid, bool flash, int flashRow)
{
Dictionary<string, Color> colors = new Dictionary<string, Color>()
{
{"R", Color.Red},
{"G", Color.Green},
{"B", Color.Blue},
{"Y", Color.Yellow}
};
Dictionary<string, Color> flashColors = new Dictionary<string, Color>()
{
{"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 (int y = 0; y <= 29; y++)
{
for (int x = 0; x <= 19; x++)
{
if (string.IsNullOrEmpty(grid[y][x]))
{
this.Rows[y].Cells[x].Style.BackColor = Color.Black;
}
else
{
if (!flash || (flash && !(flashRow == y)))
{
this.Rows[y].Cells[x].Style.BackColor = colors[grid[y][x]];
}
else
{
this.Rows[y].Cells[x].Style.BackColor = flashColors[grid[y][x]];
}
}
}
}
}
}
}
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
See Also
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#