Hi again, it’s been some time last I wrote a tutorial. I know I wanted to do more about my blog and open-source projects but what can I say, life sometimes overcomes those goals.

Anyways, let’s try to focus on today’s topic. Developing a simple platformer game using solely C++. Don’t get me wrong though we will not reinvent the wheel (although it might look like) we will develop a platformer using terminal.

I’m gearing up to kick off my streaks next week (07.07.2025)! Until then, I’m warming up by crafting some fresh posts. Let’s keep the momentum going, right?


Requirements

As always, somewhat mediocre understanding of C++ would suffice for our purposes. I would suggest you learning more about SDL if you wanted to do some GUI apps/games. Maybe I’ll touch that field as well, who knows?

Some requirements;

  • A modern C++ compiler (g++ recommended)
  • An IDE/text editor
  • A computer (optional, if not you’ll need a buffer output. JK use a computer.)
  • make (or don’t)

Game Idea

I was thinking of creating a clone of well known Google’s T-Rex Chrome Dino Game. Mildly creative, am I right?

There’ll be three states;

  1. Idle (waiting for user input to start the game)
  2. Game Loop (the actual gameplay)
  3. Dead screen & high-score (when the player loses)

We can calculate the high-score easily by counting the frames we updated/rendered as the length of the path itself (multiplied with dino’s length) – simple and effective!


Part I. Setup

So as you may noticed, there’s no need to develop a terminal based application since Apple Lisa (correct me if I’m wrong please.) and yet here we are, I would like to ask you, why the heck not?

Jokes aside, learning from scratch is important even though you know everything, since it will help you grasp how does the cogs turn behind the scenes.

Let’s begin by creating our project.

mkdir -p Platui/src && cd Platui && touch src/platui.cpp src/game.cpp src/input.cpp src/player.cpp src/map.hpp src/game.hpp src/player.hpp src/input.hpp src/platui.hpp readme.md .gitignore

This command will generate the overall look of the project itself. Now open your preferred IDE (in my case nvim, insert i use arch btw joke here). It doesn’t matter which tools you use, so if you want to go with vscode, go for it.

Handling User Input

So, this is an important aspect of developing a game. We want user/s to be able to interact with the game to create the illusion of fun! YAAAY!

If we were using opengl or any other graphics library, they would’ve included this logic just for us, but as our laziness requires (as a collective of developers) we won’t be installing any libraries other than stl.

That being aside, we will use a thing called raw mode to grab the keystrokes immediately. This will make sure users won’t need to press enter every freaking loop. This is crucial for a responsive game; you don’t want your dino to wait for an Enter press to jump!

More on raw mode here.

Here’s where things get a little C++-unfriendly across different operating systems. There isn’t a single, standard C++ way to put the terminal into raw mode. So we need to create a unified header/library (whatever…) to make our app compatible with your Apple II. (Joking, it won’t work. Probably?)

Okay, enough talk. Here’s the code (with explanations aside). We’re encapsulating all this platform-specific magic into two files: input.hpp and input.cpp.

// src/input.hpp

#ifndef INPUT_HPP
#define INPUT_HPP

// Include platform-specific headers
#ifdef _WIN32
    #include <conio.h> // For _kbhit() and _getch()
#else
    #include <termios.h> // For termios functions
    #include <unistd.h>  // For STDIN_FILENO, read()
    #include <sys/time.h> // For timeval struct in select()
    #include <sys/types.h> // For fd_set in select()
#endif

// Function to enable raw (non-canonical) terminal mode
void enableRawMode();

// Function to disable raw mode and restore original terminal settings
void disableRawMode();

// Function to get a character from input without blocking, returns 0 if no key pressed
char getch_non_blocking();

#endif // INPUT_HPP

And here’s the input’s implementation.

// src/input.cpp

#include "input.hpp" // Include our custom input header

#ifndef _WIN32
// Global variable to store original terminal settings for Unix-like systems
static struct termios original_termios;
#endif

void enableRawMode() {
#ifdef _WIN32
    // Windows: No special "raw mode" setup needed globally like termios.
    // _getch() and _kbhit() directly provide non-canonical, no-echo input.
    // You might want to disable cursor visibility or other console features here if needed.
#else
    // Unix-like (Linux, macOS): Using termios
    tcgetattr(STDIN_FILENO, &original_termios); // Get current terminal attributes
    struct termios raw = original_termios;      // Make a copy to modify

    // Disable canonical mode (ICANON): input is read byte-by-byte, not line-by-line
    // Disable echoing (ECHO): input characters are not printed to the terminal
    raw.c_lflag &= ~(ICANON | ECHO);

    // Apply the new settings immediately (TCSAFLUSH)
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
#endif
}

