aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorfschildt <florian.schildt@protonmail.com>2025-07-21 16:07:28 +0200
committerfschildt <florian.schildt@protonmail.com>2025-07-21 16:07:28 +0200
commitb46a0d9369fbaa1938f0968ab216bc2d564a9c37 (patch)
treec28b75187d01be9642af56a54a6101f51b25e4a7 /src
first commitHEADmaster
Diffstat (limited to 'src')
-rw-r--r--src/basic/defs.hpp14
-rw-r--r--src/basic/math.cpp98
-rw-r--r--src/basic/math.hpp59
-rw-r--r--src/common/stb_truetype.cpp3
-rw-r--r--src/games/Game.cpp39
-rw-r--r--src/games/Game.hpp26
-rw-r--r--src/games/minesweeper/Minesweeper.cpp321
-rw-r--r--src/games/minesweeper/Minesweeper.hpp81
-rw-r--r--src/games/snake/Snake.cpp345
-rw-r--r--src/games/snake/Snake.hpp62
-rw-r--r--src/games/tetris/Board.cpp139
-rw-r--r--src/games/tetris/Board.hpp30
-rw-r--r--src/games/tetris/Tetris.cpp368
-rw-r--r--src/games/tetris/Tetris.hpp67
-rw-r--r--src/games/tetris/Tetromino.cpp212
-rw-r--r--src/games/tetris/Tetromino.hpp59
-rw-r--r--src/main.cpp191
-rw-r--r--src/renderer/RenderGroup.cpp94
-rw-r--r--src/renderer/RenderGroup.hpp83
-rw-r--r--src/renderer/Renderer.cpp18
-rw-r--r--src/renderer/Renderer.hpp26
-rw-r--r--src/renderer/opengl/GlIndexBuffer.cpp44
-rw-r--r--src/renderer/opengl/GlIndexBuffer.hpp23
-rw-r--r--src/renderer/opengl/GlRenderer.cpp220
-rw-r--r--src/renderer/opengl/GlRenderer.hpp34
-rw-r--r--src/renderer/opengl/GlShader.cpp80
-rw-r--r--src/renderer/opengl/GlShader.hpp15
-rw-r--r--src/renderer/opengl/GlVertexBuffer.cpp54
-rw-r--r--src/renderer/opengl/GlVertexBuffer.hpp27
29 files changed, 2832 insertions, 0 deletions
diff --git a/src/basic/defs.hpp b/src/basic/defs.hpp
new file mode 100644
index 0000000..bfd302e
--- /dev/null
+++ b/src/basic/defs.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+#include <cassert>
+#include <cstdint>
+#include <cstddef>
+
+#define ARRAY_COUNT(x) (sizeof(x) / sizeof(x[0]))
+
+#define InvalidDefaultCase assert(0)
+
+#define KIBIBYTES(x) ((x)*1024)
+#define MEBIBYTES(x) ((x)*KIBIBYTES(1024))
+#define GIBIBYTES(x) ((x)*MEBIBYTES(1024))
+
diff --git a/src/basic/math.cpp b/src/basic/math.cpp
new file mode 100644
index 0000000..3067255
--- /dev/null
+++ b/src/basic/math.cpp
@@ -0,0 +1,98 @@
+#include <basic/math.hpp>
+
+
+V2ST::V2ST(size_t x, size_t y) : x(x), y(y) {
+}
+
+V2ST::V2ST(int32_t x, int32_t y) : x((size_t)x), y((size_t)y) {
+}
+
+bool V2ST::operator==(V2ST &b) {
+ bool result = this->x == b.x && this->y == b.y;
+ return result;
+}
+
+
+V2F32::V2F32(float x, float y) {
+ this->x = x;
+ this->y = y;
+}
+
+V2F32 V2F32::operator/(float scalar) {
+ V2F32 result = {};
+ result.x = this->x / scalar;
+ result.y = this->y / scalar;
+ return result;
+}
+
+V2F32 V2F32::operator*(float scalar) {
+ V2F32 result = {};
+ result.x = this->x * scalar;
+ result.y = this->y * scalar;
+ return result;
+}
+
+V2F32 V2F32::operator+(V2F32 other) {
+ V2F32 result = {};
+ result.x = this->x + other.x;
+ result.y = this->y + other.y;
+ return result;
+}
+
+
+V3F32::V3F32(float x, float y, float z) {
+ this->x = x;
+ this->y = y;
+ this->z = z;
+}
+
+V3F32 V3F32::operator/(float scalar) {
+ V3F32 result = {};
+ result.x = this->x / scalar;
+ result.y = this->y / scalar;
+ result.z = this->z / scalar;
+ return result;
+}
+
+V3F32 V3F32::operator*(float scalar) {
+ V3F32 result = {};
+ result.x = this->x * scalar;
+ result.y = this->y * scalar;
+ result.z = this->z * scalar;
+ return result;
+}
+
+
+V4F32::V4F32(float x, float y, float z, float w) {
+ this->x = x;
+ this->y = y;
+ this->z = z;
+ this->w = w;
+}
+
+V4F32 V4F32::operator/(float scalar) {
+ V4F32 result = {};
+ result.x = this->x / scalar;
+ result.y = this->y / scalar;
+ result.z = this->z / scalar;
+ result.w = this->w / scalar;
+ return result;
+}
+
+V4F32 V4F32::operator*(float scalar) {
+ V4F32 result = {};
+ result.x = this->x * scalar;
+ result.y = this->y * scalar;
+ result.z = this->z * scalar;
+ result.w = this->z * scalar;
+ return result;
+}
+
+V2I32::V2I32(int32_t x, int32_t y) : x(x), y(y) {
+}
+
+bool V2I32::operator==(V2I32 other) {
+ bool result = x == other.x && y == other.y;
+ return result;
+}
+
diff --git a/src/basic/math.hpp b/src/basic/math.hpp
new file mode 100644
index 0000000..0f21181
--- /dev/null
+++ b/src/basic/math.hpp
@@ -0,0 +1,59 @@
+#pragma once
+
+#include <basic/defs.hpp>
+
+struct V2ST {
+ size_t x;
+ size_t y;
+
+ V2ST() = default;
+ V2ST(size_t x, size_t y);
+ V2ST(int32_t x, int32_t y);
+ bool operator==(V2ST &b);
+ bool operator==(const V2ST& other) const {
+ return x == other.x && y == other.y;
+ }
+};
+
+struct V2F32 {
+ float x;
+ float y;
+
+ V2F32() = default;
+ V2F32(float x, float y);
+ V2F32 operator/(float scalar);
+ V2F32 operator*(float scalar);
+ V2F32 operator+(V2F32 other);
+};
+
+struct V3F32 {
+ float x;
+ float y;
+ float z;
+
+ V3F32() = default;
+ V3F32(float x, float y, float z);
+ V3F32 operator/(float scalar);
+ V3F32 operator*(float scalar);
+};
+
+struct V4F32 {
+ float x;
+ float y;
+ float z;
+ float w;
+
+ V4F32() = default;
+ V4F32 (float x, float y, float z, float w);
+ V4F32 operator/(float scalar);
+ V4F32 operator*(float scalar);
+};
+
+struct V2I32 {
+ int32_t x;
+ int32_t y;
+
+ V2I32() = default;
+ V2I32 (int32_t x, int32_t y);
+ bool operator==(V2I32 other);
+};
diff --git a/src/common/stb_truetype.cpp b/src/common/stb_truetype.cpp
new file mode 100644
index 0000000..cc11b20
--- /dev/null
+++ b/src/common/stb_truetype.cpp
@@ -0,0 +1,3 @@
+#define STB_TRUETYPE_IMPLEMENTATION
+#include <stb_truetype.h>
+
diff --git a/src/games/Game.cpp b/src/games/Game.cpp
new file mode 100644
index 0000000..628f5d4
--- /dev/null
+++ b/src/games/Game.cpp
@@ -0,0 +1,39 @@
+#include <basic/defs.hpp>
+#include <games/Game.hpp>
+#include <games/tetris/Tetris.hpp>
+#include <games/snake/Snake.hpp>
+#include <games/minesweeper/Minesweeper.hpp>
+
+#include <assert.h>
+#include <memory>
+
+
+std::unique_ptr<Game>
+Game::Select(GameType type)
+{
+ switch (type) {
+ case NO_GAME: {
+ return nullptr;
+ }
+
+ case TETRIS: {
+ return std::make_unique<Tetris>();
+ } break;
+
+ case SNAKE: {
+ return std::make_unique<Snake>();
+ } break;
+
+ case MINESWEEPER: {
+ return std::make_unique<Minesweeper>();
+ } break;
+
+ InvalidDefaultCase;
+ }
+
+ return nullptr;
+}
+
+
+Game::~Game() {}
+
diff --git a/src/games/Game.hpp b/src/games/Game.hpp
new file mode 100644
index 0000000..9af98b4
--- /dev/null
+++ b/src/games/Game.hpp
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <basic/defs.hpp>
+#include <SDL3/SDL.h>
+#include <memory>
+#include <vector>
+#include <renderer/RenderGroup.hpp>
+
+struct SDL_Window;
+
+class Game {
+public:
+ enum GameType {
+ NO_GAME,
+ TETRIS,
+ SNAKE,
+ MINESWEEPER
+ };
+
+ Game() = default;
+ virtual ~Game();
+ static std::unique_ptr<Game> Select(GameType type);
+
+ virtual bool Update(std::vector<SDL_Event> &events, RenderGroup &render_group) = 0;
+};
+
diff --git a/src/games/minesweeper/Minesweeper.cpp b/src/games/minesweeper/Minesweeper.cpp
new file mode 100644
index 0000000..60502c6
--- /dev/null
+++ b/src/games/minesweeper/Minesweeper.cpp
@@ -0,0 +1,321 @@
+#include <renderer/RenderGroup.hpp>
+#include <games/minesweeper/Minesweeper.hpp>
+
+#include <algorithm>
+#include <random>
+
+// Todo: draw a difficulty selection menu at top-mid.
+
+// Note: How many mines?
+// - Beginner: 8x8 or 9x9 grid with 10 mines.
+// - Intermediate: 16x16 grid with 40 mines.
+// - Expert: 30x16 grid with 99 mines.
+
+Minesweeper::Minesweeper()
+{
+ float map_width = static_cast<float>(m_MapWidth);
+ float map_height = static_cast<float>(m_MapHeight);
+ float cell_size = 0.8f * std::min(m_WorldHeight / MAX_MAP_HEIGHT, m_WorldWidth / MAX_MAP_WIDTH);
+ float cell_size_without_border = 0.8f * cell_size;
+
+ m_MapViewPos = {
+ 0.1f * cell_size_without_border + (m_WorldWidth - cell_size * map_width) / 2,
+ 0.1f * cell_size_without_border + (m_WorldHeight - cell_size * map_height) / 2
+ };
+ m_CellOuterViewSize = {cell_size, cell_size};
+ m_CellInnerViewSize = {cell_size_without_border, cell_size_without_border};
+
+
+ // Todo: assert various stuff
+
+ Reinit();
+}
+
+void Minesweeper::Reinit() {
+ int32_t mine_count = 40;
+ memset(m_IsCoveredBitmap, 0xff, sizeof(m_IsCoveredBitmap));
+ memset(m_IsFlaggedBitmap, 0 , sizeof(m_IsFlaggedBitmap));
+ InitIsMineBitmap(mine_count);
+ InitAdjacentMineCounters();
+}
+
+void Minesweeper::InitIsMineBitmap(int32_t mine_count) {
+ assert(mine_count < m_MapWidth * m_MapHeight);
+
+ memset(m_IsMineBitmap, 0 , sizeof(m_IsMineBitmap));
+
+ std::mt19937 rng((std::random_device()()));
+ std::uniform_int_distribution<int32_t> dist(0, m_MapWidth * m_MapHeight - 1);
+
+ while (mine_count) {
+ int32_t random_pos = dist(rng);
+ int32_t x = random_pos / m_MapWidth;
+ int32_t y = random_pos % m_MapWidth;
+ if (!IsMine(x, y)) {
+ m_IsMineBitmap[y] |= 1 << x;
+ mine_count--;
+ }
+ }
+}
+
+void Minesweeper::InitAdjacentMineCounters() {
+ for (int32_t y = 0; y < m_MapHeight; y++) {
+ int32_t y0 = y > 0 ? y-1 : y;
+ int32_t y1 = y < m_MapHeight-1 ? y+1 : y;
+
+ for (int32_t x = 0; x < m_MapHeight; x++) {
+ int32_t x0 = x > 0 ? x-1 : x;
+ int32_t x1 = x < m_MapWidth-1 ? x+1 : x;
+
+ int32_t adjacent_mine_counter = 0;
+ for (int32_t inner_y = y0; inner_y <= y1; inner_y++) {
+ for (int32_t inner_x = x0; inner_x <= x1; inner_x++) {
+ if (IsMine(inner_x, inner_y)) {
+ adjacent_mine_counter++;
+ }
+ }
+ }
+ if (IsMine(x, y)) {
+ adjacent_mine_counter = -1;
+ }
+
+ m_AdjacentMineCounters[y * m_MapWidth + x] = adjacent_mine_counter;
+ }
+ }
+}
+
+bool Minesweeper::Update(std::vector<SDL_Event> &events, RenderGroup &render_group) {
+ V3F32 clear_color = {0.3f, 0.2f, 0.3f};
+ render_group.SetCameraSize(4.0f, 3.0f);
+ render_group.Clear(clear_color);
+
+ if (m_RunState == MinesweeperRunState::Restart) {
+ Reinit();
+ m_RunState = MinesweeperRunState::Resume;
+ }
+
+ for (SDL_Event &event : events) {
+ if (m_RunState == MinesweeperRunState::Exit) {
+ return false;
+ }
+ else if (m_RunState == MinesweeperRunState::Pause) {
+ ProcessEventDuringPause(event, render_group);
+ }
+ else if (m_RunState == MinesweeperRunState::Resume) {
+ ProcessEventDuringResume(event, render_group);
+ }
+ }
+
+ if (m_RunState == MinesweeperRunState::Pause) {
+ DrawPauseMenu(render_group);
+ }
+ else if (m_RunState == MinesweeperRunState::GameOver) {
+ DrawGameOverMenu(render_group);
+ }
+
+ DrawBoard(render_group);
+
+ bool keep_running = m_RunState != MinesweeperRunState::Exit;
+ return keep_running;
+}
+
+void Minesweeper::ProcessEventDuringPause(SDL_Event &event, RenderGroup &render_group) {
+ switch (event.type) {
+ case SDL_EVENT_KEY_DOWN: {
+ if (event.key.key == SDLK_ESCAPE) {
+ m_RunState = MinesweeperRunState::Resume;
+ }
+ } break;
+ default:;
+ }
+}
+
+void Minesweeper::ProcessEventDuringResume(SDL_Event &event, RenderGroup &render_group) {
+ switch (event.type) {
+ case SDL_EVENT_KEY_DOWN: {
+ if (event.key.key == SDLK_ESCAPE) {
+ m_RunState = MinesweeperRunState::Pause;
+ }
+ } break;
+
+ case SDL_EVENT_MOUSE_BUTTON_DOWN: {
+ V2F32 click_screen_pos = {event.button.x, (float)render_group.m_ScreenHeight -1 - event.button.y};
+ V2F32 click_view_pos = ScreenPosToViewPos(click_screen_pos, render_group);
+
+ float x_adjusted = click_view_pos.x - m_MapViewPos.x;
+ float y_adjusted = click_view_pos.y - m_MapViewPos.y;
+ if (x_adjusted < 0.0f) {
+ break;
+ }
+ if (y_adjusted < 0.0f) {
+ break;
+ }
+
+ int32_t x = (int32_t)(x_adjusted / m_CellOuterViewSize.x);
+ int32_t y = (int32_t)(y_adjusted / m_CellOuterViewSize.y);
+ if (x >= m_MapWidth) {
+ break;
+ }
+ if (y >= m_MapHeight) {
+ break;
+ }
+
+ if (event.button.button == 1) {
+ if (IsCovered(x, y)) {
+ if (IsMine(x, y)) {
+ m_IsCoveredBitmap[y] &= ~(1 << x);
+ m_RunState = MinesweeperRunState::GameOver;
+ }
+ else {
+ Uncover(x, y);
+ }
+ }
+ }
+ else if (event.button.button == 3) {
+ if (IsCovered(x, y)) {
+ ToggleFlag(x ,y);
+ }
+ }
+
+ } break;
+
+ default:;
+ }
+}
+
+// Todo: maybe find a more efficient non-naive solution
+void Minesweeper::Uncover(int32_t x, int32_t y) {
+ if (x < 0) return;
+ if (x >= m_MapWidth) return;
+ if (y < 0) return;
+ if (y >= m_MapHeight) return;
+ if (!IsCovered(x, y)) return;
+
+ m_IsCoveredBitmap[y] &= ~(1 << x);
+ if (IsFlagged(x, y)) {
+ ToggleFlag(x, y);
+ }
+
+ if (m_AdjacentMineCounters[y*m_MapWidth + x] > 0) {
+ return;
+ }
+ Uncover(x-1, y-1);
+ Uncover(x , y-1);
+ Uncover(x+1, y-1);
+
+ Uncover(x-1, y);
+ Uncover(x+1, y);
+
+ Uncover(x-1, y+1);
+ Uncover(x , y+1);
+ Uncover(x+1, y+1);
+}
+
+void Minesweeper::ToggleFlag(int32_t x, int32_t y) {
+ m_IsFlaggedBitmap[y] ^= (1 << x);
+}
+
+bool Minesweeper::IsCovered(int32_t x, int32_t y) {
+ bool is_covered = m_IsCoveredBitmap[y] & 1 << x;
+ return is_covered;
+}
+
+bool Minesweeper::IsFlagged(int32_t x, int32_t y) {
+ bool is_flagged = m_IsFlaggedBitmap[y] & 1 << x;
+ return is_flagged;
+}
+
+bool Minesweeper::IsMine(int32_t x, int32_t y) {
+ bool is_mine = m_IsMineBitmap[y] & 1 << x;
+ return is_mine;
+}
+
+V2F32 Minesweeper::ScreenPosToViewPos(V2F32 screen_pos, RenderGroup &render_group) {
+ // e.g. [0, 1024] -> [0, 1] -> [0, 4]
+ // e.g. [0, 768] -> [0, 1] -> [0, 3]
+ float screen_width = (float)render_group.m_ScreenWidth;
+ float screen_height = (float)render_group.m_ScreenHeight;
+
+ V2F32 view_pos;
+ view_pos.x = (screen_pos.x / screen_width) * m_WorldWidth;
+ view_pos.y = (screen_pos.y / screen_height) * m_WorldHeight;
+ return view_pos;
+}
+
+void Minesweeper::DrawPauseMenu(RenderGroup &render_group) {
+ ImGui::Begin("MinesweeperPause");
+ if (ImGui::Button("Resume")) {
+ m_RunState = MinesweeperRunState::Resume;
+ }
+ if (ImGui::Button("Exit")) {
+ m_RunState = MinesweeperRunState::Exit;
+ }
+ ImGui::End();
+}
+
+void Minesweeper::DrawGameOverMenu(RenderGroup &render_group) {
+ ImGui::Begin("MinesweeperGameOver");
+ ImGui::Text("Score = ???");
+ if (ImGui::Button("Restart")) {
+ m_RunState = MinesweeperRunState::Restart;
+ }
+ if (ImGui::Button("Exit")) {
+ m_RunState = MinesweeperRunState::Exit;
+ }
+ ImGui::End();
+}
+
+void Minesweeper::DrawBoard(RenderGroup &render_group) {
+ V3F32 covered_cell_color = V3F32(0.4f, 0.4f, 0.4f);
+ V3F32 uncovered_cell_color = V3F32(0.2f, 0.2f, 0.2f);
+ V3F32 flag_color = {0.6f, 0.3f, 03.f};
+ V3F32 mine_color = {0.8f, 0.2f, 0.2f};
+
+ V2F32 flag_draw_size = {m_CellInnerViewSize.x * 0.5f, m_CellInnerViewSize.y * 0.5f};
+ V2F32 flag_draw_offset = {
+ (m_CellInnerViewSize.x - flag_draw_size.x) / 2,
+ (m_CellInnerViewSize.y - flag_draw_size.y) / 2
+ };
+
+
+
+ // Todo: avoid if-statement by having them in separate contiguous locations?
+
+ for (int32_t y = 0; y < m_MapHeight; y++) {
+ for (int32_t x = 0; x < m_MapWidth; x++) {
+ V3F32 world_pos = {
+ m_MapViewPos.x + (float)x * m_CellOuterViewSize.x,
+ m_MapViewPos.y + (float)y * m_CellOuterViewSize.y,
+ 0.0f
+ };
+ bool is_covered = IsCovered(x, y);
+ bool is_flagged = IsFlagged(x, y);
+ bool is_mine = IsMine(x, y);
+
+ if (is_covered) {
+ render_group.PushRectangle(world_pos, m_CellInnerViewSize, covered_cell_color);
+ }
+ if (is_covered && is_flagged) {
+ assert(IsCovered(x ,y));
+ V3F32 flag_world_pos = {
+ world_pos.x + flag_draw_offset.x,
+ world_pos.y + flag_draw_offset.y,
+ 1.0f
+ };
+ render_group.PushRectangle(flag_world_pos, flag_draw_size, flag_color);
+ }
+ if (!is_covered && !is_mine) {
+ render_group.PushRectangle(world_pos, m_CellInnerViewSize, uncovered_cell_color);
+ }
+ if (!is_covered && is_mine) {
+ V3F32 mine_world_pos = {
+ world_pos.x,
+ world_pos.y,
+ 2.0f
+ };
+ render_group.PushRectangle(mine_world_pos, m_CellInnerViewSize, mine_color);
+ }
+ }
+ }
+}
+
diff --git a/src/games/minesweeper/Minesweeper.hpp b/src/games/minesweeper/Minesweeper.hpp
new file mode 100644
index 0000000..4906464
--- /dev/null
+++ b/src/games/minesweeper/Minesweeper.hpp
@@ -0,0 +1,81 @@
+#pragma once
+
+#include <games/Game.hpp>
+
+namespace std {
+ template <>
+ struct hash<V2ST> {
+ size_t operator()(const V2ST& v) const {
+ size_t h1 = hash<size_t>{}(v.x);
+ size_t h2 = hash<size_t>{}(v.y);
+ return h1 ^ (h2 << 1);
+ }
+ };
+}
+
+enum class MinesweeperRunState {
+ Resume,
+ Pause,
+ GameOver,
+ Restart,
+ Exit
+};
+
+class Minesweeper : public Game {
+ public:
+ Minesweeper();
+ ~Minesweeper() = default;
+
+ bool Update(std::vector<SDL_Event> &events, RenderGroup &render_group) override;
+
+ void ProcessEventDuringPause(SDL_Event &event, RenderGroup &render_group);
+ void ProcessEventDuringResume(SDL_Event &event, RenderGroup &render_group);
+
+
+ private:
+ void Reinit();
+ void InitIsMineBitmap(int32_t mine_count);
+ void InitAdjacentMineCounters();
+
+ void Uncover(int32_t x, int32_t y);
+ void ToggleFlag(int32_t x, int32_t y);
+
+ bool IsCovered(int32_t x, int32_t y);
+ bool IsFlagged(int32_t x, int32_t y);
+ bool IsMine(int32_t x, int32_t y);
+
+ V2F32 ScreenPosToViewPos(V2F32 screen_pos, RenderGroup &render_group);
+
+
+ private:
+ void DrawPauseMenu(RenderGroup &render_group);
+ void DrawGameOverMenu(RenderGroup &render_group);
+ void DrawBoard(RenderGroup &render_group);
+
+
+ private:
+ static constexpr int32_t MAX_MAP_HEIGHT = 32;
+ static constexpr int32_t MAX_MAP_WIDTH = 32;
+ static constexpr std::string_view s_FontFilepath = "./fonts/dejavu_ttf/DejaVuSans.ttf";
+
+
+ private:
+ MinesweeperRunState m_RunState = MinesweeperRunState::Resume;
+
+ float m_WorldWidth = 4.0f;
+ float m_WorldHeight = 3.0f;
+
+ int32_t m_MapWidth = 16;
+ int32_t m_MapHeight = 16;
+
+ V2F32 m_MapViewPos;
+ V2F32 m_CellOuterViewSize;
+ V2F32 m_CellInnerViewSize;
+
+ uint32_t m_IsCoveredBitmap[MAX_MAP_HEIGHT] {};
+ uint32_t m_IsFlaggedBitmap[MAX_MAP_HEIGHT] {};
+ uint32_t m_IsMineBitmap[MAX_MAP_HEIGHT] {};
+ int32_t m_AdjacentMineCounters[MAX_MAP_WIDTH * MAX_MAP_HEIGHT] {};
+};
+
+
diff --git a/src/games/snake/Snake.cpp b/src/games/snake/Snake.cpp
new file mode 100644
index 0000000..9764a9e
--- /dev/null
+++ b/src/games/snake/Snake.cpp
@@ -0,0 +1,345 @@
+#include <games/snake/Snake.hpp>
+#include <imgui.h>
+
+
+Snake::Snake () {
+ m_IsPaused = false;
+ m_IsRunning = true;
+
+ m_DtInSecondsRemaining = 0.0f;
+ m_LastMillisecondsSinceT0 = SDL_GetTicks();
+
+ m_TilesPerSecond = 4.0f;
+ m_Direction = DIRECTION_RIGHT;
+ m_LastAdvancedDirection = DIRECTION_RIGHT;
+
+ m_MapWidth = 16;
+ m_MapHeight = 16;
+ assert(MAX_MAP_WIDTH <= 64); // m_BodyBitmap is uint64_t[]. We can't exceed that!
+ assert(MAX_MAP_HEIGHT <= 64);
+ assert(m_MapWidth <= MAX_MAP_WIDTH);
+ assert(m_MapHeight <= MAX_MAP_WIDTH);
+
+ m_Tail = 0;
+ m_Head = 1;
+ memset(m_BodyBitmap, 0, sizeof(m_BodyBitmap));
+
+ int32_t head_x = m_MapWidth / 2;
+ int32_t head_y = m_MapHeight / 2;
+ m_BodyPositions[0] = {head_x -1, head_y};
+ m_BodyPositions[1] = {head_x, head_y};
+
+ m_Rng = std::mt19937((std::random_device()()));
+ m_Dist = std::uniform_int_distribution<int32_t>(0, m_MapWidth * m_MapHeight - 3);
+
+ SpawnFood();
+ }
+
+bool Snake::Update(std::vector<SDL_Event> &events, RenderGroup &render_group) {
+ uint64_t milliseconds_since_t0 = SDL_GetTicks();
+ uint64_t milliseconds_since_t0_last = m_LastMillisecondsSinceT0;
+ uint64_t dt_in_milliseconds = milliseconds_since_t0 - milliseconds_since_t0_last;
+ float dt_in_seconds = (float)dt_in_milliseconds / 1000.0f;
+ m_LastMillisecondsSinceT0 = milliseconds_since_t0;
+
+
+ V3F32 clear_color = V3F32(0.3f, 0.3f, 0.3f);
+ render_group.SetCameraSize(4.0f, 3.0f);
+ render_group.Clear(clear_color);
+
+
+ for (SDL_Event &event : events) {
+ if (!m_IsRunning) {
+ printf("event loop is running just false\n");
+ return false;
+ }
+ if (m_IsPaused) {
+ ProcessEventDuringPause(event);
+ }
+ else {
+ ProcessEventDuringResume(event);
+ }
+ }
+
+ if (!m_IsPaused) {
+ MaybeMoveSnake(dt_in_seconds);
+ }
+
+
+ Draw(render_group);
+ DoImgui();
+
+ return m_IsRunning;
+}
+
+void Snake::MaybeMoveSnake(float dt_in_seconds) {
+ float dt_in_seconds_to_use = m_DtInSecondsRemaining + dt_in_seconds;
+ float tiles_per_second = m_TilesPerSecond;
+ float seconds_per_tile = 1.0f / tiles_per_second;
+ while (dt_in_seconds_to_use > seconds_per_tile) {
+ V2I32 head_pos = m_BodyPositions[m_Head];
+ V2I32 tail_pos = m_BodyPositions[m_Tail];
+
+
+ // find head_pos
+ if (m_Direction == DIRECTION_UP) {
+ head_pos.y += 1;
+ }
+ else if (m_Direction == DIRECTION_DOWN) {
+ head_pos.y -= 1;
+ }
+ else if (m_Direction == DIRECTION_RIGHT) {
+ head_pos.x += 1;
+ }
+ else if (m_Direction == DIRECTION_LEFT) {
+ head_pos.x -= 1;
+ }
+ if ((head_pos.x < 0 || head_pos.x >= m_MapWidth) ||
+ (head_pos.y < 0 || head_pos.y >= m_MapHeight))
+ {
+ m_IsRunning = false;
+ return;
+ }
+ uint64_t head_bit = 1 << head_pos.x;
+ uint64_t body_bits = m_BodyBitmap[head_pos.y];
+ if (head_pos.y == tail_pos.y) {
+ body_bits &= ~(1 << tail_pos.x);
+ }
+ if (head_bit & body_bits) {
+ m_IsRunning = false;
+ return;
+ }
+
+
+ // advance head
+ int32_t max_positions = sizeof(m_BodyPositions) / sizeof(m_BodyPositions[0]);
+ m_Head += 1;
+ if (m_Head >= max_positions) {
+ m_Head = 0;
+ }
+
+ m_BodyPositions[m_Head] = head_pos;
+ m_BodyBitmap[head_pos.y] |= (1 << head_pos.x);
+
+
+ if (m_BodyPositions[m_Head] == m_FoodPosition) {
+ SpawnFood();
+ }
+ else {
+ // advance tail
+ V2I32 next_tail_pos = m_BodyPositions[m_Tail];
+ m_BodyBitmap[next_tail_pos.y] &= ~(1 << next_tail_pos.x);
+
+ m_Tail += 1;
+ if (m_Tail >= max_positions) {
+ m_Tail = 0;
+ }
+ }
+
+
+ m_LastAdvancedDirection = m_Direction;
+ dt_in_seconds_to_use -= seconds_per_tile;
+ }
+
+
+ m_DtInSecondsRemaining = dt_in_seconds_to_use;
+}
+
+void Snake::ProcessEventDuringPause(SDL_Event &event) {
+ switch (event.type) {
+ case SDL_EVENT_KEY_DOWN: {
+ if (event.key.key == SDLK_ESCAPE) {
+ m_IsPaused = false;
+ }
+ }
+ default:;
+ }
+}
+
+void Snake::ProcessEventDuringResume(SDL_Event &event) {
+ switch (event.type) {
+ case SDL_EVENT_KEY_DOWN: {
+ if (event.key.key == SDLK_UP) {
+ if (m_LastAdvancedDirection == DIRECTION_RIGHT ||
+ m_LastAdvancedDirection == DIRECTION_LEFT)
+ {
+ m_Direction = DIRECTION_UP;
+ }
+ }
+ else if (event.key.key == SDLK_DOWN) {
+ if (m_LastAdvancedDirection == DIRECTION_RIGHT ||
+ m_LastAdvancedDirection == DIRECTION_LEFT)
+ {
+ m_Direction = DIRECTION_DOWN;
+ }
+ }
+ else if (event.key.key == SDLK_RIGHT) {
+ if (m_LastAdvancedDirection == DIRECTION_UP ||
+ m_LastAdvancedDirection == DIRECTION_DOWN)
+ {
+ m_Direction = DIRECTION_RIGHT;
+ }
+ }
+ else if (event.key.key == SDLK_LEFT) {
+ if (m_LastAdvancedDirection == DIRECTION_UP ||
+ m_LastAdvancedDirection == DIRECTION_DOWN)
+ {
+ m_Direction = DIRECTION_LEFT;
+ }
+ }
+ else if (event.key.key == SDLK_ESCAPE) {
+ m_IsPaused = true;
+ }
+ }
+
+ default:;
+ }
+}
+
+void Snake::SpawnFood() {
+ int32_t bit0_counts[MAX_MAP_HEIGHT];
+ int32_t bit0_count_total = 0;
+
+ // count bits
+ for (int32_t y = 0; y < m_MapHeight; y++) {
+ int32_t bit1_count = 0;
+
+ uint64_t bitmap_row = m_BodyBitmap[y];
+ while (bitmap_row != 0) {
+ bitmap_row = bitmap_row & (bitmap_row - 1);
+ bit1_count += 1;
+ }
+
+ int32_t bit0_count = m_MapWidth - bit1_count;
+ bit0_counts[y] = bit0_count;
+ bit0_count_total += bit0_count;
+ }
+
+ if (bit0_count_total == 0) {
+ return;
+ }
+
+ m_Dist.param(std::uniform_int_distribution<int32_t>::param_type(0, bit0_count_total - 1));
+ int32_t bit0_index = m_Dist(m_Rng);
+ int32_t bit0_x = 0;
+ int32_t bit0_y = 0;
+
+ // find y
+ for (int32_t y = 0; y < m_MapHeight; y++) {
+ if (bit0_index < bit0_counts[y]) {
+ bit0_y = y;
+ break;
+ }
+ bit0_index -= bit0_counts[y];
+ }
+
+ // find x
+ uint64_t bitmap_row_not = ~m_BodyBitmap[bit0_y];
+ for (int32_t x = 0; x < m_MapWidth; x++) {
+ if (bitmap_row_not & 1) {
+ if (bit0_index == 0) {
+ bit0_x = x;
+ break;
+ }
+ bit0_index--;
+ }
+ bitmap_row_not >>= 1;
+ }
+
+ m_FoodPosition = {bit0_x, bit0_y};
+}
+
+void Snake::Draw(RenderGroup &render_group) {
+ float world_width = 4.0f;
+ float world_height = 3.0f;
+
+ float tile_size = (world_width / 2) / MAX_MAP_WIDTH;
+ float bodypart_size = 0.8f * tile_size;
+ float bodypart_offset = (tile_size - bodypart_size) / 2;
+
+ float map_view_width = tile_size * (float)m_MapWidth;
+ float map_view_height = tile_size * (float)m_MapHeight;
+ float map_x = (world_width - map_view_width) / 2;
+ float map_y = (world_height - map_view_height) / 2;
+
+ int32_t max_positions = sizeof(m_BodyPositions) / sizeof(m_BodyPositions[0]);
+
+
+ /* draw map background */
+ V3F32 map_world_pos = {map_x, map_y, 0.0f};
+ V2F32 map_world_dim = {map_view_width, map_view_height};
+ V3F32 bg_color = {0.0f, 0.0f, 0.0f};
+ render_group.PushRectangle(map_world_pos, map_world_dim, bg_color);
+
+
+ /* draw snake */
+ // 1) if tail > head: advance to end first
+ int32_t tail = m_Tail;
+ if (tail > m_Head) {
+ while (tail < max_positions) {
+ V3F32 local_pos = {
+ (float)m_BodyPositions[tail].x * tile_size + bodypart_offset,
+ (float)m_BodyPositions[tail].y * tile_size + bodypart_offset,
+ 1.0f
+ };
+ V2F32 local_dim = {bodypart_size, bodypart_size};
+
+ V3F32 world_pos = {
+ map_world_pos.x + local_pos.x,
+ map_world_pos.y + local_pos.y,
+ 1.0f
+ };
+ V2F32 world_dim = local_dim;
+
+ V3F32 color = {0.3f, 0.3f, 0.3f};
+ render_group.PushRectangle(world_pos, world_dim, color);
+ tail++;
+ }
+ tail = 0;
+ }
+ // 2) advance to head
+ while (tail <= m_Head) {
+ V3F32 local_pos = {
+ (float)m_BodyPositions[tail].x * tile_size + bodypart_offset,
+ (float)m_BodyPositions[tail].y * tile_size + bodypart_offset,
+ 1.0f
+ };
+ V2F32 local_dim = {bodypart_size, bodypart_size};
+
+ V3F32 world_pos = {
+ map_world_pos.x + local_pos.x,
+ map_world_pos.y + local_pos.y,
+ 1.0f
+ };
+ V2F32 world_dim = local_dim;
+
+ V3F32 color = {0.3f, 0.3f, 0.3f};
+ render_group.PushRectangle(world_pos, world_dim, color);
+ tail++;
+ }
+
+
+ /* draw food */
+ V3F32 pos = {
+ map_world_pos.x + (float)m_FoodPosition.x * tile_size + bodypart_offset,
+ map_world_pos.y + (float)m_FoodPosition.y * tile_size + bodypart_offset,
+ 1.0f
+ };
+ V2F32 dim = {bodypart_size, bodypart_size};
+ V3F32 color = {0.3f, 0.6f, 0.4f};
+ render_group.PushRectangle(pos, dim, color);
+}
+
+void Snake::DoImgui() {
+ if (m_IsPaused) {
+ ImGui::Begin("SnakePause");
+ if (ImGui::Button("Resume")) {
+ m_IsPaused = false;
+ }
+ if (ImGui::Button("Exit")) {
+ m_IsRunning = false;
+ }
+ ImGui::End();
+ }
+}
+
diff --git a/src/games/snake/Snake.hpp b/src/games/snake/Snake.hpp
new file mode 100644
index 0000000..f04ad16
--- /dev/null
+++ b/src/games/snake/Snake.hpp
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <renderer/RenderGroup.hpp>
+#include <games/Game.hpp>
+
+#include <random>
+
+
+class Snake : public Game {
+public:
+ enum Direction : int32_t {
+ DIRECTION_UP,
+ DIRECTION_DOWN,
+ DIRECTION_LEFT,
+ DIRECTION_RIGHT,
+ };
+
+
+public:
+ Snake();
+ bool Update(std::vector<SDL_Event> &events, RenderGroup &render_group) override;
+
+
+private:
+ void ProcessEventDuringPause(SDL_Event &event);
+ void ProcessEventDuringResume(SDL_Event &event);
+
+ void MaybeMoveSnake(float dt_in_seconds);
+ void SpawnFood();
+
+ void Draw(RenderGroup &render_group);
+ void DoImgui();
+
+
+
+private:
+ static constexpr int32_t MAX_MAP_WIDTH = 16;
+ static constexpr int32_t MAX_MAP_HEIGHT = 16;
+
+ bool m_IsPaused;
+ bool m_IsRunning;
+
+ float m_DtInSecondsRemaining;
+ uint64_t m_LastMillisecondsSinceT0;
+
+ float m_TilesPerSecond;
+ Direction m_Direction;
+ Direction m_LastAdvancedDirection;
+
+ int32_t m_MapWidth;
+ int32_t m_MapHeight;
+ int32_t m_Tail;
+ int32_t m_Head;
+ uint64_t m_BodyBitmap[MAX_MAP_HEIGHT];
+ V2I32 m_BodyPositions[MAX_MAP_WIDTH * MAX_MAP_HEIGHT];
+ V2I32 m_FoodPosition;
+
+ std::mt19937 m_Rng;
+ std::uniform_int_distribution<int32_t> m_Dist;
+};
+
+
diff --git a/src/games/tetris/Board.cpp b/src/games/tetris/Board.cpp
new file mode 100644
index 0000000..9dff135
--- /dev/null
+++ b/src/games/tetris/Board.cpp
@@ -0,0 +1,139 @@
+#include <renderer/RenderGroup.hpp>
+#include <games/tetris/Tetromino.hpp>
+#include <games/tetris/Board.hpp>
+
+Board::Board() {
+ for (int y = 0; y < 2; y++) {
+ m_Bitmap[y] = 0xffff; // 1111111111111111
+ }
+ for (int y = 2; y < 24; y++) {
+ m_Bitmap[y] = 0xe007; // 1110000000000111
+ }
+
+ for (int y = 0; y < 22; y++) {
+ for (int x = 0; x < 10; x++) {
+ m_Idmap[y][x] = (uint8_t)TetrominoId::TETROMINO_ID_NONE;
+ }
+ }
+}
+
+int32_t Board::PlaceTetromino(Tetromino &tetromino) {
+ BoardPos pos = tetromino.GetPos();
+ TetrominoId id = tetromino.GetId();
+ uint16_t tetromino_bitmap[4];
+ tetromino.GetBitmap(tetromino_bitmap);
+
+
+ // check if Tetromino cannot be placed (Game Over)
+ if (tetromino.IsCollisionWithBoard()) {
+ return -1;
+ }
+
+
+ // place in Board's Bitmap
+ m_Bitmap[pos.y+0] |= tetromino_bitmap[0];
+ m_Bitmap[pos.y+1] |= tetromino_bitmap[1];
+ m_Bitmap[pos.y+2] |= tetromino_bitmap[2];
+ m_Bitmap[pos.y+3] |= tetromino_bitmap[3];
+
+
+ // place in Board's Idmap
+ for (int32_t y = 0; y < 4; y++) {
+ for (int32_t x = 0; x < 4; x++) {
+ int32_t bitmap_x = 0x8000 >> (pos.x + x);
+ if (tetromino_bitmap[y] & bitmap_x) {
+ int32_t idmap_x = pos.x + x - 3;
+ int32_t idmap_y = pos.y + y - 2;
+ m_Idmap[idmap_y][idmap_x] = static_cast<uint8_t>(id);
+ }
+ }
+ }
+
+ int32_t rows_cleared = ClearRows(pos.y);
+ return rows_cleared;
+}
+
+int32_t Board::ClearRows(int32_t y0) {
+ int32_t rows_cleared = 0;
+ int32_t y1 = y0 + 3;
+
+ // ignore for y = {0,1}. Those bitmap rows are all 1's for collision testing
+ if (y0 < 2) {
+ y0 += 2 - y0;
+ }
+
+ for (int32_t y = y0; y <= y1; y++) {
+ if (m_Bitmap[y] == 0xffff) {
+ rows_cleared++;
+ }
+ else {
+ m_Bitmap[y-rows_cleared] = m_Bitmap[y];
+ std::copy(m_Idmap[y-2], m_Idmap[y-2] + 10, m_Idmap[y-2-rows_cleared]);
+ }
+ }
+ for (int32_t y = y1+1; y < 24; y++) {
+ m_Bitmap[y-rows_cleared] = m_Bitmap[y];
+ std::copy(m_Idmap[y-2], m_Idmap[y-2] + 10, m_Idmap[y-2-rows_cleared]);
+ }
+ for (int32_t y = 24-rows_cleared; y < 24; y++) {
+ m_Bitmap[y] = 0xe007;
+ std::fill(m_Idmap[y-2], m_Idmap[y-2] + 10, (uint8_t)TetrominoId::TETROMINO_ID_NONE);
+ }
+
+
+ return rows_cleared;
+}
+
+void Board::Draw(int32_t level, RenderGroup& render_group) {
+ float world_width = 4.0f;
+ float world_height = 3.0f;
+ float tetromino_size_with_border = world_height / 20.0f;
+ float tetromino_size = 0.8f * tetromino_size_with_border;
+ float tetromino_offset = 0.1f * tetromino_size_with_border;
+ V2F32 board_world_pos = {
+ (world_width - tetromino_size_with_border*10) / 2.0f,
+ 0.0f
+ };
+
+
+ // background
+ V3F32 bg_world_pos = {
+ board_world_pos.x,
+ board_world_pos.y,
+ 0.0f
+ };
+ V2F32 bg_world_dim = {
+ tetromino_size_with_border * 10,
+ tetromino_size_with_border * 20
+ };
+ V3F32 bg_color = {0.0f, 0.0f, 0.0f};
+ render_group.PushRectangle(bg_world_pos, bg_world_dim, bg_color);
+
+
+ // tetromino parts
+ for (size_t y = 0; y < 20; y++) {
+ for (size_t x = 0; x < 10; x++) {
+ uint8_t tetromino_id = m_Idmap[y][x];
+ if (tetromino_id < (uint8_t)TetrominoId::TETROMINO_ID_COUNT) {
+ V2F32 local_pos = {
+ (float)x * tetromino_size_with_border + tetromino_offset,
+ (float)y * tetromino_size_with_border + tetromino_offset
+ };
+ V2F32 local_dim = {tetromino_size, tetromino_size};
+
+
+ V3F32 world_pos = {
+ board_world_pos.x + local_pos.x,
+ board_world_pos.y + local_pos.y,
+ 1.0f
+ };
+ V2F32 world_dim = local_dim;
+
+
+ V3F32 color = Tetromino::GetColor(static_cast<TetrominoId>(tetromino_id));
+ render_group.PushRectangle(world_pos, world_dim, color);
+ }
+ }
+ }
+}
+
diff --git a/src/games/tetris/Board.hpp b/src/games/tetris/Board.hpp
new file mode 100644
index 0000000..0019a7f
--- /dev/null
+++ b/src/games/tetris/Board.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <basic/defs.hpp>
+#include <renderer/RenderGroup.hpp>
+
+class Tetromino;
+
+struct BoardPos {
+ int32_t x;
+ int32_t y;
+};
+
+class Board {
+public:
+ Board();
+
+ int32_t PlaceTetromino(Tetromino &tetromino);
+ void Draw(int32_t level, RenderGroup& render_group);
+
+
+private:
+ int32_t ClearRows(int32_t y0);
+
+
+private:
+ friend class Tetromino;
+ uint16_t m_Bitmap[24];
+ uint8_t m_Idmap[22][10];
+};
+
diff --git a/src/games/tetris/Tetris.cpp b/src/games/tetris/Tetris.cpp
new file mode 100644
index 0000000..e776fb0
--- /dev/null
+++ b/src/games/tetris/Tetris.cpp
@@ -0,0 +1,368 @@
+#include <games/tetris/Tetromino.hpp>
+#include <SDL3/SDL_events.h>
+#include <SDL3/SDL_timer.h>
+#include <games/tetris/Tetris.hpp>
+#include <imgui.h>
+
+#include <fstream>
+
+// Todo: change to new font scaling api in imgui first
+// Todo: test text with hardcoded gap + dummy to ensure it gets placed as expected
+
+Tetris::Tetris() :
+ m_ActiveTetromino(m_Board),
+ m_NextTetromino(m_Board)
+{
+ m_TetrominoCounters[(size_t)m_ActiveTetromino.GetId()] += 1;
+}
+
+void Tetris::Restart() {
+ m_RunningState = TetrisRunningState::Resume;
+ m_DtInSecondsRemaining = 0.0f;
+ m_MillisecondsSinceT0Last = SDL_GetTicks();
+
+ // Todo: Don't reconstruct! Make reset methods.
+ m_Board = Board();
+ m_ActiveTetromino = Tetromino(m_Board);
+ m_NextTetromino = Tetromino(m_Board);
+
+ memset(m_TetrominoCounters, 0, sizeof(m_TetrominoCounters));
+ m_Score = 0;
+ m_LineCounter = 0;
+ m_StartingLevel = 0;
+ m_Level = 0;
+ m_SoftdropCounter = 0;
+}
+
+bool Tetris::Update(std::vector<SDL_Event> &events, RenderGroup &render_group) {
+ V3F32 clear_color = V3F32(0.2f, 0.2f, 0.2f);
+ render_group.SetCameraSize(4.0f, 3.0f);
+ render_group.Clear(clear_color);
+
+ if (m_RunningState == TetrisRunningState::Restart) {
+ Restart();
+ }
+
+ uint64_t milliseconds_since_t0 = SDL_GetTicks();
+ uint64_t milliseconds_since_t0_last = m_MillisecondsSinceT0Last;
+ uint64_t milliseconds_dt = milliseconds_since_t0 - milliseconds_since_t0_last;
+ float seconds_dt = static_cast<float>(milliseconds_dt) / 1000.0f;
+ m_MillisecondsSinceT0Last = milliseconds_since_t0;
+
+
+ if (m_RunningState == TetrisRunningState::Resume) {
+ uint32_t harddrop_count = GetHarddropCount(seconds_dt);
+ while (harddrop_count) {
+ bool moved_down = m_ActiveTetromino.MaybeMoveDown();
+ if (!moved_down) {
+ HandleTetrominoPlacement();
+ }
+ harddrop_count--;
+ }
+ }
+
+
+ for (auto &event : events) {
+ using enum TetrisRunningState;
+ switch (m_RunningState) {
+ case Resume: UpdateResumeState(event); break;
+ case Pause: UpdatePauseState(event); break;
+ default:;
+ }
+ }
+
+ Draw(render_group);
+
+
+ bool keep_running = m_RunningState != TetrisRunningState::Exit;
+ return keep_running;
+}
+
+void Tetris::UpdateResumeState(SDL_Event &event) {
+ switch (event.type) {
+ case SDL_EVENT_KEY_DOWN: {
+ auto key = event.key.key;
+ if (key == SDLK_RIGHT) {
+ m_ActiveTetromino.MaybeMoveHorizontally(TetrominoDirection::Right);
+ } else if (key == SDLK_LEFT) {
+ m_ActiveTetromino.MaybeMoveHorizontally(TetrominoDirection::Left);
+ } else if (key == SDLK_DOWN) {
+ bool moved_down = m_ActiveTetromino.MaybeMoveDown();
+ if (!moved_down) {
+ HandleTetrominoPlacement();
+ }
+ else {
+ m_SoftdropCounter++;
+ }
+ } else if (key == SDLK_X) {
+ m_ActiveTetromino.MaybeRotate(TetrominoRotation::Clockwise);
+ } else if (key == SDLK_Z || key == SDLK_Y) {
+ m_ActiveTetromino.MaybeRotate(TetrominoRotation::CounterClockwise);
+ } else if (key == SDLK_ESCAPE) {
+ m_RunningState = TetrisRunningState::Pause;
+ }
+ }
+ default:;
+ }
+}
+
+void Tetris::UpdatePauseState(SDL_Event &event) {
+ switch (event.type) {
+ case SDL_EVENT_KEY_DOWN: {
+ auto key = event.key.key;
+ if (key == SDLK_ESCAPE) {
+ m_RunningState = TetrisRunningState::Resume;
+ }
+ }
+ default:;
+ }
+}
+
+void Tetris::HandleTetrominoPlacement() {
+ int32_t rows_cleared = m_Board.PlaceTetromino(m_ActiveTetromino);
+
+ m_ActiveTetromino = m_NextTetromino;
+ m_NextTetromino = Tetromino(m_Board);
+
+ if (rows_cleared == -1) {
+ HandleGameOver();
+ return;
+ }
+
+
+ m_LineCounter += rows_cleared;
+ m_TetrominoCounters[(size_t)m_ActiveTetromino.GetId()] += 1;
+
+ if (rows_cleared == 1) {
+ m_Score += 40 * (m_Level + 1);
+ }
+ else if (rows_cleared == 2) {
+ m_Score += 100 * (m_Level + 1);
+ }
+ else if (rows_cleared == 3) {
+ m_Score += 300 * (m_Level + 1);
+ }
+ else if (rows_cleared == 4) {
+ m_Score += 1200 * (m_Level + 1);
+ }
+
+ m_Score += m_SoftdropCounter;
+ m_SoftdropCounter = 0;
+
+ m_Level = m_StartingLevel + m_LineCounter / 10;
+}
+
+uint32_t Tetris::GetHarddropCount(float dt) {
+ float nes_frame_time = 1.0f / 60;
+ int32_t nes_frames_per_cell;
+ if (m_Level <= 8) nes_frames_per_cell = 48 - m_Level * 5;
+ else if (m_Level == 9) nes_frames_per_cell = 6;
+ else if (m_Level <= 12) nes_frames_per_cell = 5;
+ else if (m_Level <= 15) nes_frames_per_cell = 4;
+ else if (m_Level <= 18) nes_frames_per_cell = 3;
+ else if (m_Level <= 28) nes_frames_per_cell = 2;
+ else nes_frames_per_cell = 1;
+
+
+ float dt_level = static_cast<float>(nes_frames_per_cell) * nes_frame_time;
+ float dt_total = m_DtInSecondsRemaining + dt;
+
+ uint32_t harddrop_count = 0;
+ while (dt_total > dt_level) {
+ harddrop_count += 1;
+ dt_total -= dt_level;
+ }
+
+ m_DtInSecondsRemaining = dt_total;
+ return harddrop_count;
+}
+
+void Tetris::HandleGameOver() {
+ m_RunningState = TetrisRunningState::GameOver;
+ const char *filepath = "tetris_highscore.txt";
+ int32_t highscore = 0;
+
+
+ std::ifstream highscore_file_in { filepath };
+ if (highscore_file_in) {
+ highscore_file_in >> highscore;
+ highscore_file_in.close();
+ }
+ else {
+ SDL_LogInfo(0, "Tetris: cannot open tetris_highscore.txt for reading");
+ }
+
+ if (highscore > 0 && highscore > m_HighScore) {
+ m_HighScore = highscore;
+ }
+
+
+ if (m_Score > m_HighScore) {
+ m_HighScore = m_Score;
+ std::ofstream highscore_file_out { filepath };
+ if (highscore_file_out) {
+ highscore_file_out << m_HighScore << std::endl;
+ highscore_file_out.close();
+ }
+ else {
+ SDL_LogInfo(0, "Tetris: cannot open tetris_highscore.txt for writing");
+ }
+ }
+}
+
+void Tetris::Draw(RenderGroup &render_group) {
+ m_Board.Draw(m_Level, render_group);
+ m_ActiveTetromino.Draw(render_group);
+
+ DrawNextTetromino(render_group);
+ DrawStatistics(render_group);
+ DrawLineCounter(render_group);
+ DrawLevel(render_group);
+ DrawScore(render_group);
+
+ // Todo: Use transparency
+ if (m_RunningState == TetrisRunningState::Pause) {
+ DrawPauseMenu(render_group);
+ }
+ else if (m_RunningState == TetrisRunningState::GameOver) {
+ DrawGameOverMenu(render_group);
+ }
+}
+
+void Tetris::DrawPauseMenu(RenderGroup &render_group) {
+ ImGui::Begin("TetrisPause", nullptr, s_MenuImGuiWindowFlags);
+ if (ImGui::Button("Resume")) {
+ m_RunningState = TetrisRunningState::Resume;
+ }
+ if (ImGui::Button("Restart")) {
+ m_RunningState = TetrisRunningState::Restart;
+ }
+ if (ImGui::Button("Exit")) {
+ m_RunningState = TetrisRunningState::Exit;
+ }
+ ImGui::End();
+}
+
+void Tetris::DrawGameOverMenu(RenderGroup &render_group) {
+ ImGui::Begin("TetrisGameOver", nullptr, s_MenuImGuiWindowFlags);
+ ImGui::Text("Score = %d", m_Score);
+ ImGui::Text("HighScore = %d", m_HighScore);
+ if (ImGui::Button("Restart")) {
+ m_RunningState = TetrisRunningState::Restart;
+ }
+ if (ImGui::Button("Exit")) {
+ m_RunningState = TetrisRunningState::Exit;
+ }
+ ImGui::End();
+}
+
+void Tetris::DrawLineCounter(RenderGroup &render_group) {
+ V2F32 view_pos = {0.5f, 2.6f};
+ ImVec2 screen_pos = render_group.ViewPosToScreenPosImGui(view_pos);
+
+ ImGui::SetNextWindowPos(screen_pos);
+ ImGui::Begin("TetrisLines", nullptr, s_DefaultImGuiWindowFlags);
+ ImGui::Text("LINES - %d", m_LineCounter);
+ ImGui::End();
+}
+
+void Tetris::DrawStatistics(RenderGroup &render_group) {
+ V2F32 view_tetrominoes_pos = {0.4f, 1.8f};
+ V2F32 view_advance = {0.0f, 0.2f};
+
+ V2F32 view_text_title_pos = view_tetrominoes_pos + V2F32(0.02f, 0.4f);
+ ImVec2 screen_text_title_pos = render_group.ViewPosToScreenPosImGui(view_text_title_pos);
+
+ V2F32 view_text_pos = view_tetrominoes_pos + V2F32(0.4f, 0.16f);
+ V2F32 view_text_gap = {0.0f, 0.124f};
+ ImVec2 screen_text_pos = render_group.ViewPosToScreenPosImGui(view_text_pos);
+ ImVec2 screen_text_gap = render_group.ViewSizeToScreenSizeImGui(view_text_gap);
+
+
+ using enum TetrominoId;
+
+ Tetromino::Draw(TETROMINO_T, 0, view_tetrominoes_pos, 0.5f, render_group);
+ view_tetrominoes_pos.y -= view_advance.y;
+
+ Tetromino::Draw(TETROMINO_J, 0, view_tetrominoes_pos, 0.5f, render_group);
+ view_tetrominoes_pos.y -= view_advance.y;
+
+ Tetromino::Draw(TETROMINO_Z, 0, view_tetrominoes_pos, 0.5f, render_group);
+ view_tetrominoes_pos.y -= view_advance.y;
+
+ Tetromino::Draw(TETROMINO_O, 0, view_tetrominoes_pos, 0.5f, render_group);
+ view_tetrominoes_pos.y -= view_advance.y;
+
+ Tetromino::Draw(TETROMINO_S, 0, view_tetrominoes_pos, 0.5f, render_group);
+ view_tetrominoes_pos.y -= view_advance.y;
+
+ Tetromino::Draw(TETROMINO_L, 0, view_tetrominoes_pos, 0.5f, render_group);
+ view_tetrominoes_pos.y -= view_advance.y;
+
+ Tetromino::Draw(TETROMINO_I, 0, view_tetrominoes_pos, 0.5f, render_group);
+ view_tetrominoes_pos.y -= view_advance.y;
+
+
+ ImGui::SetNextWindowPos(screen_text_title_pos);
+ ImGui::Begin("TetrisStatisticsTitle", nullptr, s_DefaultImGuiWindowFlags);
+ ImGui::Text("STATISTICS");
+ ImGui::End();
+
+
+ ImGui::SetNextWindowPos(screen_text_pos);
+ ImGui::Begin("TetrisStatistics", nullptr, s_DefaultImGuiWindowFlags);
+
+ ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_T]);
+ ImGui::Dummy(screen_text_gap);
+ ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_J]);
+ ImGui::Dummy(screen_text_gap);
+ ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_Z]);
+ ImGui::Dummy(screen_text_gap);
+ ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_O]);
+ ImGui::Dummy(screen_text_gap);
+ ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_S]);
+ ImGui::Dummy(screen_text_gap);
+ ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_L]);
+ ImGui::Dummy(screen_text_gap);
+ ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_I]);
+ ImGui::Dummy(screen_text_gap);
+
+ ImGui::End();
+}
+
+void Tetris::DrawScore(RenderGroup &render_group) {
+ V2F32 view_pos = {3.0f, 2.2f};
+ ImVec2 screen_pos = render_group.ViewPosToScreenPosImGui(view_pos);
+
+ ImGui::SetNextWindowPos(screen_pos);
+ ImGui::Begin("TetrisScore", nullptr, s_DefaultImGuiWindowFlags);
+ ImGui::Text("Score");
+ ImGui::Text("%d", m_Score);
+ ImGui::End();
+}
+
+void Tetris::DrawNextTetromino(RenderGroup &render_group) {
+ V2F32 text_view_pos = {3.0f, 1.8f};
+ ImVec2 text_screen_pos = render_group.ViewPosToScreenPosImGui(text_view_pos);
+
+ ImGui::SetNextWindowPos(text_screen_pos);
+ ImGui::Begin("TetrisNextTetromino", nullptr, s_DefaultImGuiWindowFlags);
+ ImGui::Text("Next");
+ ImGui::End();
+
+
+ V2F32 tetromino_view_pos = {3.0, 1.4f};
+ Tetromino::Draw(m_NextTetromino.GetId(), 0, tetromino_view_pos, 0.5f, render_group);
+}
+
+void Tetris::DrawLevel(RenderGroup &render_group) {
+ V2F32 view_pos = {3.0f, 1.2f};
+ ImVec2 screen_pos = render_group.ViewPosToScreenPosImGui(view_pos);
+
+ ImGui::SetNextWindowPos(screen_pos);
+ ImGui::Begin("TetrisLevel", nullptr, s_DefaultImGuiWindowFlags);
+ ImGui::Text("Level");
+ ImGui::Text("%d", m_Level);
+ ImGui::End();
+}
+
diff --git a/src/games/tetris/Tetris.hpp b/src/games/tetris/Tetris.hpp
new file mode 100644
index 0000000..30167d8
--- /dev/null
+++ b/src/games/tetris/Tetris.hpp
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <imgui.h>
+#include <games/Game.hpp>
+#include <games/tetris/Tetromino.hpp>
+#include <games/tetris/Board.hpp>
+#include <renderer/RenderGroup.hpp>
+
+enum class TetrisRunningState {
+ Resume,
+ Pause,
+ GameOver,
+ Restart,
+ Exit
+};
+
+
+class Tetris : public Game {
+public:
+ Tetris();
+ bool Update(std::vector<SDL_Event> &events, RenderGroup& render_group) override;
+ void HandleTetrominoPlacement();
+
+private:
+ void Restart();
+ void UpdateResumeState(SDL_Event &event);
+ void UpdatePauseState(SDL_Event &event);
+
+ uint32_t GetHarddropCount(float dt);
+ void HandleGameOver();
+
+ void Draw(RenderGroup &render_group);
+ void DrawLineCounter(RenderGroup &render_group);
+ void DrawStatistics(RenderGroup &render_group);
+ void DrawScore(RenderGroup &render_group);
+ void DrawNextTetromino(RenderGroup &render_group);
+ void DrawLevel(RenderGroup &render_group);
+
+ void DrawPauseMenu(RenderGroup &render_group);
+ void DrawGameOverMenu(RenderGroup &render_group);
+
+private:
+ static constexpr ImGuiWindowFlags s_MenuImGuiWindowFlags = ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_AlwaysAutoResize;
+ static constexpr ImGuiWindowFlags s_DefaultImGuiWindowFlags = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoScrollbar;
+
+
+private:
+ TetrisRunningState m_RunningState = TetrisRunningState::Resume;
+
+ float m_DtInSecondsRemaining = 0.0f;
+ uint64_t m_MillisecondsSinceT0Last = SDL_GetTicks();
+
+ Board m_Board;
+ Tetromino m_ActiveTetromino;
+ Tetromino m_NextTetromino;
+
+ int32_t m_TetrominoCounters[(size_t)TetrominoId::TETROMINO_ID_COUNT] {};
+ int32_t m_Score = 0;
+ int32_t m_LineCounter = 0;
+ int32_t m_StartingLevel = 0;
+ int32_t m_Level = 0;
+ int32_t m_SoftdropCounter = 0;
+
+ int32_t m_HighScore = 0;
+};
+
+
diff --git a/src/games/tetris/Tetromino.cpp b/src/games/tetris/Tetromino.cpp
new file mode 100644
index 0000000..577a869
--- /dev/null
+++ b/src/games/tetris/Tetromino.cpp
@@ -0,0 +1,212 @@
+#include <games/tetris/Tetromino.hpp>
+#include <random>
+#include <stdlib.h>
+
+// layout of a left_aligned_bitmap: xxxx000000000000
+// layout of a board_bitmap is 111xxxxxxxxxx111
+static const uint16_t s_left_aligned_bitmaps[7][4][4] = {
+ {
+ // O piece
+ {0x6000, 0x6000, 0x0000, 0x0000}, // orientation 0
+ {0x6000, 0x6000, 0x0000, 0x0000}, // orientation 1
+ {0x6000, 0x6000, 0x0000, 0x0000}, // orientation 2
+ {0x6000, 0x6000, 0x0000, 0x0000}, // orientation 3
+ },
+ {
+ // S piece
+ {0x6000, 0x3000, 0x0000, 0x0000}, // orientation 0
+ {0x1000, 0x3000, 0x2000, 0x0000}, // orientation 1
+ {0x6000, 0x3000, 0x0000, 0x0000}, // orientation 2
+ {0x1000, 0x3000, 0x2000, 0x0000}, // orientation 3
+ },
+ {
+ // Z piece
+ {0x3000, 0x6000, 0x0000, 0x0000}, // orientation 0
+ {0x2000, 0x3000, 0x1000, 0x0000}, // orientation 1
+ {0x3000, 0x6000, 0x0000, 0x0000}, // orientation 2
+ {0x2000, 0x3000, 0x1000, 0x0000}, // orientation 3
+ },
+ {
+ // T piece
+ {0x2000, 0x7000, 0x0000, 0x0000}, // orientation 0
+ {0x2000, 0x6000, 0x2000, 0x0000}, // orientation 1
+ {0x0000, 0x7000, 0x2000, 0x0000}, // orientation 2
+ {0x2000, 0x3000, 0x2000, 0x0000}, // orientation 3
+ },
+ {
+ // L piece
+ {0x4000, 0x7000, 0x0000, 0x0000}, // orientation 0
+ {0x2000, 0x2000, 0x6000, 0x0000}, // orientation 1
+ {0x0000, 0x7000, 0x1000, 0x0000}, // orientation 2
+ {0x3000, 0x2000, 0x2000, 0x0000}, // orientation 3
+ },
+ {
+ // J piece
+ {0x1000, 0x7000, 0x0000, 0x0000}, // orientation 0
+ {0x6000, 0x2000, 0x2000, 0x0000}, // orientation 1
+ {0x0000, 0x7000, 0x4000, 0x0000}, // orientation 2
+ {0x2000, 0x2000, 0x3000, 0x0000}, // orientation 3
+ },
+ {
+ // I piece
+ {0x0000, 0xf000, 0x0000, 0x0000}, // orientation 0
+ {0x2000, 0x2000, 0x2000, 0x2000}, // orientation 1
+ {0x0000, 0xf000, 0x0000, 0x0000}, // orientation 2
+ {0x2000, 0x2000, 0x2000, 0x2000}, // orientation 3
+ }
+};
+
+
+TetrominoId Tetromino::GetRandomId() {
+ static std::uniform_int_distribution<int> s_Dist(0, (int)TetrominoId::TETROMINO_ID_COUNT-1);
+ static std::mt19937 s_Rng((std::random_device()()));
+ TetrominoId id = static_cast<TetrominoId>(s_Dist(s_Rng));
+ return id;
+}
+
+Tetromino::Tetromino(uint16_t *board_bitmap) :
+ m_Id(GetRandomId()),
+ m_Pos{6, 20},
+ m_Ori{0},
+ m_BoardBitmap(board_bitmap)
+{
+}
+
+Tetromino::Tetromino(Board &board) :
+ Tetromino(board.m_Bitmap)
+{
+}
+
+TetrominoId Tetromino::GetId() {
+ return m_Id;
+}
+
+BoardPos Tetromino::GetPos() {
+ return m_Pos;
+}
+
+void Tetromino::GetBitmap(uint16_t *bitmap) {
+ GetBitmap(m_Id, m_Pos, m_Ori, bitmap);
+}
+
+bool Tetromino::IsCollisionWithBoard() {
+ bool is_collision = IsCollisionWithBoard(m_Id, m_Pos, m_Ori, m_BoardBitmap);
+ return is_collision;
+}
+
+void Tetromino::MaybeRotate(TetrominoRotation rotation) {
+ int32_t rot = static_cast<int32_t>(rotation);
+ int32_t ori = (m_Ori + rot) % 4;
+ if (!IsCollisionWithBoard(m_Id, m_Pos, ori, m_BoardBitmap)) {
+ m_Ori = ori;
+ }
+}
+
+void Tetromino::MaybeMoveHorizontally(TetrominoDirection direction) {
+ BoardPos pos = m_Pos;
+ pos.x += static_cast<int32_t>(direction);
+ if (!IsCollisionWithBoard(m_Id, pos, m_Ori, m_BoardBitmap)) {
+ m_Pos.x = pos.x;
+ }
+}
+
+bool Tetromino::MaybeMoveDown() {
+ BoardPos pos = m_Pos;
+ pos.y -= 1;
+ if (!IsCollisionWithBoard(m_Id, pos, m_Ori, m_BoardBitmap)) {
+ m_Pos.y = pos.y;
+ return true;
+ }
+ return false;
+}
+
+void Tetromino::Draw(RenderGroup &render_group) const {
+ float world_width = 4.0f;
+ float world_height = 3.0f;
+ float tetromino_size_with_border = world_height / 20.0f;
+
+ float x0 = static_cast<float>(m_Pos.x - 3);
+ float y0 = static_cast<float>(m_Pos.y - 2);
+
+ V2F32 world_pos = {
+ ((world_width - tetromino_size_with_border*10) / 2.0f) + x0 * tetromino_size_with_border,
+ y0 * tetromino_size_with_border
+ };
+
+ Tetromino::Draw(m_Id, m_Ori, world_pos, 1.0f, render_group);
+}
+
+bool Tetromino::IsCollisionWithBoard(TetrominoId id, BoardPos pos, int32_t ori, uint16_t *board_bitmap) {
+ uint16_t tetromino_bitmap[16];
+ GetBitmap(id, pos, ori, tetromino_bitmap);
+
+ uint64_t tetromino_bits = *(uint64_t*)(tetromino_bitmap);
+ uint64_t board_bits = *(uint64_t*)(&board_bitmap[pos.y]);
+ bool is_collision = tetromino_bits & board_bits;
+ return is_collision;
+}
+
+void Tetromino::GetBitmap(TetrominoId id, BoardPos pos, int32_t ori, uint16_t *bitmap) {
+ size_t id_ = static_cast<size_t>(id);
+ uint64_t *src = (uint64_t*)s_left_aligned_bitmaps[id_][ori];
+ uint64_t *dest = (uint64_t*)bitmap;
+ *dest = *src >> pos.x;
+}
+
+V3F32 Tetromino::GetColor(TetrominoId id) {
+ using enum TetrominoId;
+
+ V3F32 color;
+ switch (id) {
+ case TETROMINO_I:
+ case TETROMINO_O:
+ case TETROMINO_T: {
+ color = V3F32(0.8f, 0.8f, 0.8f);
+ } break;
+
+ case TETROMINO_J:
+ case TETROMINO_S: {
+ color = V3F32(0.8f, 0.2f, 0.2f);
+ } break;
+
+ default: {
+ color = V3F32(0.2f, 0.4f, 0.2f);
+ }
+ }
+ return color;
+}
+
+void Tetromino::Draw(TetrominoId id, int32_t ori, V2F32 pos, float scale, RenderGroup &render_group) {
+ int32_t id_ = static_cast<int32_t>(id);
+
+ float world_height = 3.0f;
+ float tetromino_size_with_border = scale * world_height / 20.0f;
+ float tetromino_size = 0.8f * tetromino_size_with_border;
+ float tetromino_offset = 0.1f * tetromino_size_with_border;
+
+ uint16_t *left_aligned_bitmap = (uint16_t*)s_left_aligned_bitmaps[id_][ori];
+ for (int y = 0; y < 4; y++) {
+ for (int x = 0; x < 4; x++) {
+ if (left_aligned_bitmap[y] & (0x8000 >> x)) {
+ V2F32 local_pos = {
+ (float)x * tetromino_size_with_border + tetromino_offset,
+ (float)y * tetromino_size_with_border + tetromino_offset
+ };
+ V2F32 local_dim = {tetromino_size, tetromino_size};
+
+
+ V3F32 world_pos = {
+ pos.x + local_pos.x,
+ pos.y + local_pos.y,
+ 1.0f
+ };
+ V2F32 world_dim = local_dim;
+
+
+ V3F32 color = GetColor(id);
+ render_group.PushRectangle(world_pos, world_dim, color);
+ }
+ }
+ }
+}
+
diff --git a/src/games/tetris/Tetromino.hpp b/src/games/tetris/Tetromino.hpp
new file mode 100644
index 0000000..c3ceeff
--- /dev/null
+++ b/src/games/tetris/Tetromino.hpp
@@ -0,0 +1,59 @@
+#pragma once
+
+#include <basic/defs.hpp>
+#include <basic/math.hpp>
+#include <renderer/RenderGroup.hpp>
+#include <games/tetris/Board.hpp>
+
+enum class TetrominoId : uint8_t {
+ TETROMINO_O = 0,
+ TETROMINO_S,
+ TETROMINO_Z,
+ TETROMINO_T,
+ TETROMINO_L,
+ TETROMINO_J,
+ TETROMINO_I,
+ TETROMINO_ID_COUNT,
+ TETROMINO_ID_NONE,
+};
+enum class TetrominoRotation {Clockwise = 1, CounterClockwise = 3};
+enum class TetrominoDirection {Left = -1, Right = 1};
+
+
+class Tetromino {
+public:
+ Tetromino() = delete;
+ Tetromino(Board &board);
+ Tetromino(uint16_t *board_bitmap);
+
+ TetrominoId GetId();
+ BoardPos GetPos();
+ int32_t GetOri();
+ void GetBitmap(uint16_t *bitmap);
+ bool IsCollisionWithBoard(); // for last tetromino to check game over
+
+ bool MaybeMoveDown();
+ void MaybeMoveHorizontally(TetrominoDirection direction);
+ void MaybeRotate(TetrominoRotation rotation);
+
+ void Draw(RenderGroup &render_group) const;
+
+
+public:
+ static bool IsCollisionWithBoard(TetrominoId id, BoardPos pos, int32_t ori, uint16_t *board_bitmap);
+ static void GetBitmap(TetrominoId id, BoardPos pos, int32_t ori, uint16_t *bitmap);
+ static V3F32 GetColor(TetrominoId id);
+ static void Draw(TetrominoId id, int32_t ori, V2F32 pos, float scale, RenderGroup &render_group);
+
+
+private:
+ static TetrominoId GetRandomId();
+
+
+private:
+ TetrominoId m_Id;
+ BoardPos m_Pos;
+ int32_t m_Ori;
+ uint16_t *m_BoardBitmap;
+};
+
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..0a632fa
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,191 @@
+#include "imgui_internal.h"
+#include <basic/defs.hpp>
+#include <cstdlib>
+#include <memory>
+#include <renderer/RenderGroup.hpp>
+#include <renderer/Renderer.hpp>
+#include <games/Game.hpp>
+
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_video.h>
+#include <GL/gl.h>
+
+#include <imgui.h>
+#include <imgui_impl_opengl3.h>
+#include <imgui_impl_sdl3.h>
+
+#include <stdlib.h>
+#include <assert.h>
+#include <iostream>
+#include <array>
+
+
+Game::GameType
+do_menu(RenderGroup &render_group)
+{
+ Game::GameType type = Game::NO_GAME;
+
+ //ImGuiWindowFlags flags = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
+ ImGuiWindowFlags flags = 0;
+
+ ImGui::Begin("Game Selection", nullptr, flags);
+ if (ImGui::Button("Tetris")) {
+ type = Game::TETRIS;
+ }
+ if (ImGui::Button("Snake")) {
+ type = Game::SNAKE;
+ }
+ if (ImGui::Button("Minesweeper")) {
+ type = Game::MINESWEEPER;
+ }
+ ImGui::End();
+
+
+ V3F32 clear_color = V3F32(0.4f, 0.4f, 0.4f);
+ render_group.SetCameraSize(4.0f, 3.0f);
+ render_group.Clear(clear_color);
+
+
+ return type;
+}
+
+
+int
+main(int argc, char **argv)
+{
+ if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) {
+ std::cerr << "Failed to init SDL3: " << SDL_GetError() << '\n';
+ return EXIT_FAILURE;
+ }
+
+ const char* glsl_version = "#version 130";
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
+
+ SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
+ SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
+ SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
+
+ SDL_Window *window = SDL_CreateWindow("fsarcade", 1024, 768, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN | SDL_WINDOW_HIGH_PIXEL_DENSITY);
+ if (!window) {
+ std::cerr << "Failed to create SDL_window: " << SDL_GetError() << '\n';
+ return EXIT_FAILURE;
+ }
+
+ SDL_GLContext sdl_gl_context = SDL_GL_CreateContext(window);
+ if (!sdl_gl_context) {
+ std::cerr << "Failed to create SDL_GLContext: " << SDL_GetError() << '\n';
+ SDL_DestroyWindow(window);
+ return EXIT_FAILURE;
+ }
+
+ SDL_GL_MakeCurrent(window, sdl_gl_context);
+ SDL_GL_SetSwapInterval(1); // enable vsync
+ SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
+ SDL_ShowWindow(window);
+
+
+ IMGUI_CHECKVERSION();
+ ImGui::CreateContext();
+ ImGuiIO& io = ImGui::GetIO();
+ io.IniFilename = NULL;
+ io.LogFilename = NULL;
+ io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
+ io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
+
+ ImGui::StyleColorsDark();
+ ImGui_ImplSDL3_InitForOpenGL(window, sdl_gl_context);
+ ImGui_ImplOpenGL3_Init(glsl_version);
+
+
+ std::unique_ptr<Game> game = nullptr;
+ std::unique_ptr<Renderer> renderer = Renderer::Select(Renderer::API_OPENGL, window);
+ if (!renderer->Init()) {
+ return EXIT_FAILURE;
+ }
+
+ RenderGroup render_group;
+ SDL_GetWindowSize(window, &render_group.m_ScreenWidth, &render_group.m_ScreenHeight);
+
+
+ std::vector<SDL_Event> game_events;
+ game_events.reserve(32);
+ for (;;) {
+ ImGui_ImplOpenGL3_NewFrame();
+ ImGui_ImplSDL3_NewFrame();
+ ImGui::NewFrame();
+
+
+ size_t cur_game_events = 0;
+ size_t max_game_events = game_events.max_size();
+
+ SDL_Event event;
+ while (cur_game_events < max_game_events && SDL_PollEvent(&event)) {
+ if (event.type == SDL_EVENT_KEY_DOWN ||
+ event.type == SDL_EVENT_MOUSE_BUTTON_DOWN)
+ {
+ game_events.emplace_back(event);
+ cur_game_events++;
+ }
+ ImGui_ImplSDL3_ProcessEvent(&event);
+
+ if (event.type == SDL_EVENT_QUIT)
+ goto QUIT;
+ if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED && event.window.windowID == SDL_GetWindowID(window))
+ goto QUIT;
+ if (event.type == SDL_EVENT_WINDOW_DESTROYED && event.window.windowID == SDL_GetWindowID(window)) {
+ goto QUIT;
+ }
+ }
+
+
+ int w, h;
+ SDL_GetWindowSize(window, &w, &h);
+ render_group.m_ScreenWidth = w;
+ render_group.m_ScreenHeight = h;
+
+
+ if (game) {
+ bool keep_game_running = game->Update(game_events, render_group);
+ if (!keep_game_running) {
+ game.reset();
+ }
+ }
+ else {
+ Game::GameType type = do_menu(render_group);
+ if (type != Game::NO_GAME) {
+ game = Game::Select(type);
+ }
+ }
+ game_events.clear();
+
+
+
+ ImGuiWindowFlags flags = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
+
+ ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x - 200, 0));
+ ImGui::Begin("Performance", nullptr, flags);
+ ImGui::Text("%.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);
+ ImGui::End();
+
+
+ renderer->Draw(render_group);
+ ImGui::Render();
+ ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
+ renderer->Present();
+ render_group.Reset();
+ }
+
+QUIT:
+ ImGui_ImplOpenGL3_Shutdown();
+ ImGui_ImplSDL3_Shutdown();
+ ImGui::DestroyContext();
+
+ SDL_GL_DestroyContext(sdl_gl_context);
+ SDL_DestroyWindow(window);
+ SDL_Quit();
+ return 0;
+}
+
diff --git a/src/renderer/RenderGroup.cpp b/src/renderer/RenderGroup.cpp
new file mode 100644
index 0000000..56e7ec5
--- /dev/null
+++ b/src/renderer/RenderGroup.cpp
@@ -0,0 +1,94 @@
+#include <renderer/RenderGroup.hpp>
+#include <algorithm>
+
+RSortEntry::RSortEntry(float z, size_t entity_index)
+ : z(z), entity_index(entity_index) {
+}
+
+RenderGroup::RenderGroup() {
+ m_REntities.reserve(1024);
+ m_RSortEntries.reserve(1024);
+}
+
+void RenderGroup::Reset() {
+ m_CameraWidth = 0;
+ m_CameraHeight = 0;
+ m_REntities.clear();
+ m_RSortEntries.clear();
+ m_REntities.reserve(1024);
+ m_RSortEntries.reserve(1024);
+}
+
+void RenderGroup::SetCameraSize(float width, float height) {
+ m_CameraWidth = width;
+ m_CameraHeight = height;
+}
+
+float RenderGroup::GetScale() {
+ float screen_width = static_cast<float>(m_ScreenWidth);
+ float screen_height = static_cast<float>(m_ScreenHeight);
+ float xunits = screen_width / m_CameraWidth;
+ float yunits = screen_height / m_CameraHeight;
+ float scale = std::min(xunits, yunits);
+ return scale;
+}
+
+V2F32 RenderGroup::ViewPosToScreenPos(V2F32 view_pos) {
+ float scale = GetScale();
+ float screen_width = static_cast<float>(m_ScreenWidth);
+ float screen_height = static_cast<float>(m_ScreenHeight);
+ float viewport_width = m_CameraWidth * scale;
+ float viewport_height = m_CameraHeight * scale;
+ float viewport_x0 = (screen_width - viewport_width) / 2;
+ float viewport_y0 = (screen_height - viewport_height) / 2;
+
+ V2F32 result;
+ result.x = viewport_x0 + view_pos.x * scale;
+ result.y = screen_height - (viewport_y0 + view_pos.y * scale);
+
+ return result;
+}
+
+V2F32 RenderGroup::ViewSizeToScreenSize(V2F32 view_size) {
+ float scale = GetScale();
+
+ V2F32 result;
+ result.x = view_size.x * scale;
+ result.y = view_size.y * scale;
+
+ return result;
+}
+
+ImVec2 RenderGroup::ViewPosToScreenPosImGui(V2F32 view_pos) {
+ V2F32 screen_pos = ViewPosToScreenPos(view_pos);
+ ImVec2 result = {screen_pos.x, screen_pos.y};
+ return result;
+}
+
+ImVec2 RenderGroup::ViewSizeToScreenSizeImGui(V2F32 view_size) {
+ V2F32 screen_size = ViewSizeToScreenSize(view_size);
+ ImVec2 result = {screen_size.x, screen_size.y};
+ return result;
+}
+
+void RenderGroup::Clear(V3F32 color) {
+ m_ClearColor = color;
+}
+
+void RenderGroup::PushRectangle(V3F32 pos, V2F32 dim, V3F32 color) {
+ m_REntities.emplace_back(REntity{.rect{REntityType_Rectangle, pos, dim, color}});
+ m_RSortEntries.emplace_back(pos.z, m_REntities.size()-1);
+}
+
+void RenderGroup::PushBitmap(V3F32 pos, int w, int h, void *data) {
+ m_REntities.emplace_back(REntity{.bitmap{REntityType_Bitmap, pos, w, h, data}});
+ m_RSortEntries.emplace_back(pos.z, m_REntities.size()-1);
+}
+
+void RenderGroup::Sort() {
+ std::sort(m_RSortEntries.begin(), m_RSortEntries.end(),
+ [](const RSortEntry& e1, const RSortEntry& e2) {
+ return e1.z < e2.z;
+ });
+}
+
diff --git a/src/renderer/RenderGroup.hpp b/src/renderer/RenderGroup.hpp
new file mode 100644
index 0000000..4904220
--- /dev/null
+++ b/src/renderer/RenderGroup.hpp
@@ -0,0 +1,83 @@
+#pragma once
+
+#include <basic/defs.hpp>
+#include <basic/math.hpp>
+#include <vector>
+#include <imgui.h>
+
+enum REntityType : int32_t {
+ REntityType_Rectangle,
+ REntityType_Bitmap,
+};
+
+struct REntity_Rectangle {
+ REntityType type;
+ V3F32 pos;
+ V2F32 dim;
+ V3F32 color;
+};
+
+struct REntity_Bitmap {
+ REntityType type;
+ V3F32 pos;
+ int32_t width;
+ int32_t height;
+ void *data;
+};
+
+union REntity {
+ REntityType type;
+ REntity_Rectangle rect;
+ REntity_Bitmap bitmap;
+};
+
+struct RSortEntry {
+ RSortEntry(float z, size_t entity_index);
+ float z;
+ size_t entity_index;
+};
+
+
+class RenderGroup {
+public:
+ RenderGroup();
+ void Clear(V3F32 color);
+ void Reset();
+
+ void SetCameraSize(float width, float height);
+ V2F32 ViewPosToScreenPos(V2F32 view_pos);
+ V2F32 ViewSizeToScreenSize(V2F32 view_size);
+ ImVec2 ViewPosToScreenPosImGui(V2F32 view_pos);
+ ImVec2 ViewSizeToScreenSizeImGui(V2F32 view_size);
+ float GetScale();
+
+
+public:
+ void PushRectangle(V3F32 pos, V2F32 dim, V3F32 color);
+ void PushBitmap(V3F32 pos, int width, int height, void *bitmap);
+
+
+private:
+ void Sort();
+
+
+public:
+ int32_t m_ScreenWidth;
+ int32_t m_ScreenHeight;
+
+
+private:
+ friend class GlRenderer;
+
+
+ float m_CameraWidth;
+ float m_CameraHeight;
+
+
+ V3F32 m_ClearColor;
+
+ std::vector<REntity> m_REntities;
+ std::vector<RSortEntry> m_RSortEntries;
+};
+
+
diff --git a/src/renderer/Renderer.cpp b/src/renderer/Renderer.cpp
new file mode 100644
index 0000000..13f5feb
--- /dev/null
+++ b/src/renderer/Renderer.cpp
@@ -0,0 +1,18 @@
+#include <renderer/Renderer.hpp>
+#include <renderer/opengl/GlRenderer.hpp>
+
+
+std::unique_ptr<Renderer>
+Renderer::Select(Api api, SDL_Window *window) {
+ switch (api) {
+ case API_OPENGL: {
+ return std::make_unique<GlRenderer>(window);
+ }
+ InvalidDefaultCase;
+ }
+
+ return nullptr;
+}
+
+
+Renderer::~Renderer() {}
diff --git a/src/renderer/Renderer.hpp b/src/renderer/Renderer.hpp
new file mode 100644
index 0000000..a3e0f98
--- /dev/null
+++ b/src/renderer/Renderer.hpp
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <basic/math.hpp>
+#include <renderer/RenderGroup.hpp>
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_video.h>
+
+#include <memory>
+
+class Renderer {
+public:
+ enum Api {
+ API_OPENGL
+ };
+
+ Renderer() = default;
+ static std::unique_ptr<Renderer> Select(Api api, SDL_Window *window);
+
+
+public:
+ virtual ~Renderer() = 0;
+ virtual bool Init() = 0;
+ virtual void Draw(RenderGroup &render_group) = 0;
+ virtual void Present() = 0;
+};
+
diff --git a/src/renderer/opengl/GlIndexBuffer.cpp b/src/renderer/opengl/GlIndexBuffer.cpp
new file mode 100644
index 0000000..c5bd713
--- /dev/null
+++ b/src/renderer/opengl/GlIndexBuffer.cpp
@@ -0,0 +1,44 @@
+#include <renderer/opengl/GlIndexBuffer.hpp>
+
+#include <GL/glew.h>
+#include <string.h>
+#include <stdio.h>
+#include <assert.h>
+
+void GlIndexBuffer::Init() {
+ m_CurrentIndex = 0;
+ m_Indices.reserve(16384);
+ glGenBuffers(1, &m_Id);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_Id);
+}
+
+uint32_t *GlIndexBuffer::GetData() {
+ return m_Indices.data();
+}
+
+uint32_t GlIndexBuffer::GetCount() {
+ uint32_t count = static_cast<uint32_t>(m_Indices.size());
+ return count;
+}
+
+void GlIndexBuffer::Reset() {
+ m_CurrentIndex = 0;
+ m_Indices.clear();
+}
+
+void GlIndexBuffer::PushRectangle() {
+ uint32_t current_index = m_CurrentIndex;
+ m_Indices.push_back(current_index + 0);
+ m_Indices.push_back(current_index + 1);
+ m_Indices.push_back(current_index + 2);
+ m_Indices.push_back(current_index + 0);
+ m_Indices.push_back(current_index + 2);
+ m_Indices.push_back(current_index + 3);
+ m_CurrentIndex += 4;
+}
+
+void GlIndexBuffer::TransferData() {
+ size_t size = m_Indices.size() * sizeof(uint32_t);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, m_Indices.data(), GL_STATIC_DRAW);
+}
+
diff --git a/src/renderer/opengl/GlIndexBuffer.hpp b/src/renderer/opengl/GlIndexBuffer.hpp
new file mode 100644
index 0000000..7805460
--- /dev/null
+++ b/src/renderer/opengl/GlIndexBuffer.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <renderer/Renderer.hpp>
+
+class GlIndexBuffer {
+public:
+ GlIndexBuffer() = default;
+ void Init();
+ void Reset();
+
+public:
+ uint32_t GetCount();
+ uint32_t *GetData();
+
+ void PushRectangle();
+ void TransferData();
+
+private:
+ uint32_t m_Id;
+ uint32_t m_CurrentIndex;
+ std::vector<uint32_t> m_Indices;
+};
+
diff --git a/src/renderer/opengl/GlRenderer.cpp b/src/renderer/opengl/GlRenderer.cpp
new file mode 100644
index 0000000..5d7c985
--- /dev/null
+++ b/src/renderer/opengl/GlRenderer.cpp
@@ -0,0 +1,220 @@
+#include <basic/defs.hpp>
+#include <iterator>
+#include <renderer/RenderGroup.hpp>
+#include <renderer/opengl/GlRenderer.hpp>
+#include <basic/math.hpp>
+#include <renderer/Renderer.hpp>
+#include <renderer/opengl/GlVertexBuffer.hpp>
+
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_video.h>
+#include <GL/glew.h>
+#include <GL/gl.h>
+#include <assert.h>
+#include <cstdio>
+
+void GLAPIENTRY
+MessageCallback(GLenum source,
+ GLenum type,
+ GLuint id,
+ GLenum severity,
+ GLsizei length,
+ const GLchar* message,
+ const void* userParam)
+{
+ fprintf(stderr, "GL CALLBACK: %s, type = 0x%x, severity = 0x%x, message = %s\n",
+ type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "",
+ type,
+ severity,
+ message
+ );
+}
+
+GlRenderer::~GlRenderer() {
+ SDL_GL_DestroyContext(m_Context);
+}
+
+GlRenderer::GlRenderer(SDL_Window *window)
+ : m_Window(window), m_Context(nullptr) {
+}
+
+bool GlRenderer::Init() {
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
+ SDL_GLContext sdl_gl_context = SDL_GL_CreateContext(m_Window);
+ if (!sdl_gl_context) {
+ return false;
+ }
+
+ GLenum err = glewInit();
+ if (GLEW_OK != err)
+ {
+ fprintf(stderr, "Error: %s\n", glewGetErrorString(err));
+ SDL_GL_DestroyContext(sdl_gl_context);
+ return false;
+ }
+
+#if 0
+ glEnable(GL_DEBUG_OUTPUT);
+ glDebugMessageCallback(MessageCallback, 0);
+#endif
+
+ GLuint vertex_array;
+ glGenVertexArrays(1, &vertex_array);
+ glBindVertexArray(vertex_array);
+
+
+ // init rectangle rendering
+ if (!m_RectangleShader.InitProgram()) {
+ return false;
+ }
+ m_RectangleVertexBuffer.Init();
+ m_RectangleIndexBuffer.Init();
+
+
+ // init text rendering
+ if (!m_TextShader.InitProgram()) {
+ return false;
+ }
+ m_TextVertexBuffer.Init();
+ m_TextIndexBuffer.Init();
+
+
+ m_Context = sdl_gl_context;
+ return true;
+}
+
+void GlRenderer::Draw(RenderGroup& render_group) {
+ render_group.Sort();
+
+ float camera_width = render_group.m_CameraWidth;
+ float camera_height = render_group.m_CameraHeight;
+
+
+ glViewport(0, 0, render_group.m_ScreenWidth, render_group.m_ScreenHeight);
+ glClearColor(render_group.m_ClearColor.x,
+ render_group.m_ClearColor.y,
+ render_group.m_ClearColor.z,
+ 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+
+ // viewport space
+ float scale = render_group.GetScale();
+ int32_t viewport_width = static_cast<int32_t>(camera_width * scale);
+ int32_t viewport_height = static_cast<int32_t>(camera_height * scale);
+ int32_t viewport_x0 = (render_group.m_ScreenWidth - viewport_width) / 2;
+ int32_t viewport_y0 = (render_group.m_ScreenHeight - viewport_height) / 2;
+ glViewport(viewport_x0, viewport_y0, viewport_width, viewport_height);
+
+
+ // draw batches
+ float last_z = -1;
+ for (auto [z, entity_index] : render_group.m_RSortEntries) {
+ REntity& render_entity = render_group.m_REntities[entity_index];
+ switch (render_entity.type) {
+ case REntityType_Rectangle: {
+ // clip space (from camera space to [-1, 1] for pos and [0, 2] for dim)
+ V3F32 pos = render_entity.rect.pos;
+ V2F32 dim = render_entity.rect.dim;
+ pos.x = 2*(pos.x / camera_width) - 1;
+ pos.y = 2*(pos.y / camera_height) - 1;
+ dim.x = 2*(dim.x / camera_width);
+ dim.y = 2*(dim.y / camera_height);
+
+ if (render_entity.rect.pos.z > last_z) {
+ DrawBatch();
+ last_z = z;
+ }
+
+ m_RectangleVertexBuffer.PushRectangle(pos, dim, render_entity.rect.color);
+ m_RectangleIndexBuffer.PushRectangle();
+ }
+
+ case REntityType_Bitmap: {
+ REntity_Bitmap& bitmap = render_entity.bitmap;
+
+ // clip space (from camera space to [-1, 1] for pos and [0, 2] for dim)
+ V3F32 pos = render_entity.bitmap.pos;
+ pos.x = 2*(pos.x / camera_width) - 1;
+ pos.y = 2*(pos.y / camera_height) - 1;
+
+#if 0
+
+ // create setup texture
+ unsigned int texture_id;
+ glGenTextures(1, &texture_id);
+ glBindTexture(GL_TEXTURE_2D, texture_id);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, bitmap.width, bitmap.height, 0, GL_RGB, GL_UNSIGNED_BYTE, bitmap.data);
+
+ glGenerateMipmap(GL_TEXTURE_2D);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+
+ // apply texture
+ glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
+ glEnableVertexAttribArray(2);
+
+ // delete texture
+ glDeleteTextures(1, &texture_id);
+
+
+
+ // 1) set vertex buffer
+ // 2) set texture coordinates
+ pos.x = 2*(pos.x / camera_width) - 1;
+ pos.y = 2*(pos.y / camera_height) - 1;
+ dim.x = 2*(dim.x / camera_width);
+ dim.y = 2*(dim.y / camera_height);
+ if (render_entity.rect.pos.z > last_z) {
+ DrawBatch();
+ last_z = z;
+ }
+#endif
+
+
+ } break;
+
+ InvalidDefaultCase;
+ }
+ }
+
+ DrawBatch();
+}
+
+void GlRenderer::Present() {
+ SDL_GL_SwapWindow(m_Window);
+}
+
+void GlRenderer::DrawBatch() {
+ int32_t rectangle_index_count = static_cast<int32_t>(m_RectangleIndexBuffer.GetCount());
+ int32_t text_index_count = static_cast<int32_t>(m_TextIndexBuffer.GetCount());
+
+ if (rectangle_index_count) {
+ m_RectangleVertexBuffer.TransferData();
+ m_RectangleIndexBuffer.TransferData();
+ m_RectangleShader.UseProgram();
+
+ glDrawElements(GL_TRIANGLES, rectangle_index_count, GL_UNSIGNED_INT, 0);
+
+ m_RectangleVertexBuffer.Reset();
+ m_RectangleIndexBuffer.Reset();
+ }
+
+ if (text_index_count) {
+ m_TextVertexBuffer.TransferData();
+ m_TextIndexBuffer.TransferData();
+ m_TextShader.UseProgram();
+
+ glDrawElements(GL_TRIANGLES, text_index_count, GL_UNSIGNED_INT, 0);
+
+ m_TextVertexBuffer.Reset();
+ m_TextIndexBuffer.Reset();
+ }
+}
+
+
diff --git a/src/renderer/opengl/GlRenderer.hpp b/src/renderer/opengl/GlRenderer.hpp
new file mode 100644
index 0000000..d59ba45
--- /dev/null
+++ b/src/renderer/opengl/GlRenderer.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <renderer/Renderer.hpp>
+#include <renderer/opengl/GlVertexBuffer.hpp>
+#include <renderer/opengl/GlIndexBuffer.hpp>
+#include <renderer/opengl/GlShader.hpp>
+
+class GlRenderer : public Renderer {
+public:
+ GlRenderer(SDL_Window *window);
+ ~GlRenderer() override;
+
+ bool Init() override;
+ void Draw(RenderGroup& render_group) override;
+ void Present() override;
+
+ void InitTexture();
+
+private:
+ void DrawBatch();
+
+private:
+ SDL_Window *m_Window;
+ SDL_GLContext m_Context;
+
+ GlVertexBuffer m_RectangleVertexBuffer;
+ GlIndexBuffer m_RectangleIndexBuffer;
+ GlShader m_RectangleShader;
+
+ GlVertexBuffer m_TextVertexBuffer;
+ GlVertexBuffer m_TextIndexBuffer;
+ GlShader m_TextShader;
+};
+
diff --git a/src/renderer/opengl/GlShader.cpp b/src/renderer/opengl/GlShader.cpp
new file mode 100644
index 0000000..cf7c6c5
--- /dev/null
+++ b/src/renderer/opengl/GlShader.cpp
@@ -0,0 +1,80 @@
+#include <renderer/opengl/GlShader.hpp>
+#include <GL/glew.h>
+#include <stdio.h>
+
+static const char *s_vertex_shader_src =
+"#version 330 core\n"
+"layout (location = 0) in vec3 aPos;\n"
+"layout (location = 1) in vec3 aColor;\n"
+"\n"
+"out vec4 vertexColor;\n"
+"\n"
+"void main()\n"
+"{\n"
+" gl_Position = vec4(aPos.xy, 0.0, 1.0);\n"
+" vertexColor = vec4(aColor, 1.0);\n"
+"}\n"
+;
+
+static const char *s_fragment_shader_src =
+"#version 330 core\n"
+"in vec4 vertexColor;\n"
+"\n"
+"out vec4 FragColor;\n"
+"\n"
+"void main()\n"
+"{\n"
+" FragColor = vertexColor;\n"
+"}\n"
+;
+
+bool GlShader::InitProgram() {
+ GLuint vertex_shader;
+ GLuint fragment_shader;
+ if (!CompileShader(&vertex_shader, s_vertex_shader_src, GL_VERTEX_SHADER) ||
+ !CompileShader(&fragment_shader, s_fragment_shader_src, GL_FRAGMENT_SHADER)) {
+ return false;
+ }
+
+ GLuint program = glCreateProgram();
+ glAttachShader(program, vertex_shader);
+ glAttachShader(program, fragment_shader);
+ glLinkProgram(program);
+
+ glDeleteShader(vertex_shader);
+ glDeleteShader(fragment_shader);
+
+ int success;
+ char info_log[512];
+ glGetProgramiv(program, GL_LINK_STATUS, &success);
+ if (!success) {
+ printf("error linking shader program\n%s\n", info_log);
+ return false;
+ }
+
+ m_ProgramId = program;
+ return true;
+}
+
+void GlShader::UseProgram() {
+ glUseProgram(m_ProgramId);
+}
+
+bool GlShader::CompileShader(GLuint *id, const char *src, GLenum type) {
+ GLuint shader = glCreateShader(type);
+ glShaderSource(shader, 1, &src, 0);
+ glCompileShader(shader);
+
+ int success;
+ char info_log[512];
+ glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
+ if(!success) {
+ glGetShaderInfoLog(shader, 512, 0, info_log);
+ printf("error %s shader compilation failed\n%s\n", type == GL_VERTEX_SHADER ? "vertex" : "fragment", info_log);
+ glDeleteShader(shader);
+ return false;
+ }
+
+ *id = shader;
+ return true;
+}
diff --git a/src/renderer/opengl/GlShader.hpp b/src/renderer/opengl/GlShader.hpp
new file mode 100644
index 0000000..d195a8a
--- /dev/null
+++ b/src/renderer/opengl/GlShader.hpp
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <GL/glew.h>
+
+class GlShader {
+public:
+ GlShader() = default;
+ bool InitProgram();
+ void UseProgram();
+private:
+ bool CompileShader(GLuint *id, const char *src, GLenum type);
+
+private:
+ GLuint m_ProgramId;
+};
diff --git a/src/renderer/opengl/GlVertexBuffer.cpp b/src/renderer/opengl/GlVertexBuffer.cpp
new file mode 100644
index 0000000..f716c50
--- /dev/null
+++ b/src/renderer/opengl/GlVertexBuffer.cpp
@@ -0,0 +1,54 @@
+#include <renderer/opengl/GlVertexBuffer.hpp>
+
+#include <GL/glew.h>
+#include <string.h>
+
+#include <stdio.h>
+
+void GlVertexBuffer::Init() {
+ m_Vertices.reserve(16384);
+ glGenBuffers(1, &m_Id);
+ glBindBuffer(GL_ARRAY_BUFFER, m_Id);
+}
+
+void GlVertexBuffer::Reset() {
+ glDisableVertexAttribArray(0);
+ glDisableVertexAttribArray(1);
+ m_Vertices.clear();
+}
+
+float *GlVertexBuffer::GetData() {
+ return reinterpret_cast<float*>(m_Vertices.data());
+}
+
+uint32_t GlVertexBuffer::GetCount() {
+ return static_cast<uint32_t>(m_Vertices.size());
+}
+
+void GlVertexBuffer::PushRectangle(V3F32 pos, V2F32 dim, V3F32 color) {
+ V3F32 positions[4] = {
+ V3F32(pos.x, pos.y, pos.z),
+ V3F32(pos.x + dim.x, pos.y, pos.z),
+ V3F32(pos.x + dim.x, pos.y + dim.y, pos.z),
+ V3F32(pos.x, pos.y + dim.y, pos.z),
+ };
+
+ for (int i = 0; i < 4; i++) {
+ GlVertex vertex = {};
+ vertex.pos = positions[i];
+ vertex.color = color;
+ m_Vertices.push_back(vertex);
+ }
+}
+
+void GlVertexBuffer::TransferData() {
+ size_t size = m_Vertices.size() * sizeof(GlVertex);
+ GLsizei stride = sizeof(GlVertex);
+ const void *offset_color = (const void*)(3*sizeof(float));
+ glEnableVertexAttribArray(0);
+ glEnableVertexAttribArray(1);
+ glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, 0);
+ glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, offset_color);
+ glBufferData(GL_ARRAY_BUFFER, size, m_Vertices.data(), GL_STATIC_DRAW);
+}
+
diff --git a/src/renderer/opengl/GlVertexBuffer.hpp b/src/renderer/opengl/GlVertexBuffer.hpp
new file mode 100644
index 0000000..4099ca2
--- /dev/null
+++ b/src/renderer/opengl/GlVertexBuffer.hpp
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <renderer/Renderer.hpp>
+
+struct GlVertex {
+ V3F32 pos;
+ V3F32 color;
+};
+
+class GlVertexBuffer {
+public:
+ GlVertexBuffer() = default;
+ void Init();
+ void Reset();
+
+public:
+ float *GetData();
+ uint32_t GetCount();
+
+ void PushRectangle(V3F32 pos, V2F32 dim, V3F32 color);
+ void TransferData();
+
+private:
+ uint32_t m_Id;
+ std::vector<GlVertex> m_Vertices;
+};
+