Tetris With C/C++ (Win32)

How to program Tetris.

Updated July 5, 2022

Table of contents

main.cpp

#define WIN32_LEAN_AND_MEAN
#define UNICODE
#include <windows.h>
#include <stdint.h>
#include <stdlib.h>
#include <time.h>
#include <stdio.h>

LRESULT CALLBACK WindowProc(HWND Window, UINT Message, WPARAM WParam, LPARAM LParam);

#include "engine.h"
#include "game.h"

LRESULT CALLBACK WindowProc(HWND Window, UINT Message, WPARAM WParam, LPARAM LParam) {
    switch(Message) {
        case WM_KEYDOWN: {
            switch(WParam) { 
                case 'O': { DestroyWindow(Window); } break;
                case VK_UP: { Rotate(); } break;
                case VK_LEFT: { Move(LEFT); } break;
                case VK_DOWN: { Move(DOWN); } break;
                case VK_RIGHT: { Move(RIGHT); } break;
                case VK_SPACE: { Drop(); } break;
            }
        } break;
        case WM_DESTROY: { PostQuitMessage(0); } break;
        default: { 
            return DefWindowProc(Window, Message, WParam,  LParam); 
        }
    }
    return 0;
}

int WINAPI wWinMain(HINSTANCE Instance, HINSTANCE PrevInstance, PWSTR CmdLine, int CmdShow) {
    
    srand((unsigned int)time(NULL));
    
    window Window = {};
    Window.ClientWidth = TILE_SIZE * BOARD_X_TILES;
    Window.ClientHeight = TILE_SIZE * BOARD_Y_TILES;
    Window.Center = true;
    DWORD Error = InitWindow(&Window, Instance);
    if(Error) { return Error; }
    
    bitmap Bitmap = {};
    Error = InitBitmap(&Bitmap, &Window);
    if(Error) { return Error; }
    
    timer Timer = {};
    Error = InitTimer(&Timer);
    if(Error) { return Error; }
    
    CurrentBlock = GetRandomBlock();
    
    bool Running = true;
    
    while(Running) {
        MSG Message;
        while(PeekMessage(&Message, nullptr, 0, 0, PM_REMOVE)) {
            if(Message.message == WM_QUIT) Running = false;
            TranslateMessage(&Message);
            DispatchMessage(&Message);
        }
        
        UpdateTimer(&Timer);
        
        if(Timer.ElapsedMilliSeconds > 500) {
            Update();
            StartTimer(&Timer);
        }
        
        ClearScreen(0x333333, &Bitmap);
        DrawStack(&Bitmap);
        DrawBlock(&CurrentBlock, &Bitmap);
        Blit(&Bitmap, &Window);
    }
    
    return 0;
}

game.h

#define SHAPES 7
#define ROTATIONS 4
#define TILE_SIZE 32
#define BOARD_X_TILES 10
#define BOARD_Y_TILES 16

typedef enum { LEFT, RIGHT, UP, DOWN } direction;

typedef struct {
	int X;
	int Y;
	int Shape;
	int Rotation;
	u32 Color;
} block;

// Globals

u32 StackColors[BOARD_X_TILES * BOARD_Y_TILES];

u32 BlockColors[SHAPES] = {
	0x20B2AA, 0x4169E1, 0xffa500, 0xFFFF33,
	0x32CD32, 0xBA55D3, 0xff0000
};

u16 Shapes[SHAPES][ROTATIONS] = {
    {0x0f00, 0x2222, 0x00f0, 0x4444}, // I
    {0x8e00, 0x6440, 0x0e20, 0x44c0}, // J
    {0x2e00, 0x4460, 0x0e80, 0xc440}, // L
    {0x6600, 0x6600, 0x6600, 0x6600}, // O
    {0x6c00, 0x4620, 0x06c0, 0x8c40}, // S
    {0x4e00, 0x4640, 0x0e40, 0x4c40}, // T
    {0xc600, 0x2640, 0x0c60, 0x4c80}, // Z
};