void disableRawMode() {
#ifdef _WIN32
    // Windows: Nothing to restore for input mode as it wasn't globally changed.
#else
    // Unix-like: Restore original terminal attributes
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios);
#endif
}

char getch_non_blocking() {
#ifdef _WIN32
    if (_kbhit()) {
        return _getch(); // Returns the character pressed
    }
    return 0; // No key pressed
#else
    // Unix-like: Using select() for non-blocking read
    char buf = 0;
    struct timeval tv; // Timeout structure
    fd_set fds;        // File descriptor set

    tv.tv_sec = 0;     // No timeout seconds
    tv.tv_usec = 0;    // No timeout microseconds (makes it non-blocking)

    FD_ZERO(&fds);             // Clear the set
    FD_SET(STDIN_FILENO, &fds); // Add standard input to the set

    // Check if input is available without blocking
    // select() returns the number of ready file descriptors, 0 for timeout, -1 for error
    if (select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv) > 0) {
        read(STDIN_FILENO, &buf, 1); // Read one byte
    }
    return buf; // Returns the character or 0 if no key was pressed
#endif
}

It may appear as confusing, I’ll write another post about entering raw mode within the future to fill those gaps but its getting late and we are just getting started. For now, just remember: enableRawMode() at the start, disableRawMode() at the end, and getch_non_blocking() to read keys.

Now, let’s talk a bit more about game loop.


Part II. Game Loop

The game loop is the beating heart of any game. It’s a continuous cycle that processes player input, updates the game world, and renders everything to the screen. For our T-Rex clone, we’ll encapsulate this logic within a Game class, which will manage the different states of our game: IdlePlaying, and GameOver.

src/game.hpp – The Game States and Class Definition

This file defines our GameMode enum and the Game class, which will orchestrate our entire game.

// src/game.hpp

#ifndef GAME_HPP
#define GAME_HPP

// Forward declarations to avoid circular includes if Player or Map need Game
class Player; // We'll define Player in player.hpp
// Assume Map is a char[][] in map.hpp, so no need for a class forward-decl for now.

// Enum to define the different states of our game
enum class GameMode {
    Idle,       // Waiting for player to start
    Playing,    // The main game loop is active
    GameOver    // Player has died, display score, wait for restart/quit
};

class Game {
public:
    Game(); // Constructor

    // Main function to run the game
    void run();

private:
    GameMode currentMode; // Tracks the current state of the game
    bool isRunning;       // Flag to control the main game loop (overall program execution)
    long long score;      // Player's score (frames passed)

    // Private helper functions for each game mode/phase
    void initialize();    // Set up map, player, initial state
    void handleInput();   // Process user input based on current mode
    void update();        // Update game logic (physics, score) based on current mode
    void render();        // Draw the game state to the console

    // Specific mode handlers
    void handleIdleInput(); // Not directly used in current draft, but good for structure
    void updatePlaying();
    void renderPlaying();
    void renderGameOver();
};

#endif // GAME_HPP

src/game.cpp – The Game Loop Implementation

This is where the magic happens! We’ll implement the Game class methods, linking our input system, managing states, and setting up the core loop.

// src/game.cpp

#include "game.hpp"
#include "input.hpp" // For enableRawMode, disableRawMode, getch_non_blocking
#include "player.hpp" // For Player struct/class (we'll implement this soon)
#include "map.hpp"    // For Map data and functions (we'll implement this soon)

#include <iostream>
#include <chrono> // For time-based frame rate limiting
#include <thread> // For std::this_thread::sleep_for
#include <string> // For std::string

// --- External Dependencies (from player.hpp and map.hpp, to be implemented) ---
// These will be globally accessible, or passed into Game, Player, Map objects
extern Player player; // Assumes player is a global or accessible instance
extern char gameMap[MAP_HEIGHT][MAP_WIDTH]; // Assumes gameMap is global or accessible

