Initial Commit

This commit is contained in:
Cole
2024-01-11 12:55:43 -06:00
commit 982624bb5b
24 changed files with 746 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
./src/tetris-clone

18
.vscode/c_cpp_properties.json vendored Normal file
View File

@@ -0,0 +1,18 @@
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"~/src/raylib/src/**",
"/usr/include/**"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c99",
"cppStandard": "gnu++17",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"files.associations": {
"*.m": "c",
"tetromino.h": "c"
}
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

51
README.md Normal file
View File

@@ -0,0 +1,51 @@
# Tetris Clone
Creating a Tetris clone in C to practice C programming and learn more about Raylib (awesome library btw!).
## Description
This game is a clone of the popular game Tetris. This clone features what you would expect from a simplistic version of Tetris, namely: falling blocks, which upon completion of a row disappear, score keeping and levels with incremental difficulty.
I hope you enjoy!
## Getting Started
### Dependencies
* Describe any prerequisites, libraries, OS version, etc., needed before installing program.
### Installing
* How/where to download your program
* Any modifications needed to be made to files/folders
### Executing program
* How to run the program
* Step-by-step bullets
```
code blocks for commands
```
## Help
## Authors
Contributors names and contact info
John Landers [jcolelanders@gmail.com](mailto:jcolelanders@gmail.com)
## Version History
* 0.1
* Initial Release
## License
This project is licensed under the MIT License - see the LICENSE file for details
## Acknowledgments
Inspiration, code snippets, etc.
* [Tetris](https://en.wikipedia.org/wiki/Tetris)
* [raylib](https://github.com/raysan5/raylib)

6
TODO Normal file
View File

@@ -0,0 +1,6 @@
- Rotate (Partially Implemented/Buggy. Should try to understand Rotation Matrix better.)
- Empty rows that are full should "break"
- Shift rows down after row "breaks"
- Track and Display Score. During and After game
- display Upcoming block
- Introduce levels/speed up with every level

13
src/Makefile Normal file
View File

@@ -0,0 +1,13 @@
CC=clang
CFLAGS=-Wall -Wextra -include raylib.h
LIBS=-lraylib -lGL -lm -lpthread -ldl -lrt -lX11
tetris-clone: main.c manager.c renderer.c app.c
$(CC) -o $@ $^ $(CFLAGS) $(LIBS)
.PHONY: clean
clean:
rm -f ./tetris-clone
rm -f ./*.o
rm -f ./*.gch

71
src/app.c Normal file
View File

@@ -0,0 +1,71 @@
#include <stdio.h>
#include "raylib.h"
#include "manager.h"
#include "renderer.h"
#include "tetromino.h"
#define TARGET_FPS 60
void EmptyGameBoard(void) {
EmptyAllBlocks();
int numberOfUpdatedBlocks = 0;
struct TetrominoBlock** updatedBlocks = GetUpdatedBlocks(&numberOfUpdatedBlocks);
}
void InitRaylib(void) {
InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "Tetris Clone");
SetTargetFPS(TARGET_FPS);
}
void Init(void) {
InitRaylib();
InitManager();
InitRenderer();
EmptyGameBoard();
}
void ProcessInput(void) {
if (IsKeyPressed(KEY_SPACE)) {
RequestRotate();
}
if (IsKeyDown(KEY_A)) {
RequestMoveLeft();
} else if (IsKeyDown(KEY_D)) {
RequestMoveRight();
}
}
void Loop(void) {
int tickRate = 0; // Guaranteed a better way to do this XD
while(!WindowShouldClose()) {
while(!CantSpawnBlock()) {
ProcessInput();
if (tickRate == 0) {
Update();
tickRate = 5;
}
int numberOfUpdatedBlocks = 0;
struct TetrominoBlock** updatedBlocks = GetUpdatedBlocks(&numberOfUpdatedBlocks);
RenderBlocks(updatedBlocks, numberOfUpdatedBlocks);
tickRate--;
}
// TODO present Game over message and score
RenderGameOver();
}
}
void Cleanup() {
CleanupManager();
CloseWindow();
}
void start(void) {
Init();
Loop();
Cleanup();
}

6
src/app.h Normal file
View File

@@ -0,0 +1,6 @@
#ifndef TETRIS_CLONE_APP_H_
#define TETRIS_CLONE_APP_H_
void start(void);
#endif // TETRIS_CLONE_APP_H_

7
src/main.c Normal file
View File

@@ -0,0 +1,7 @@
#include "app.h"
int main(void) {
start();
return 0;
}

418
src/manager.c Normal file
View File

@@ -0,0 +1,418 @@
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define __USE_MISC
#include <math.h>
#include "manager.h"
#define FIELD_WIDTH 10
#define FIELD_HEIGHT 20
#define BLOCKS_WITHIN_A_TETROMINO 4
// should probably call these offsets
struct Offset {
int xOffset;
int yOffset;
};
struct Point {
int x;
int y;
};
struct Tetromino {
enum TetrominoType type;
struct TetrominoBlock* blocks[BLOCKS_WITHIN_A_TETROMINO];
};
// To find block (x,y) index = (x % FIELD_WIDTH) + (y * FIELD_WIDTH)
// Height doesn't matter but height is just the factor of how tall the board is.
// And in the end each row is just the next representation of width * units, height just determines row count.
// Doesn't factor into determining X,Y
struct TetrominoBlock* _blocks[FIELD_HEIGHT * FIELD_WIDTH];
struct TetrominoBlock* _updatedBlocks[FIELD_HEIGHT * FIELD_WIDTH] = { NULL };
int _updatedBlockLength = 0;
bool shouldSpawnTetromino;
struct Tetromino fallingTetromino;
const char kSpawnX = FIELD_WIDTH / 2 - 1;
const struct Offset kShiftDownOffset = { .xOffset = 0, .yOffset = 1 };
const struct Offset kShiftLeftOffset = { .xOffset = -1, .yOffset = 0 };
const struct Offset kShiftRightOffset = { .xOffset = 1, .yOffset = 0 };
bool moveLeft = false;
bool moveRight = false;
bool rotate = false;
bool cannotSpawn = false;
void CleanupManager(void) {
for(int i = 0; i < sizeof(_blocks) / sizeof(struct TetrominoBlock*); i++) {
free(_blocks[i]);
}
}
struct TetrominoBlock** GetUpdatedBlocks(int* numberOfUpdatedBlocks) {
*numberOfUpdatedBlocks = _updatedBlockLength;
return &_updatedBlocks[0];
}
void RequestRotate(void) {
rotate = true;
}
void RequestMoveLeft(void) {
moveLeft = true;
moveRight = false;
}
void RequestMoveRight(void) {
moveRight = true;
moveLeft = false;
}
struct TetrominoBlock* GetBlockAtPoint(struct Point point) {
return _blocks[(point.x % FIELD_WIDTH) + (point.y * FIELD_WIDTH)];
}
void RegisterUpdatedBlock(struct TetrominoBlock* pTetrominoBlock) {
if (_updatedBlockLength < FIELD_HEIGHT * FIELD_WIDTH) {
_updatedBlocks[_updatedBlockLength] = pTetrominoBlock;
_updatedBlockLength++;
}
}
void UpdateBlockType(struct TetrominoBlock* pTetrominoBlock, enum TetrominoType type) {
pTetrominoBlock->type = type;
RegisterUpdatedBlock(pTetrominoBlock);
}
void ShiftFallingTetrominoByOffset(struct Offset offset) {
for(int i = 0; i < BLOCKS_WITHIN_A_TETROMINO; i++) {
UpdateBlockType(fallingTetromino.blocks[i], EMPTY);
RegisterUpdatedBlock(fallingTetromino.blocks[i]);
}
for(int i = 0; i < BLOCKS_WITHIN_A_TETROMINO; i++) {
struct Point updatedPoint = { .x = fallingTetromino.blocks[i]->x + offset.xOffset, .y = fallingTetromino.blocks[i]->y + offset.yOffset};
fallingTetromino.blocks[i] = GetBlockAtPoint(updatedPoint);
UpdateBlockType(fallingTetromino.blocks[i], fallingTetromino.type);
RegisterUpdatedBlock(fallingTetromino.blocks[i]);
}
}
// I HATE THIS NAME... but it's verbose for now TODO - RENAME
bool DoesPointIntersectNonFallingBlock(struct Point point) {
bool result = false;
struct TetrominoBlock* blockAtPoint = GetBlockAtPoint(point);
if (blockAtPoint->type != EMPTY) {
result = true;
for(int i = 0; i < BLOCKS_WITHIN_A_TETROMINO; i++) {
if (fallingTetromino.blocks[i] == blockAtPoint) {
result = false;
break;
}
}
}
return result;
}
bool CanBlockFall(struct TetrominoBlock block) {
struct Point newBlockPoint = { .x = block.x, .y = block.y + 1};
if (newBlockPoint.y >= FIELD_HEIGHT) {
return false;
} else if (DoesPointIntersectNonFallingBlock(newBlockPoint)) {
return false;
} else {
return true;
}
}
bool FallingTetrominoCanFall(void) {
bool result = true;
for(int i = 0; i < BLOCKS_WITHIN_A_TETROMINO; i++) {
if (!CanBlockFall(*(fallingTetromino.blocks[i]))) {
result = false;
break;
}
}
return result;
}
void UpdateFallingTetromino(void) {
if(FallingTetrominoCanFall()) {
ShiftFallingTetrominoByOffset(kShiftDownOffset);
} else {
shouldSpawnTetromino = true;
}
}
bool CanBlockMoveLeft(struct TetrominoBlock block) {
struct Point newBlockPoint = { .x = block.x - 1, .y = block.y };
if (newBlockPoint.x < 0) {
return false;
} else if (DoesPointIntersectNonFallingBlock(newBlockPoint)) {
return false;
} else {
return true;
}
}
bool FallingTetrominoCanMoveLeft(void) {
bool result = true;
for(int i = 0; i < BLOCKS_WITHIN_A_TETROMINO; i++) {
if (!CanBlockMoveLeft(*(fallingTetromino.blocks[i]))) {
result = false;
break;
}
}
return result;
}
bool CanBlockMoveRight(struct TetrominoBlock block) {
struct Point newBlockPoint = { .x = block.x + 1, .y = block.y };
if (newBlockPoint.x >= FIELD_WIDTH) {
return false;
} else if (DoesPointIntersectNonFallingBlock(newBlockPoint)) {
return false;
} else {
return true;
}
}
bool FallingTetrominoCanMoveRight(void) {
bool result = true;
for(int i = 0; i < BLOCKS_WITHIN_A_TETROMINO; i++) {
if (!CanBlockMoveRight(*(fallingTetromino.blocks[i]))) {
result = false;
break;
}
}
return result;
}
struct Point GetRotatedPointForBlock(struct Point origin, struct TetrominoBlock block) {
// Only doing counter clockwise for now
struct Point newPoint;
newPoint.x = (block.x - origin.x) * cos(M_PI_2) - (block.y - origin.y) * sin(M_PI_2);
newPoint.y = (block.x - origin.x) * sin(M_PI_2) + (block.y - origin.y) * cos(M_PI_2);
return newPoint;
}
void RotateFallingTetromino(void) {
// Will be a bit buggy because i'm not checking for intersections
struct Point origin = { .x = fallingTetromino.blocks[0]->x, .y = fallingTetromino.blocks[0]->y };
for (int i = 0; i < BLOCKS_WITHIN_A_TETROMINO; i++) {
// Empty the old
UpdateBlockType(fallingTetromino.blocks[i], EMPTY);
RegisterUpdatedBlock(fallingTetromino.blocks[i]);
// Update the new
struct Point rotatedPointAdjustedForOrigin = GetRotatedPointForBlock(origin, *fallingTetromino.blocks[i]);
struct Point newRotatedPoint = { .x = rotatedPointAdjustedForOrigin.x + origin.x, .y = rotatedPointAdjustedForOrigin.y + origin.y };
fallingTetromino.blocks[i] = GetBlockAtPoint(newRotatedPoint);
UpdateBlockType(fallingTetromino.blocks[i], fallingTetromino.type);
RegisterUpdatedBlock(fallingTetromino.blocks[i]);
}
}
void TryRotateFallingTetromino(void) {
// TODO IMPLEMENT
if (fallingTetromino.type == O_TYPE) {
return;
} else {
RotateFallingTetromino();
}
}
void SpawnBlocksAtPoints(enum TetrominoType type, struct Offset* offsets) {
fallingTetromino.type = type;
int index = 0;
while(index < BLOCKS_WITHIN_A_TETROMINO) {
struct Point spawnPoint;
spawnPoint.x = kSpawnX + offsets->xOffset;
spawnPoint.y = offsets->yOffset;
struct TetrominoBlock* spawnBlock = GetBlockAtPoint(spawnPoint);
if (spawnBlock->type != EMPTY) {
cannotSpawn = true;
return;
}
UpdateBlockType(spawnBlock, type);
// I'm not sure if this makes more sense to be here or return a list of spawnedBlocks to be handled in Parent function
fallingTetromino.blocks[index] = spawnBlock;
index++;
offsets++;
}
}
enum TetrominoType GenerateRandomTetrominoType() {
srand(time(NULL));
return (enum TetrominoType) rand() % 6;
}
void SpawnTetromino() {
enum TetrominoType type = GenerateRandomTetrominoType();
struct Offset offsets[BLOCKS_WITHIN_A_TETROMINO];
if (type == O_TYPE) {
offsets[0].xOffset = 0;
offsets[0].yOffset = 0;
offsets[1].xOffset = 1;
offsets[1].yOffset = 0;
offsets[2].xOffset = 0;
offsets[2].yOffset = 1;
offsets[3].xOffset = 1;
offsets[3].yOffset = 1;
} else if (type == J_TYPE) {
offsets[0].xOffset = 0;
offsets[0].yOffset = 0;
offsets[1].xOffset = 0;
offsets[1].yOffset = 1;
offsets[2].xOffset = 1;
offsets[2].yOffset = 1;
offsets[3].xOffset = 2;
offsets[3].yOffset = 1;
} else if (type == L_TYPE) {
offsets[0].xOffset = 0;
offsets[0].yOffset = 1;
offsets[1].xOffset = 1;
offsets[1].yOffset = 1;
offsets[2].xOffset = 2;
offsets[2].yOffset = 1;
offsets[3].xOffset = 2;
offsets[3].yOffset = 0;
} else if (type == I_TYPE) {
offsets[0].xOffset = 0;
offsets[0].yOffset = 0;
offsets[1].xOffset = 0;
offsets[1].yOffset = 1;
offsets[2].xOffset = 0;
offsets[2].yOffset = 2;
offsets[3].xOffset = 0;
offsets[3].yOffset = 3;
} else if (type == S_TYPE) {
offsets[0].xOffset = 0;
offsets[0].yOffset = 1;
offsets[1].xOffset = 1;
offsets[1].yOffset = 1;
offsets[2].xOffset = 1;
offsets[2].yOffset = 0;
offsets[3].xOffset = 2;
offsets[3].yOffset = 0;
} else if (type == Z_TYPE) {
offsets[0].xOffset = 0;
offsets[0].yOffset = 0;
offsets[1].xOffset = 1;
offsets[1].yOffset = 0;
offsets[2].xOffset = 1;
offsets[2].yOffset = 1;
offsets[3].xOffset = 2;
offsets[3].yOffset = 1;
} else if (type == T_TYPE) {
offsets[0].xOffset = 1;
offsets[0].yOffset = 0;
offsets[1].xOffset = 0;
offsets[1].yOffset = 1;
offsets[2].xOffset = 1;
offsets[2].yOffset = 1;
offsets[3].xOffset = 2;
offsets[3].yOffset = 1;
}
SpawnBlocksAtPoints(type, offsets);
}
void DeregisterUpdatedBlocks() {
_updatedBlockLength = 0;
}
void Update(void) {
DeregisterUpdatedBlocks();
if (shouldSpawnTetromino) {
moveLeft = false;
moveRight = false;
SpawnTetromino();
shouldSpawnTetromino = false;
} else {
// TODO move left and right refactor naming
if(rotate) {
TryRotateFallingTetromino();
rotate = false;
}
if (moveRight && FallingTetrominoCanMoveRight()) {
ShiftFallingTetrominoByOffset(kShiftRightOffset);
moveRight = false;
}
if (moveLeft && FallingTetrominoCanMoveLeft()) {
ShiftFallingTetrominoByOffset(kShiftLeftOffset);
moveLeft = false;
}
UpdateFallingTetromino();
}
}
bool CantSpawnBlock(void) {
return cannotSpawn;
}
void EmptyAllBlocks(void) {
for (int i = 0; i < FIELD_HEIGHT * FIELD_WIDTH; i++) {
UpdateBlockType(_blocks[i], EMPTY);
}
}
void AllocBlocks(void) {
for (int i = 0; i < FIELD_HEIGHT * FIELD_WIDTH; i++) {
struct TetrominoBlock* block = calloc(1, sizeof(struct TetrominoBlock));
block->x = i % FIELD_WIDTH;
block->y = i / FIELD_WIDTH;
_blocks[i] = block;
}
}
void InitManager(void) {
AllocBlocks();
shouldSpawnTetromino = true;
}

18
src/manager.h Normal file
View File

@@ -0,0 +1,18 @@
#ifndef TETRIS_CLONE_MANAGER_H_
#define TETRIS_CLONE_MANAGER_H_
#include <stdbool.h>
#include "tetromino.h"
void InitManager(void);
void EmptyAllBlocks(void);
bool CantSpawnBlock(void);
void Update(void);
void RequestRotate(void);
void RequestMoveLeft(void);
void RequestMoveRight(void);
struct TetrominoBlock** GetUpdatedBlocks(int* lengthOfBlocksUpdated);
void CleanupManager(void);
#endif

76
src/renderer.c Normal file
View File

@@ -0,0 +1,76 @@
#include <stdio.h>
#include "raylib.h"
#include "renderer.h"
#define PNG_RES(name) "resources/" #name ".png"
#define BLOCK_SIZE 32
Texture2D yellowTexture;
Texture2D blueTexture;
Texture2D lightBlueTexture;
Texture2D redTexture;
Texture2D purpleTexture;
Texture2D greenTexture;
Texture2D orangeTexture;
Texture2D emptyTexture;
Texture2D gameOverTexture;
void InitRenderer(void) {
yellowTexture = LoadTexture(PNG_RES(yellow));
blueTexture = LoadTexture(PNG_RES(blue));
lightBlueTexture = LoadTexture(PNG_RES(light-blue));
redTexture = LoadTexture(PNG_RES(red));
purpleTexture = LoadTexture(PNG_RES(purple));
greenTexture = LoadTexture(PNG_RES(green));
orangeTexture = LoadTexture(PNG_RES(orange));
emptyTexture = LoadTexture(PNG_RES(black));
gameOverTexture = LoadTexture(PNG_RES(gameOver));
}
Texture2D* GetTextureFromTetrominoType(enum TetrominoType type) {
switch (type) {
case O_TYPE:
return &yellowTexture;
case I_TYPE:
return &lightBlueTexture;
case T_TYPE:
return &purpleTexture;
case J_TYPE:
return &blueTexture;
case L_TYPE:
return &orangeTexture;
case S_TYPE:
return &greenTexture;
case Z_TYPE:
return &redTexture;
default:
return &emptyTexture;
}
}
void RenderTetrominoBlock(struct TetrominoBlock blockToRender) {
Texture2D* textureToRender = GetTextureFromTetrominoType(blockToRender.type);
DrawTexture(*textureToRender, blockToRender.x * BLOCK_SIZE, blockToRender.y * BLOCK_SIZE, WHITE);
}
void RenderBlocks(struct TetrominoBlock** updatedBlocks, int length) {
BeginDrawing();
while(length--) {
RenderTetrominoBlock(*(*updatedBlocks));
updatedBlocks++;
}
EndDrawing();
}
void RenderGameOver(void) {
BeginDrawing();
ClearBackground(BLACK);
DrawText("Game Over", SCREEN_WIDTH / 2 - MeasureText("Game Over", 24) / 2, 200, 24, RED);
DrawText("Score: N/A", SCREEN_WIDTH / 2 - MeasureText("Score: N/A", 18) / 2, 232, 18, RED);
EndDrawing();
}

13
src/renderer.h Normal file
View File

@@ -0,0 +1,13 @@
#ifndef TETRIS_CLONE_RENDERER_H_
#define TETRIS_CLONE_RENDERER_H_
#include "tetromino.h"
#define SCREEN_HEIGHT 640
#define SCREEN_WIDTH 320
void InitRenderer(void);
void RenderBlocks(struct TetrominoBlock** updatedBlocks, int length);
void RenderGameOver(void);
#endif

BIN
src/resources/black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

BIN
src/resources/blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

BIN
src/resources/green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

BIN
src/resources/orange.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

BIN
src/resources/purple.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

BIN
src/resources/red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

BIN
src/resources/yellow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

BIN
src/tetris-clone Executable file

Binary file not shown.

21
src/tetromino.h Normal file
View File

@@ -0,0 +1,21 @@
#ifndef TETRIS_CLONE_TETROMINO_H_
#define TETRIS_CLONE_TETROMINO_H_
enum TetrominoType {
I_TYPE,
J_TYPE,
L_TYPE,
O_TYPE,
S_TYPE,
T_TYPE,
Z_TYPE,
EMPTY
};
struct TetrominoBlock {
unsigned char x;
unsigned char y;
enum TetrominoType type;
};
#endif