int WallKickOffsetsCommon[8][10] = {    // all except I
    0, 0, -1, 0,  -1, 1,  0,-2, -1,-2,  // 0->R
    0, 0,  1, 0,   1,-1,  0, 2,  1, 2,  // R->0
    0, 0,  1, 0,   1,-1,  0, 2,  1, 2,  // R->2
    0, 0, -1, 0,  -1, 1,  0,-2, -1,-2,  // 2->R
    
    0, 0,  1, 0,   1, 1,  0,-2,  1,-2,  // 2->L
    0, 0, -1, 0,  -1,-1,  0, 2, -1, 2,  // L->2
    0, 0, -1, 0,  -1,-1,  0, 2, -1, 2,  // L->0
    0, 0,  1, 0,   1, 1,  0,-2,  1,-2,  // 0->L
};

int WallKickOffsetsI[8][10] = {         // I
    0, 0, -2, 0,  1, 0,  -2,-1,  1, 2,  // 0->R
    0, 0,  2, 0, -1, 0,   2, 1, -1,-2,  // R->0
    0, 0, -1, 0,  2, 0,  -1, 2,  2,-1,  // R->2
    0, 0,  1, 0, -2, 0,   1,-2, -2, 1,  // 2->R
    
    0, 0,  2, 0, -1, 0,   2, 1, -1,-2,  // 2->L
    0, 0, -2, 0,  1, 0,  -2,-1,  1, 2,  // L->2
    0, 0,  1, 0, -2, 0,   1,-2, -2, 1,  // L->0
    0, 0, -1, 0,  2, 0,  -1, 2,  2,-1,  // 0->L
};

block CurrentBlock;

// Declarations

void Rotate();
int Collides(block *Block);
int WithinBounds(int X, int Y);
void Move(direction Direction);
void Drop();
block GetRandomBlock();

void Drop() {
	block TempBlock = CurrentBlock;
	while(!Collides(&TempBlock)) {
		TempBlock.Y += TILE_SIZE;
	}
	CurrentBlock.Y = TempBlock.Y - TILE_SIZE;
}

void WallKick(block *TempBlock) {
	int *Offsets = NULL;
	
	if(TempBlock->Shape == 0) {
		Offsets = &WallKickOffsetsI[0][0];
	} else {
		Offsets = &WallKickOffsetsCommon[0][0];
	}
    
	int OffsetRow = 0;
	
	int R1 = CurrentBlock.Rotation;
	int R2 = TempBlock->Rotation;
    
	if(R1 == 0 && R2 == 1) { OffsetRow = 0; }
	else if(R1 == 1 && R2 == 2) { OffsetRow = 2; }
	else if(R1 == 2 && R2 == 3) { OffsetRow = 4; }
	else if(R1 == 3 && R2 == 0) { OffsetRow = 6; }
    
	for(int I = 0; I < 10; I += 2) {
		TempBlock->X = CurrentBlock.X +
			*(Offsets + OffsetRow * 10 + I + 0) * TILE_SIZE;
		TempBlock->Y = CurrentBlock.Y +
			*(Offsets + OffsetRow * 10 + I + 1) * TILE_SIZE;
		if(!Collides(TempBlock)) {
			CurrentBlock = *TempBlock;
			return;
		}
	}
}

void Rotate() {
	block TempBlock = CurrentBlock;
	++TempBlock.Rotation;
	if(TempBlock.Rotation == 4) { TempBlock.Rotation = 0; }
    WallKick(&TempBlock);
	if(Collides(&TempBlock)) { return; };  
	CurrentBlock = TempBlock;
}

void Move(direction Direction) {
	block TempBlock = CurrentBlock;
	switch(Direction) {
		case LEFT: { TempBlock.X -= TILE_SIZE; } break;
		case RIGHT: { TempBlock.X += TILE_SIZE; } break;
		case DOWN: { TempBlock.Y += TILE_SIZE; } break;
	}
	if(Collides(&TempBlock)) { return; };
	CurrentBlock = TempBlock;
}

int IsBlockTile(block *Block, int X, int Y) {
	if((0x8000 >> (Y * 4 + X)) &
       Shapes[Block->Shape][Block->Rotation]) {
		return 1;
	}
	return 0;
}