// Functions that will be defined in player.cpp and map.cpp
void initializePlayer();
void applyGravity();       // From player.cpp (or game.cpp)
void handlePlayerCollision(); // From player.cpp (or game.cpp)
void initializeMap();      // From map.cpp
void renderMapAndPlayer(); // From map.cpp or a rendering utility

// --- Constants ---
const int GAME_TICK_MS = 50; // Milliseconds per game frame (approx. 20 FPS)
                             // Adjust for desired game speed.

// --- Game Class Implementation ---

Game::Game() : currentMode(GameMode::Idle), isRunning(true), score(0) {
    // Constructor: basic initialization of game state variables.
}

void Game::run() {
    // Enable raw mode for immediate input (Unix-like only, Windows handled by _getch)
#ifndef _WIN32
    enableRawMode();
#endif

    initialize(); // Initial setup before the main loop starts

    auto lastTickTime = std::chrono::high_resolution_clock::now();

    while (isRunning) {
        handleInput();
        update();
        render();

        // Frame rate limiting: Ensures the game doesn't run too fast,
        // making it playable and consuming less CPU.
        auto currentTime = std::chrono::high_resolution_clock::now();
        auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastTickTime);

        if (elapsedTime.count() < GAME_TICK_MS) {
            std::this_thread::sleep_for(std::chrono::milliseconds(GAME_TICK_MS) - elapsedTime);
        }
        lastTickTime = std::chrono::high_resolution_clock::now();
    }

    // Disable raw mode before exiting (Unix-like only). CRITICAL!
#ifndef _WIN32
    disableRawMode();
#endif
    std::cout << "Thanks for playing! Final Score: " << score << std::endl;
}

void Game::initialize() {
    initializeMap();    // Reset the map to its initial state
    initializePlayer(); // Place the player at the starting position
    score = 0;
    currentMode = GameMode::Idle; // Start in Idle mode, waiting for player input
}

void Game::handleInput() {
    char key = getch_non_blocking(); // Get input without blocking the game loop

    switch (currentMode) {
        case GameMode::Idle:
            // In Idle mode, we only care about starting the game or quitting.
            if (key == ' ' || key == 's' || key == 'S') { // Spacebar or 's' to start
                currentMode = GameMode::Playing;
            } else if (key == 'q' || key == 'Q') { // 'q' to quit the program
                isRunning = false;
            }
            break;

        case GameMode::Playing:
            // In Playing mode, handle player movement and actions.
            // These will eventually call functions in player.cpp.
            if (key == 'a' || key == 'A') {
                player.x--; // Move left (simple direct modification for now)
            } else if (key == 'd' || key == 'D') {
                player.x++; // Move right (simple direct modification for now)
            } else if (key == ' ' && player.onGround) { // Spacebar to jump, only if on ground
                player.velocityY = -1.8; // Give an upward velocity
                player.onGround = false;
            } else if (key == 'q' || key == 'Q') {
                isRunning = false; // Quit during gameplay
            }
            break;

        case GameMode::GameOver:
            // In Game Over mode, allow restarting or quitting.
            if (key == 'r' || key == 'R') { // 'r' to restart the game
                initialize(); // Reset everything
            } else if (key == 'q' || key == 'Q') { // 'q' to quit the program
                isRunning = false;
            }
            break;
    }
    // Ensure player's X position stays within map boundaries after input
    if (player.x < 0) player.x = 0;
    if (player.x >= MAP_WIDTH) player.x = MAP_WIDTH - 1;
}

void Game::update() {
    // Game logic updates are dependent on the current game mode.
    switch (currentMode) {
        case GameMode::Idle:
            // Nothing to update; game is paused, waiting for input.
            break;
        case GameMode::Playing:
            updatePlaying(); // Call the specific update logic for gameplay.
            break;
        case GameMode::GameOver:
            // Nothing to update; game is waiting for restart/quit.
            break;
    }
}

void Game::updatePlaying() {
    // --- Player Physics & Movement ---
    applyGravity();           // Apply gravity to player's vertical velocity.
    handlePlayerCollision();  // Check for collisions and resolve them (e.g., landing on ground).

    // --- Game Logic (e.g., world scrolling for Dino game) ---
    // For a true T-Rex clone, the world would scroll left while the player's X is relatively fixed.
    // This is a more advanced topic for a later stage, involving map offsets and obstacle generation.
    // For now, our player moves left/right on a static map, and obstacles will appear statically.

    // --- Score Update ---
    score++; // Increment score every frame the game is playing.

    // --- Check for Game Over Conditions ---
    // If the player falls off the bottom of the map, it's game over!
    // More complex conditions like hitting obstacles will come later.
    if (player.y >= MAP_HEIGHT) { // Player fell below the visible map area
        currentMode = GameMode::GameOver;
    }
}

