Commit 0c4e5c78 authored by LAFORÊT Nicolas's avatar LAFORÊT Nicolas 🐇
Browse files

Merge branch 'develop' into 'master'

Tetris Engine & SDL Graphics

See merge request !3
parents 547f28ed 1444a41f
Pipeline #51564 passed with stage
in 32 seconds
......@@ -71,3 +71,4 @@ CMakeUserPresets.json
# End of https://www.toptal.com/developers/gitignore/api/c++,vscode,cmake
build/
.vscode/
\ No newline at end of file
......@@ -5,33 +5,34 @@ image: gcc
build:
stage: build
before_script:
- apt update && apt -y install cmake make libsdl2-dev gcovr
- apt update && apt -y install cmake make libsdl2-dev libsdl2-ttf-dev
script:
- mkdir build && cd build
- cmake ..
- make
artifacts:
expire_in: 1 day
paths:
- build/bin/tetris
only:
- master
- merge_requests
test:
stage: test
before_script:
- apt update && apt -y install cmake make libsdl2-dev gcovr
script:
- cd build
- cmake -DCODE_COVERAGE=ON ..
- make
- ./bin/tetris_test --gtest_output="xml:report.xml"
- ./bin/tetris
- gcovr -r ..
artifacts:
when: always
reports:
junit: build/report.xml
only:
- master
- merge_requests
# test:
# stage: test
# before_script:
# - apt update && apt -y install cmake make libsdl2-dev gcovr
# script:
# - cd build
# - cmake -DCODE_COVERAGE=ON ..
# - make
# - ./bin/tetris_test --gtest_output="xml:report.xml"
# - ./bin/tetris
# - gcovr -r ..
# artifacts:
# when: always
# expire_in: 1 day
# reports:
# junit: build/report.xml
# only:
# - master
# - merge_requests
......@@ -32,7 +32,22 @@ find_package(SDL2 REQUIRED)
set(TARGET_NAME tetris)
# Set sources files
set(SOURCES src/main.cpp src/sum.h src/sum.cpp)
set(
SOURCES
src/main.cpp
src/application.cpp
src/tetris/tetromino.cpp
src/tetris/grid.cpp
src/tetris/randomGenerator.cpp
src/tetris/input/inputSystem.cpp
src/tetris/game.cpp
src/ui/textureRenderer.cpp
src/ui/gameRenderer.cpp
src/core/gameContext.cpp
)
# Set exexutable outout path to bin/ folder
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)
......@@ -42,43 +57,43 @@ add_executable(${TARGET_NAME} ${SOURCES})
# SDL2 platform dependent config
if (${CMAKE_SYSTEM_NAME} MATCHES Linux)
target_include_directories(${TARGET_NAME} PUBLIC /usr/include/SDL2)
target_link_libraries(${TARGET_NAME} PUBLIC SDL2 SDL2main)
target_link_libraries(${TARGET_NAME} PUBLIC SDL2 SDL2main SDL2_ttf)
else()
target_link_libraries(${TARGET_NAME} PUBLIC SDL2::SDL2 SDL2::SDL2main)
target_link_libraries(${TARGET_NAME} PUBLIC SDL2::SDL2 SDL2::SDL2main SDL2::SDL2_ttf)
endif()
target_compile_definitions(${TARGET_NAME} PUBLIC _USE_MATH_DEFINES)
################################################################################
## Add tests
enable_testing()
add_subdirectory(test)
# enable_testing()
# add_subdirectory(test)
################################################################################
## Add Code Coverage
set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
option(CODE_COVERAGE "Enable coverage reporting" OFF)
if(CODE_COVERAGE)
include(CodeCoverage)
append_coverage_compiler_flags()
# we need to turn off optimization for non-skewed coverage reports
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0 --coverage -fprofile-arcs -ftest-coverage")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -O0 --coverage -fprofile-arcs -ftest-coverage")
# optional excludes - None needed here
# set(COVERAGE_EXCLUDES test/*)
# Works
setup_target_for_coverage_gcovr_xml(
NAME TetrisCoverageXml
EXECUTABLE tetris_test
DEPENDENCIES tetris_test tetris
)
# Works
setup_target_for_coverage_gcovr_html(
NAME TetrisCoverageHtml
EXECUTABLE tetris_test
DEPENDENCIES tetris_test tetris
)
endif ()
# set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
# option(CODE_COVERAGE "Enable coverage reporting" OFF)
# if(CODE_COVERAGE)
# include(CodeCoverage)
# append_coverage_compiler_flags()
# # we need to turn off optimization for non-skewed coverage reports
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0 --coverage -fprofile-arcs -ftest-coverage")
# set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -O0 --coverage -fprofile-arcs -ftest-coverage")
# # optional excludes - None needed here
# # set(COVERAGE_EXCLUDES test/*)
# # Works
# setup_target_for_coverage_gcovr_xml(
# NAME TetrisCoverageXml
# EXECUTABLE tetris_test
# DEPENDENCIES tetris_test tetris
# )
# # Works
# setup_target_for_coverage_gcovr_html(
# NAME TetrisCoverageHtml
# EXECUTABLE tetris_test
# DEPENDENCIES tetris_test tetris
# )
# endif ()
#include "application.h"
#include "core/contextStack.h"
#include "core/gameContext.h"
Application::Application(std::string title, double framerate, int width,
int height, Theme *theme)
: m_framerate(framerate), b_quit(false), p_theme(theme) {
p_window =
SDL_CreateWindow(title.c_str(), SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_SHOWN);
p_renderer = SDL_CreateRenderer(p_window, -1, SDL_RENDERER_ACCELERATED);
}
Application::~Application() {
ContextStack::Instance()->Destroy();
SDL_DestroyRenderer(p_renderer);
SDL_DestroyWindow(p_window);
}
void Application::Run() {
Uint32 currentTime = SDL_GetTicks();
Uint32 previousTime = currentTime;
// Run until b_quit is `true` or ContextStack is empty
while (!b_quit && !ContextStack::Instance()->Empty()) {
SDL_Event e;
while (SDL_PollEvent(&e) != 0) {
if (e.type == SDL_QUIT) {
b_quit = true;
break;
}
}
currentTime = SDL_GetTicks();
m_dTime = currentTime - previousTime;
if (m_dTime > 1000. / m_framerate) {
previousTime = currentTime;
if (ContextStack::Instance()->GetActiveContext()->Update(m_dTime) == -1) {
ContextStack::Instance()->Pop();
}
// Display
SDL_RenderPresent(p_renderer);
}
}
}
void Application::LaunchGame(int nRow, int nCol, int cellSize) {
InputSystem *input = new InputSystem({
{InputSystem::Action::MOVE_LEFT, SDL_Scancode::SDL_SCANCODE_LEFT},
{InputSystem::Action::MOVE_RIGHT, SDL_Scancode::SDL_SCANCODE_RIGHT},
{InputSystem::Action::MOVE_UP, SDL_Scancode::SDL_SCANCODE_UP},
{InputSystem::Action::MOVE_DOWN, SDL_Scancode::SDL_SCANCODE_DOWN},
{InputSystem::Action::HOLD_PIECE, SDL_Scancode::SDL_SCANCODE_H},
{InputSystem::Action::TOGGLE_PAUSE, SDL_Scancode::SDL_SCANCODE_P},
{InputSystem::Action::QUIT, SDL_Scancode::SDL_SCANCODE_ESCAPE},
});
Game *game = new Game(nRow, nCol, input);
GameRenderer *gameRenderer =
new GameRenderer(game, p_renderer, cellSize, 200, p_theme);
GameContext *gContext = new GameContext(gameRenderer, game);
ContextStack::Instance()->Push(gContext);
}
#ifndef _APPLICATION_H_
#define _APPLICATION_H_
#include <SDL2/SDL.h>
#include <string>
#include "ui/theme.h"
class Application {
private:
SDL_Window *p_window;
SDL_Renderer *p_renderer;
Theme *p_theme;
double m_framerate = 60.;
double m_dTime = 0.;
bool b_quit;
public:
Application(std::string title, double framerate, int width, int height,
Theme *theme);
~Application();
void Run();
void LaunchGame(int nRow, int nCol, int cellSize);
};
#endif
\ No newline at end of file
#ifndef _CONTEXT_H_
#define _CONTEXT_H_
#include "../ui/textureRenderer.h"
class Context {
public:
TextureRenderer *p_renderer;
public:
Context(TextureRenderer *renderer) : p_renderer(renderer) {}
virtual ~Context() {
delete p_renderer;
}
virtual int Update(double deltaTime) {
p_renderer->Render(0, 0, 0., SDL_FLIP_NONE);
return 0;
}
};
#endif
\ No newline at end of file
#ifndef _CONTEXTSTACK_H_
#define _CONTEXTSTACK_H_
#include <stack>
#include "context.h"
class ContextStack {
/* SINGLETON */
public:
ContextStack(const ContextStack &) = delete;
ContextStack &operator=(const ContextStack &) = delete;
static ContextStack *Instance() {
if (!m_instance) {
m_instance = new ContextStack();
}
return m_instance;
}
private:
ContextStack() {}
static ContextStack *m_instance;
std::stack<Context *> m_contextStack;
public:
Context *GetActiveContext() {
return m_contextStack.top();
}
void Push(Context *context) {
m_contextStack.push(context);
}
void Pop() {
Context *context = m_contextStack.top();
m_contextStack.pop();
delete context;
}
bool Empty() {
return m_contextStack.empty();
}
void Destroy() {
while (!m_contextStack.empty())
Pop();
}
};
ContextStack *ContextStack::m_instance = nullptr;
#endif
\ No newline at end of file
#include "gameContext.h"
GameContext::GameContext(GameRenderer *gameRenderer, Game *game)
: Context(gameRenderer), p_game(game) {}
GameContext::~GameContext() {
delete p_game;
}
int GameContext::Update(double deltaTime) {
if (p_game->IsGameOver())
return -1;
p_game->Update();
p_renderer->Render(0, 0, 0., SDL_FLIP_NONE);
return 0;
}
#ifndef _GAMECONTEXT_H_
#define _GAMECONTEXT_H_
#include "../tetris/game.h"
#include "../ui/gameRenderer.h"
#include "context.h"
class GameContext : public Context {
private:
Theme *p_theme;
Game *p_game;
public:
GameContext(GameRenderer *gameRenderer, Game *game);
~GameContext() override;
int Update(double deltaTime) override;
};
#endif
\ No newline at end of file
#include <iostream>
#include "application.h"
#include "sum.h"
#include <SDL2/SDL_ttf.h>
#include <iostream>
int main(int argc, char const *argv[]) {
sum s;
int res = s.doSum(1, 2);
std::cout << res << std::endl;
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
return 1;
}
if (TTF_Init() < 0) {
std::cout << "Couldn't initialize TTF lib: " << TTF_GetError() << std::endl;
return 1;
}
Theme theme = {
.bg = {.r = 0x00, .g = 0x00, .b = 0x00, .a = 0xFF},
.gLines = {.r = 0x7F, .g = 0x7F, .b = 0x7F, .a = 0xFF},
.gBorder = {.r = 0x8F, .g = 0x8F, .b = 0x8F, .a = 0xFF},
.cells = {
{Tetromino::Cell::I, {.r = 0x00, .g = 0xFF, .b = 0xFF, .a = 0xFF}},
{Tetromino::Cell::O, {.r = 0xFF, .g = 0xFF, .b = 0x00, .a = 0xFF}},
{Tetromino::Cell::T, {.r = 0x80, .g = 0x00, .b = 0x80, .a = 0xFF}},
{Tetromino::Cell::L, {.r = 0xFF, .g = 0x7F, .b = 0x00, .a = 0xFF}},
{Tetromino::Cell::J, {.r = 0x00, .g = 0x00, .b = 0xFF, .a = 0xFF}},
{Tetromino::Cell::Z, {.r = 0xFF, .g = 0x00, .b = 0x00, .a = 0xFF}},
{Tetromino::Cell::S, {.r = 0x00, .g = 0xFF, .b = 0x00, .a = 0xFF}},
}};
/* APPICATION TESTS */
Application *app = new Application("Tetris", 60., 600, 800, &theme);
app->LaunchGame(20, 10, 40);
app->Run();
delete app;
SDL_Quit();
return 0;
}
#include "sum.h"
int sum::doSum(int a, int b) {
return a + b;
}
\ No newline at end of file
#ifndef _SUM_H_
#define _SUM_H_
class sum {
public:
int doSum(int a, int b);
};
#endif
\ No newline at end of file
#include "game.h"
Game::Game(uint height, uint width, InputSystem *input)
: m_grid(height, width), p_currentTetromino(nullptr),
p_nextTetromino(nullptr), p_heldTetromino(nullptr), m_generator(),
m_input(input) {
GenerateNewTetromino();
}
Game::~Game() {
delete p_currentTetromino;
delete p_heldTetromino;
delete p_nextTetromino;
delete m_input;
}
bool Game::IsGameOver() {
return b_gameOver;
}
bool Game::IsPaused() {
return b_pause;
}
void Game::Update() {
if (IsGameOver() || IsPaused())
return;
IncrementCounters();
ApplyGravity();
HandleInput();
uint removedLines = m_grid.RemoveLines();
// m_scoring->ScoreLines(removedLines);
}
void Game::ApplyGravity() {
if (ShouldDrop()) {
ResetDropCounter();
if (m_grid.TryToMoveOrPlace(p_currentTetromino, {1, 0})) {
GenerateNewTetromino();
}
}
}
void Game::HandleInput() {
if (!CanMove())
return;
m_input->GetInput();
ResetMoveCounter();
if (m_input->IsPressed(InputSystem::Action::QUIT)) {
b_gameOver = true;
return;
}
if (m_input->IsPressed(InputSystem::Action::TOGGLE_PAUSE)) {
b_pause = !b_pause;
}
if (m_input->IsPressed(InputSystem::Action::HOLD_PIECE)) {
HoldCurrentTetromino();
// If held piece doesn't fit then switch back held piece
if (!m_grid.CanFit(p_currentTetromino)) {
HoldCurrentTetromino();
}
}
if (m_input->IsPressed(InputSystem::Action::MOVE_LEFT)) {
if (m_grid.CanFit(p_currentTetromino, {0, -1})) {
p_currentTetromino->Move({0, -1});
}
} else if (m_input->IsPressed(InputSystem::Action::MOVE_RIGHT)) {
if (m_grid.CanFit(p_currentTetromino, {0, 1})) {
p_currentTetromino->Move({0, 1});
}
}
if (m_input->IsPressed(InputSystem::Action::MOVE_DOWN)) {
ResetDropCounter();
if (m_grid.TryToMoveOrPlace(p_currentTetromino, {1, 0})) {
GenerateNewTetromino();
}
}
if (m_input->IsPressed(InputSystem::Action::MOVE_UP)) {
m_grid.TryToRotate(p_currentTetromino);
}
m_grid.Print();
}
void Game::GenerateNewTetromino() {
delete p_currentTetromino;
p_currentTetromino = nullptr;
p_currentTetromino = p_nextTetromino;
Tetromino::Shape shape = (Tetromino::Shape)m_generator.Generate(1, 7);
// Generate Tetromino at the middle top of the grid
Vec2I position = {0, (int)(m_grid.GetWidth() / 2) - 2};
p_nextTetromino = new Tetromino(shape, position);
if (p_currentTetromino == nullptr) {
GenerateNewTetromino();
}
if (!m_grid.CanFit(p_currentTetromino)) {
b_gameOver = true;
}
}
void Game::HoldCurrentTetromino() {
// Switch held and current Tetromino
Tetromino *tmp = p_heldTetromino;
p_heldTetromino = p_currentTetromino;
p_currentTetromino = tmp;
if (p_currentTetromino == nullptr)
GenerateNewTetromino();
p_currentTetromino->SetPosition(p_heldTetromino->GetPosition());
}
#ifndef _GAME_H_
#define _GAME_H_
#include "grid.h"
#include "input/inputSystem.h"
#include "randomGenerator.h"
#include "tetromino.h"
class ScoringSystem;
class Game {
private:
Grid m_grid;
Tetromino *p_currentTetromino;
Tetromino *p_heldTetromino;
Tetromino *p_nextTetromino;
RandomGenerator m_generator;
InputSystem *m_input;
ScoringSystem *m_scoring;
bool b_gameOver = false;
bool b_pause = false;
uint m_moveCounter = 0;
uint m_dropCounter = 0;
const uint MOVE_SPEED = 6;
public:
Game(uint height, uint width, InputSystem *input);
~Game();
void Update();
bool IsGameOver();
bool IsPaused();
void SetGameOver(bool value) {
b_gameOver = value;
}
void SetPause(bool value) {
b_pause = value;
}
void IncrementCounters() {
m_moveCounter++;
m_dropCounter++;
}
Tetromino *GetCurrentTetromino() {
return p_currentTetromino;
}
Tetromino *GetHeldTetromino() {
return p_heldTetromino;
}
Tetromino *GetNextTetromino() {
return p_nextTetromino;
}
Grid &GetGrid() {
return m_grid;
}
private:
void Init();
void HandleInput();
void GenerateNewTetromino();
void HoldCurrentTetromino();
void ApplyGravity();
bool ShouldDrop() {
return m_dropCounter > 24 /*m_scoring->GetDropRate()*/;
}
bool CanMove() {