void DrawStack(bitmap *Bitmap) {
	for(int Y = 0; Y < BOARD_Y_TILES; ++Y) {
		for(int X = 0; X < BOARD_X_TILES; ++X) {
			if(StackColors[Y * BOARD_X_TILES + X] != 0) {
				DrawRectangleBordered(
                                      X * TILE_SIZE,
                                      Y * TILE_SIZE,
                                      TILE_SIZE, TILE_SIZE,
                                      StackColors[Y * BOARD_X_TILES + X],
                                      1, 0x000000,
                                      Bitmap);
			}
		}
	}
}

void DrawBlock(block *Block, bitmap *Bitmap) {
	for(int Y = 0; Y < 4; ++Y) {
		for(int X = 0; X < 4; ++X) {
			if(IsBlockTile(Block, X, Y)) {
				DrawRectangleBordered(
                                      Block->X + (X * TILE_SIZE),
                                      Block->Y + (Y * TILE_SIZE),
                                      TILE_SIZE, TILE_SIZE,
                                      Block->Color,
                                      1, 0x000000,
                                      Bitmap
                                      );
			}
		}
	}
}

block GetRandomBlock() {
    block Block = {};
    Block.X = 4 * TILE_SIZE;
    Block.Y = 0;
    Block.Shape = rand() % SHAPES;
    Block.Rotation = rand() % ROTATIONS;
    Block.Color = BlockColors[Block.Shape];
    return Block;
}

void DeleteStackRow(int Row) {
	for(int Y = Row; Y > 0; --Y) {
		for(int X = 0; X < BOARD_X_TILES; ++X) {
			StackColors[Y * BOARD_X_TILES + X] =
				StackColors[(Y-1) * BOARD_X_TILES + X];
		}
	}
}

void UpdateStackRows() {
	for(int Y = 0; Y < BOARD_Y_TILES; ++Y) {
		int LineCounter = 0;
		for(int X = 0; X < BOARD_X_TILES; ++X) {
			if(StackColors[Y * BOARD_X_TILES + X] != 0) {
				++LineCounter;
			}
		}
		if(LineCounter == BOARD_X_TILES) {
			DeleteStackRow(Y);
		}
	}
}

void AddStack(block *Block) {
    for(int Y = 0; Y < 4; ++Y) {
		for(int X = 0; X < 4; ++X) {
			if(IsBlockTile(Block, X, Y)) {  
				StackColors[
                            (Block->Y / TILE_SIZE + Y) * BOARD_X_TILES +
                            Block->X / TILE_SIZE + X] = Block->Color;
			}
		}
	}
    UpdateStackRows();
    CurrentBlock = GetRandomBlock();
}

int Collides(block *Block) {
	for(int Y = 0; Y < 4; ++Y) {
		for(int X = 0; X < 4; ++X) {
			if(IsBlockTile(Block, X, Y)) {  
				if(!WithinBounds(Block->X + X * TILE_SIZE, Block->Y + Y * TILE_SIZE)) {
					return 1;
				}
				if(StackColors[(Block->Y / TILE_SIZE + Y) * BOARD_X_TILES + Block->X / TILE_SIZE + X] != 0) {
					return 1;
				}
			}
		}
	}
	return 0;
}

int WithinBounds(int X, int Y) {
	if(Y < 0 || Y >= BOARD_Y_TILES * TILE_SIZE) { return 0; }
	if(X < 0 || X >= BOARD_X_TILES * TILE_SIZE) { return 0; }
	return 1;
}

void Update() {
	block TempBlock = CurrentBlock;
	TempBlock.Y += TILE_SIZE;
	if(!Collides(&TempBlock)) {
		CurrentBlock.X = TempBlock.X;
		CurrentBlock.Y = TempBlock.Y;
	} else {
		AddStack(&CurrentBlock);
	}
}

engine.h

typedef uint16_t u16;
typedef uint32_t u32;

typedef struct {
    HWND Handle;
    HDC DeviceContext;
    int ClientWidth;
    int ClientHeight;
    bool Center;
} window;

typedef struct {
    void* Memory;
    BITMAPINFO Info;
    int Width;
    int Height;
} bitmap;