void Game::render() {
    // Clear the console screen before drawing the new frame.
#ifdef _WIN32
    system("cls");
#else
    system("clear"); // Works on most Unix-like systems
#endif

    // Render content based on the current game mode.
    switch (currentMode) {
        case GameMode::Idle:
            // Simple text for the start screen.
            std::cout << "\n\n";
            std::cout << "        --- Platui! ---" << std::endl;
            std::cout << "        Press SPACE or 'S' to start!" << std::endl;
            std::cout << "        Press 'Q' to quit." << std::endl;
            std::cout << "\n\n";
            break;
        case GameMode::Playing:
            renderPlaying(); // Delegate to the gameplay rendering function.
            break;
        case GameMode::GameOver:
            renderGameOver(); // Delegate to the game over screen rendering function.
            break;
    }
    // Flush cout to ensure immediate output.
    std::cout.flush();
}

void Game::renderPlaying() {
    // This function will draw the map content and then overlay the player character.
    // We'll rely on a utility function, likely from map.cpp or a general render file.
    renderMapAndPlayer(); // Will iterate through the map and draw 'P' at player's location.
    std::cout << "Score: " << score << std::endl;
}

void Game::renderGameOver() {
    // Display the game over message and final score.
    std::cout << "\n\n";
    std::cout << "        --- GAME OVER ---" << std::endl;
    std::cout << "        Final Score: " << score << std::endl;
    std::cout << "        Press 'R' to restart, 'Q' to quit." << std::endl;
    std::cout << "\n\n";
}

Part III. Player and Map – The Game World

Now that we have our game loop, input, and states, let’s define the fundamental entities of our game: the player and the game map.

Player (src/player.hpp and src/player.cpp)

The player needs a position, vertical velocity for jumping and gravity, and a flag to know if they’re on the ground.

// src/player.hpp

#ifndef PLAYER_HPP
#define PLAYER_HPP

// Define map dimensions to help with player constraints
const int MAP_WIDTH = 40;
const int MAP_HEIGHT = 20;

struct Player {
    int x;
    int y;
    double velocityY; // For vertical movement (gravity and jump)
    bool onGround;    // True if player is currently on a platform/ground
};

// Global player instance (simplified for this tutorial)
extern Player player;

// Function declarations for player-related actions
void initializePlayer();
void applyGravity();
void handlePlayerCollision(); // Checks and resolves collisions with map elements

#endif // PLAYER_HPP
// src/player.cpp

#include "player.hpp"
#include "map.hpp" // For accessing gameMap and MAP_HEIGHT/WIDTH

// Global player instance definition
Player player;

// Game constants (could also be in game.hpp or a constants.hpp)
const double GRAVITY = 0.2; // How strong gravity pulls the player down
const double MAX_FALL_SPEED = 1.5; // To prevent "tunneling" through thin platforms

void initializePlayer() {
    player.x = 2;              // Start player at a specific X position
    player.y = MAP_HEIGHT - 2; // Start player one unit above the ground
    player.velocityY = 0.0;    // No initial vertical velocity
    player.onGround = true;    // Assume starts on ground for simplicity
}

void applyGravity() {
    player.velocityY += GRAVITY; // Accelerate downwards due to gravity

    // Limit maximum falling speed
    if (player.velocityY > MAX_FALL_SPEED) {
        player.velocityY = MAX_FALL_SPEED;
    }
}