typedef struct {
    LARGE_INTEGER StartingCount;
    LARGE_INTEGER EndingCount;
    LARGE_INTEGER TotalCount;
    LARGE_INTEGER CountsPerSecond;
    double ElapsedMilliSeconds;
} timer;

void DrawPixel(int X, int Y, u32 Color, bitmap *Bitmap) {
    u32 *Pixel = (u32 *)Bitmap->Memory;
    Pixel += Y * Bitmap->Width + X;
    *Pixel = Color;
}

void ClearScreen(u32 Color, bitmap *Bitmap) {
    u32 *Pixel = (u32 *)Bitmap->Memory;
    for(int Index = 0; 
        Index < Bitmap->Width * Bitmap->Height;
        ++Index) { *Pixel++ = Color; }
}

void DrawRectangle(int RectangleX,
                   int RectangleY,
                   int RectangleWidth,
                   int RectangleHeight,
                   u32 Color,
                   bitmap *Bitmap) {
    
    u32 *Pixel = (u32 *)Bitmap->Memory;
    Pixel += RectangleY * Bitmap->Width + RectangleX;
    
    for(int Y = 0; Y < RectangleHeight; ++Y) {
        for(int X = 0;
            X < RectangleWidth;
            ++X) { *Pixel++ = Color; }
        Pixel += Bitmap->Width - RectangleWidth;
    }
}

void DrawRectangleBordered(int RectangleX, int RectangleY,
                           int RectangleWidth, int RectangleHeight, 
                           u32 Color,
                           int BorderSize, u32 BorderColor,
                           bitmap *Bitmap) {
    
	DrawRectangle(RectangleX, RectangleY, RectangleWidth, RectangleHeight, BorderColor, Bitmap);
	DrawRectangle(RectangleX + BorderSize,
                  RectangleY + BorderSize,
                  RectangleWidth - BorderSize * 2,
                  RectangleHeight - BorderSize * 2,
                  Color, Bitmap);
}

void Blit(bitmap *Bitmap, window *Window) {
    StretchDIBits(Window->DeviceContext, 
                  0, 0,
                  Bitmap->Width, Bitmap->Height,
                  0, 0,
                  Window->ClientWidth, Window->ClientHeight,
                  Bitmap->Memory, &Bitmap->Info,
                  DIB_RGB_COLORS, SRCCOPY);
}

DWORD InitWindow(window *Window, HINSTANCE Instance) {
    
    // WindowClass
    
    WNDCLASS WindowClass = {};
    const wchar_t ClassName[] = L"MyWindowClass";
    WindowClass.lpfnWndProc = WindowProc;
    WindowClass.hInstance = Instance;
    WindowClass.lpszClassName = ClassName;
    WindowClass.hCursor = LoadCursor(0, IDC_CROSS);
    
    if(!RegisterClass(&WindowClass)) {
        MessageBox(0, L"RegisterClass failed", 0, 0);
        return GetLastError();
    }
    
    // Window dimensions
    
    int WindowWidth = CW_USEDEFAULT;
    int WindowHeight = CW_USEDEFAULT;
    
    // Adjust window dimensions to given client area
    // Window dimensions will be client area dimensions + some units for the borders and title bar
    
    if(Window->ClientWidth > 0 && 
       Window->ClientHeight > 0) {
        
        RECT WindowRect;
        WindowRect.left = 0;
        WindowRect.top = 0;
        WindowRect.right = Window->ClientWidth;
        WindowRect.bottom = Window->ClientHeight;
        
        AdjustWindowRectEx(&WindowRect, 
                           WS_OVERLAPPEDWINDOW, 0, 0);
        
        WindowWidth = 
            WindowRect.right - WindowRect.left;
        WindowHeight = 
            WindowRect.bottom - WindowRect.top;
    }
    
    // Window position
    
    int WindowXPos = CW_USEDEFAULT;
    int WindowYPos = CW_USEDEFAULT;
    
    if(Window->Center) {
        int ScreenWidth = GetSystemMetrics(SM_CXSCREEN);
        int ScreenHeight = GetSystemMetrics(SM_CYSCREEN);
        
        WindowXPos = (ScreenWidth / 2) - (WindowWidth / 2);
        WindowYPos = (ScreenHeight / 2) - (WindowHeight / 2);
    }
    
    Window->Handle = CreateWindowEx(0, ClassName, L"Program",
                                    WS_OVERLAPPEDWINDOW|WS_VISIBLE,
                                    WindowXPos, 
                                    WindowYPos,
                                    WindowWidth, WindowHeight,
                                    0, 0, Instance, 0);
    
    if(!Window->Handle) {
        MessageBox(0, L"CreateWindowEx failed", 0, 0);
        return GetLastError();
    }
    
    // For StretchDIBits
    
    Window->DeviceContext = GetDC(Window->Handle);
    
    return 0;
}

DWORD InitBitmap(bitmap *Bitmap, window *Window) {
    
    // Get client area dimensions 
    
    RECT ClientRect;
    GetClientRect(Window->Handle, &ClientRect);
    Window->ClientWidth = ClientRect.right - ClientRect.left;
    Window->ClientHeight = ClientRect.bottom - ClientRect.top;
    
    Bitmap->Width = Window->ClientWidth;
    Bitmap->Height = Window->ClientHeight;
    
    // Allocate memory for the bitmap
    
    int BytesPerPixel = 4;
    
    Bitmap->Memory = VirtualAlloc(0,
                                  Bitmap->Width * Bitmap->Height * BytesPerPixel,
                                  MEM_RESERVE|MEM_COMMIT,
                                  PAGE_READWRITE
                                  );
    
    // BitmapInfo struct for StretchDIBits
    
    Bitmap->Info.bmiHeader.biSize = sizeof(Bitmap->Info.bmiHeader);
    Bitmap->Info.bmiHeader.biWidth = Bitmap->Width;
    // Negative height makes top left as the coordinate system origin for the DrawPixel function, otherwise its bottom left
    Bitmap->Info.bmiHeader.biHeight = -Bitmap->Height;
    Bitmap->Info.bmiHeader.biPlanes = 1;
    Bitmap->Info.bmiHeader.biBitCount = 32;
    Bitmap->Info.bmiHeader.biCompression = BI_RGB;
    
    return 0;
}

void StartTimer(timer *Timer) {
    QueryPerformanceCounter(&Timer->StartingCount);
}

void UpdateTimer(timer *Timer) {
    // get current tick
    QueryPerformanceCounter(&Timer->EndingCount);
    // get tick count
    Timer->TotalCount.QuadPart = Timer->EndingCount.QuadPart - Timer->StartingCount.QuadPart;
    // for precision
    Timer->TotalCount.QuadPart *= 1000000;
    // calculate elapsed milliseconds
    Timer->ElapsedMilliSeconds = (double)(Timer->TotalCount.QuadPart / Timer->CountsPerSecond.QuadPart) / 1000;
}

DWORD InitTimer(timer *Timer) {
    if(!QueryPerformanceFrequency(&Timer->CountsPerSecond)) {
        return GetLastError();
    }
    StartTimer(Timer);
    return 0;
}

void Debugd(const wchar_t *Str, double Value) {
    // usage: 
    // Debugd(L"Elapsedms: %f\n", Timer.ElapsedMilliSeconds);
    wchar_t Output[100];
    swprintf_s(Output, 100, Str, Value);
    OutputDebugString(Output);
}

Build

cl main.cpp /link user32.lib gdi32.lib

build.bat

@echo off

set COMMON_COMPILER_FLAGS=/W4 /wd4100 /Oi /nologo /Fea.exe

set DEBUG_FLAGS=/Zi /MTd
set RELEASE_FLAGS=/O2

set COMPILER_FLAGS=%COMMON_COMPILER_FLAGS% %DEBUG_FLAGS%
REM set COMPILER_FLAGS=%COMMON_COMPILER_FLAGS% %RELEASE_FLAGS%

set LINKER_FLAGS=/INCREMENTAL:NO /OPT:REF
set LIBS=user32.lib gdi32.lib

cl %COMPILER_FLAGS% main.cpp /link %LINKER_FLAGS% %LIBS%

Leave a comment

You can use Markdown to format your comment.