void handlePlayerCollision() {
    // Calculate player's potential new Y position based on current velocity
    int newPlayerY = player.y + static_cast<int>(player.velocityY);

    // --- Vertical Collision (with ground/platforms) ---
    player.onGround = false; // Assume not on ground until proven otherwise

    // Check if player is hitting the bottom of the map (our primary ground)
    if (newPlayerY >= MAP_HEIGHT - 1) {
        player.y = MAP_HEIGHT - 2; // Snap player to just above the ground line
        player.velocityY = 0;      // Stop vertical movement
        player.onGround = true;    // Player is now on the ground
    }
    // Check for collision with platforms/ground below the player's potential new Y
    // We check the tile directly below where the player would land.
    else if (newPlayerY + 1 < MAP_HEIGHT && gameMap[newPlayerY + 1][player.x] == '#') {
        player.y = newPlayerY; // Move player to the potential new Y
        // Adjust player's Y to be exactly on top of the platform
        while (player.y + 1 < MAP_HEIGHT && gameMap[player.y + 1][player.x] == '#') {
            player.y--; // Move up until just above the platform
        }
        player.y++; // Move one more down to be *on* the platform
        player.velocityY = 0;   // Stop vertical movement
        player.onGround = true; // Player is on the ground/platform
    }
    else {
        // No vertical collision detected, so update player's Y position
        player.y = newPlayerY;
    }

    // --- Horizontal Collision (with walls/obstacles) ---
    // This is handled partly in game.cpp's handleInput where `player.x` is updated.
    // However, if the player moves *into* a solid block, we need to correct it.
    if (gameMap[player.y][player.x] == '#') {
        // Simple correction: if player lands inside a wall, push them up one or back one.
        // For a basic platformer, usually this means the X movement was blocked
        // and player should not have moved into the wall.
        // This tutorial simplifies by preventing X move into '#' in game.cpp.
        // A robust solution would check player's previous X and push back.
    }

    // Ensure player is within horizontal map bounds (already done in game.cpp's handleInput)
    // if (player.x < 0) player.x = 0;
    // if (player.x >= MAP_WIDTH) player.x = MAP_WIDTH - 1;
}

Map (src/map.hpp and src/map.cpp)

Our game world will be a simple 2D character array. This is where we define its dimensions and how to initialize and draw it.

// src/map.hpp

#ifndef MAP_HPP
#define MAP_HPP

// Re-include player.hpp to get MAP_WIDTH and MAP_HEIGHT definitions if not already
// It's defined in player.hpp for now, as it's a shared constant.
#include "player.hpp" // Contains MAP_WIDTH and MAP_HEIGHT

// Our game map: a 2D array of characters
extern char gameMap[MAP_HEIGHT][MAP_WIDTH];

// Function declarations for map-related actions
void initializeMap();
void renderMapAndPlayer(); // Renders the map and overlays the player

#endif // MAP_HPP
// src/map.cpp

#include "map.hpp"
#include "player.hpp" // For accessing player's position
#include <iostream>   // For std::cout

// Define the global game map instance
char gameMap[MAP_HEIGHT][MAP_WIDTH];

void initializeMap() {
    // Fill the map with empty spaces initially
    for (int y = 0; y < MAP_HEIGHT; ++y) {
        for (int x = 0; x < MAP_WIDTH; ++x) {
            gameMap[y][x] = ' ';
        }
    }

    // Create the ground at the bottom of the map
    for (int x = 0; x < MAP_WIDTH; ++x) {
        gameMap[MAP_HEIGHT - 1][x] = '#'; // Solid ground
    }

    // Add some platforms for the player to jump on
    for (int x = 5; x < 15; ++x) {
        gameMap[MAP_HEIGHT - 5][x] = '#'; // First platform
    }
    for (int x = 20; x < 30; ++x) {
        gameMap[MAP_HEIGHT - 8][x] = '#'; // Second platform
    }
    for (int x = 10; x < 20; ++x) {
        gameMap[MAP_HEIGHT - 12][x] = '#'; // Higher platform
    }

    // Add a simple obstacle/wall (e.g., for the dino to hit if it doesn't jump)
    gameMap[MAP_HEIGHT - 2][MAP_WIDTH - 5] = '#';
    gameMap[MAP_HEIGHT - 3][MAP_WIDTH - 5] = '#';
}

void renderMapAndPlayer() {
    // Create a temporary buffer to build the frame, then print it once.
    // This reduces flickering compared to printing char by char.
    std::string frameBuffer = "";
    for (int y = 0; y < MAP_HEIGHT; ++y) {
        for (int x = 0; x < MAP_WIDTH; ++x) {
            if (y == player.y && x == player.x) {
                frameBuffer += 'P'; // Draw the player at their current coordinates
            } else {
                frameBuffer += gameMap[y][x]; // Draw the map tile
            }
        }
        frameBuffer += '\n'; // New line after each row
    }
    std::cout << frameBuffer;
}

Part IV. The Main Entry Point (src/platui.cpp)

And yes, the bridge that connects everything we did so far. Mostly known as main.cpp, has become platui!

// src/platui.hpp

#ifndef PLATUI_HPP
#define PLATUI_HPP

// This file can be used for global constants or utility functions
// specific to the Platui project if needed later. For now, it might be simple.

#endif // PLATUI_HPP
// src/platui.cpp

#include "platui.hpp" // Our project's main header (optional, good practice)
#include "game.hpp"   // For the Game class

int main() {
    Game game; // Create an instance of our Game class
    game.run(); // Start the game loop

    return 0; // Program exits gracefully
}

This is as simple as it gets. We create an instance of our Game class and call its run() method. All the complex setup, loop, and cleanup logic is handled within the Game class.


Part V. Compiling and Running Platui

Alright, you’ve got all the pieces! Now, let’s bring it to life.

  1. Navigate to the Root: Open your terminal or command prompt and navigate to the Platui/ directory where your src/and bin/ folders are.
  2. Compile with g++: Use the g++ compiler to compile all your .cpp files. The -o flag specifies the output executable name, and -std=c++17 ensures we’re using a modern C++ standard. -Wall is good practice for warnings.
    g++ src/*.cpp -o bin/platui -std=c++17 -Wall
    • Quick Note: On Windows, your executable will be platui.exe. On Linux/macOS, it’ll just be platui. Easy peasy.
  3. Run the Game: Once compilation is successful, run your platui! ./bin/platui Your terminal should clear, and you should see the Platui! idle screen! Press Space or ‘S’ to start playing.

Conclusion and What’s Next?

So, you did it! You built a bare C++ TUI platformer. Not gonna lie, it’s pretty neat to get something like this running without pulling in a bunch of external libraries. You’ve now got the low-down on:

  • Project Structure: Keeping your files organized like a sane person.
  • The Game Loop: The endless input -> update -> render dance.
  • Game States: How to make your game behave differently when it’s just sitting there, actually running, or when you mess up.
  • Raw Input: Getting those key presses instantly, because waiting for Enter is for chumps.
  • Basic Physics: Gravity and jumping, because who wants a player that just floats? (could be a feature! notch did creepers from pigs!)
  • ASCII Art: Turning # and P into a world-class game environment.

Now, this is just the beginning. The terminal is your oyster, or something like that. If you’re feeling ambitious (and let’s be real, you probably are), here are some ideas to make Platui even less mediocre:

  1. Obstacles that actually move: Make some cacti or birds (or whatever you call them in ASCII) scroll in from the right. Collision with these would be your ultimate test.
  2. Scrolling world: Instead of a static map, make the ground and platforms scroll left, just like the real Chrome Dino. This is where things get tricky, but also way more satisfying.
  3. More sophisticated collision: Our current collision is, shall we say, simple. You can always make it smarter.
  4. Less flickering: Clearing the entire screen is effective, but it flickers. Look into ANSI escape codes (or SetConsoleCursorPosition on Windows) to move the cursor and only redraw what’s changed. It’s like magic, but with more \033[ sequences.
  5. Custom Levels: Hardcoding levels is fine for a start, but imagine loading them from a simple text file. That’s next-level stuff.
  6. More ASCII Art: Get creative! Your ‘P’ can be a proper dino, your ‘#’ can be a cactus, the possibilities are endless… well, as endless as your keyboard allows.
  7. MUSIC!!!

Go on, mess around with it. Break it, fix it, make it your own. That’s how we learn, right?

If you are here wondering why your player doesn’t move, it’s not a bug. It’s just not implemented, you’ll need to implement scrolling in order to make the game work.

Byeeeee


Subscribe to my newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *


ABOUT ME

Hey there! I’m Metin, also known as devsimsek—a young, self-taught developer from Turkey. I’ve been coding since 2009, which means I’ve had plenty of time to make mistakes (and learn from them…mostly).

I love tinkering with web development and DevOps, and I’ve dipped my toes in numerous programming languages—some of them even willingly! When I’m not debugging my latest projects, you can find me dreaming up new ideas or wondering why my code just won’t work (it’s clearly a conspiracy).

Join me on this wild ride of coding, creativity, and maybe a few bad jokes along the way!