Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
357 changes: 357 additions & 0 deletions homeworks/homework_snake/homework.md

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions homeworks/homework_snake/script.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Video script

<!-- Talking head -->
Hey everyone! Welcome to the final project of the course!

Over the past lectures, you have learned an incredible amount of modern C++. We started from basic variables and scopes, tackled functions, move semantics, templates, lambdas, custom data structures, error handling, and even multithreading with mutexes!

Now, it is time to put all of these pieces together into one real, complete, interactive project. And what better way to celebrate your C++ journey than by building the classic, timeless arcade game: **Snake**!

<!-- Screen record: Show gameplay of snake in terminal -->
And yes, as you can see, this runs completely in your terminal, rendered with smooth colored blocks, responsive arrow controls, dynamic fruit generation, and real-time score tracking.

<!-- Talking head -->
Now, building a full game like this can feel daunting if you look at it as a giant monolith. But here is the secret that seasoned software engineers know: complex software is just a collection of small, well-designed components that talk to each other through clean interfaces.

Before we dive into the details, remember: the complete written homework description, the starter skeleton, and the automated tests are all available in the course repository linked right down below.

Let's break down the architecture of what you will be building.

<!-- Screen record: Diagram of decoupled architecture: UI <-> Game <-> World & Snake -->
The game is split into three main modules:

### 1. The Core Primitives (`core`)
First, we need solid mathematical and grid foundations.
You will implement a generic 2D vector template class, `Vector2D`, which provides `FromXY()` and `FromRowCol()` factory methods. Why both? Because in graphics and terminal programming, the horizontal coordinate is $X$ but the vertical row is $Y$. Trust me, keeping this clear from day one will save you hours of head-scratching!

You will also work with a generic `Matrix` class that represents a 2D grid stored in a flat `std::vector`, where you'll implement the row-major 1D indexing formula. If this sounds familiar, that's because you already saw this technique when we worked with images in the Pixelator project.

### 2. The Game Entities (`game`)
Next, we bring the game world to life.
- You'll design a `Heading` enum that uses a neat bitwise trick so opposite directions cancel each other out, preventing the snake from immediately eating its own neck if you press the opposite arrow key.
- You will implement the `World` class, which manages the grid and walls, returning `std::optional<CellType>` to elegantly handle out-of-bounds checks without crashes.
- And of course, the `Snake` class itself. The snake's body is managed using a `std::deque` and a fast lookup set. When the snake moves, its head advances by one cell, and its tail pops unless it just devoured a fruit, in which case it grows.

### 3. The Game Engine & Concurrency (`game`)
This is where the magic happens.
The `Game` class controls the game loop and runs it in its own background thread.
Why a separate thread? Because we want the snake to keep moving at a steady, ticking pace while simultaneously listening for your keyboard input in real time.
You will implement the `NextCycle()` logic, synchronize shared state between threads using `std::mutex` and `std::lock_guard`, and invoke registered callbacks using `std::function` whenever the game state updates or the game ends.

<!-- Talking head -->
Now, I hear you asking: *"Igor, do I have to write raw terminal escape sequences and POSIX termios ioctls to capture arrow keys in the terminal?"*

The answer is: **absolutely not!**

<!-- Screen record: Show snake/ui folder -->
I want you to experience **productive struggle**—mastering templates, invariants, multithreading, and callbacks. I do *not* want you pulling your hair out debugging VT100 terminal escape sequences or terminal canonical modes in C.

So, I have completely pre-built the `ui` module for you! It contains `TerminalIo` and `NcursesDrawer`. It connects to your `Game` purely through callbacks. Your game logic doesn't know or care how the pixels are drawn—it just notifies the drawer via `std::function`. That's clean architecture in action!

<!-- Talking head -->
To tackle this project without getting overwhelmed, follow the milestones in the homework guide:
1. **Milestone 1**: Build your `Vector2D`, `Matrix` indexing, and `Heading`.
2. **Milestone 2**: Implement `World` and `Snake`.
3. **Milestone 3**: Implement the `Game` engine loop, collisions, and thread synchronization.
4. **Milestone 4**: Wire the callbacks in `main.cpp`, run the binary, and play your game!

Notice that in the starter skeleton, I only give you very minimal smoke tests. Writing your own thorough unit tests for each class is a core part of this exercise! Think about edge cases—what happens when the snake turns 180 degrees? What happens when a coordinate is out of bounds? Test it all!

<!-- Screen record: Show tests passing and game launching -->
When you submit your homework, our automated checker will run your tests, and then inject our own rigorous validation tests across four stages to check each component independently.

<!-- Talking head -->
Now, a very important word on your mindset for this project.

Please treat this as a **multi-stage journey**. It is intentionally designed to take time. Do not try to rush it, and do not try to finish it all in one heroic sitting. Give yourself multiple days, taking it strictly one milestone at a time.

You will make mistakes along the way. You will see compiler errors, off-by-one indices, and maybe even a deadlock. That is completely normal! Making mistakes is an essential part of learning to program, and there is no shortcut around it.

Because of that, I strongly recommend that you **do not use AI assistants** like ChatGPT or Copilot for this project. When you let an AI write the code or diagnose your bugs, you skip the very struggle that turns you into a real engineer. Face the challenge yourself. Wrestle with the design, write your own tests, and take immense pride in knowing that every single line of this game was built by your own mind.

If you get stuck, re-watch the lectures on move semantics, lambdas, or multithreading, and don't hesitate to start a discussion in our GitHub community linked below.

I am super proud of how far you've come in this course. Now go build yourself a game!

Thanks for watching, have fun coding, and I'll see you in the next one!
11 changes: 11 additions & 0 deletions homeworks/homework_snake/snake/.clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
Language: Cpp
BasedOnStyle: Google
AllowShortBlocksOnASingleLine: true
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: All
AllowShortLoopsOnASingleLine: true
AllowShortIfStatementsOnASingleLine: true
BinPackArguments: false
BinPackParameters: false
...
4 changes: 4 additions & 0 deletions homeworks/homework_snake/snake/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
build/
/snake.sublime-workspace
.cache/
.DS_Store
14 changes: 14 additions & 0 deletions homeworks/homework_snake/snake/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.16)
project(snake LANGUAGES CXX)

if(NOT CMAKE_BUILD_TYPE OR CMAKE_BUILD_TYPE STREQUAL "")
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE)
endif()

# Must appear in the top-level CMakeLists.txt
include(CTest)

# Add code to the build
add_subdirectory(external)
add_subdirectory(examples)
add_subdirectory(snake)
3 changes: 3 additions & 0 deletions homeworks/homework_snake/snake/examples/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
add_executable(simulate_game simulate_game.cpp)
target_link_libraries(simulate_game PRIVATE game)
target_compile_features(simulate_game PRIVATE cxx_std_17)
31 changes: 31 additions & 0 deletions homeworks/homework_snake/snake/examples/simulate_game.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#include <game/game.h>
#include <game/snake.h>
#include <game/world.h>

#include <chrono>
#include <iostream>
#include <thread>

#include "core/vector_2d.h"

using snake::core::Vector2i;
using snake::game::Game;
using snake::game::Heading;
using snake::game::Snake;
using snake::game::World;

int main() {
auto world = World::CreateBoxWorld(20, 20);
world.SetCell(Vector2i::FromXY(6, 5), World::CellType::kFruit);

Snake snake{Vector2i::FromXY(5, 5), Heading::kRight};
Game game{std::move(world), std::move(snake)};

const std::chrono::milliseconds cycle_period{5};
game.Start(cycle_period);
std::this_thread::sleep_for(std::chrono::milliseconds{200});
game.End();

std::cout << "Simulation finished with score: " << game.score() << std::endl;
return 0;
}
4 changes: 4 additions & 0 deletions homeworks/homework_snake/snake/external/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include(cmake/CPM.cmake)
include(gtest.cmake)
include(abseil.cmake)
include(fmt.cmake)
10 changes: 10 additions & 0 deletions homeworks/homework_snake/snake/external/abseil.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CPMAddPackage(
NAME absl
GITHUB_REPOSITORY abseil/abseil-cpp
GIT_TAG "20250127.0"
OPTIONS
"ABSEIL_BUILD_TESTING OFF"
"ABSL_BUILD_TEST_HELPERS OFF"
"CMAKE_CXX_STANDARD 17"
"CMAKE_CXX_STANDARD_REQUIRED ON"
)
24 changes: 24 additions & 0 deletions homeworks/homework_snake/snake/external/cmake/CPM.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# SPDX-License-Identifier: MIT
#
# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors

set(CPM_DOWNLOAD_VERSION 0.40.2)
set(CPM_HASH_SUM "c8cdc32c03816538ce22781ed72964dc864b2a34a310d3b7104812a5ca2d835d")

if(CPM_SOURCE_CACHE)
set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
elseif(DEFINED ENV{CPM_SOURCE_CACHE})
set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
else()
set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
endif()

# Expand relative path. This is important if the provided path contains a tilde (~)
get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE)

file(DOWNLOAD
https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake
${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM}
)

include(${CPM_DOWNLOAD_LOCATION})
2 changes: 2 additions & 0 deletions homeworks/homework_snake/snake/external/fmt.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CPMAddPackage("gh:fmtlib/fmt#11.1.3")

10 changes: 10 additions & 0 deletions homeworks/homework_snake/snake/external/gtest.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CPMAddPackage(
NAME googletest
GITHUB_REPOSITORY google/googletest
VERSION 1.15.2
OPTIONS
"INSTALL_GTEST OFF"
"gtest_force_shared_crt"
"CMAKE_CXX_STANDARD 17"
"CMAKE_CXX_STANDARD_REQUIRED ON"
)
7 changes: 7 additions & 0 deletions homeworks/homework_snake/snake/snake/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
add_subdirectory(core)
add_subdirectory(game)
add_subdirectory(ui)

add_executable(main main.cpp)
target_link_libraries(main PRIVATE game ui)
target_compile_features(main PRIVATE cxx_std_17)
31 changes: 31 additions & 0 deletions homeworks/homework_snake/snake/snake/core/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
add_library(cxx_opts INTERFACE)
target_compile_features(cxx_opts INTERFACE cxx_std_17)
target_include_directories(cxx_opts INTERFACE
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/snake>
$<INSTALL_INTERFACE:include>)
target_compile_options(cxx_opts INTERFACE
$<$<CONFIG:DEBUG>:-g3>
$<$<CONFIG:DEBUG>:-Og>
$<$<CONFIG:RELEASE>:-O3>
-Wall -Wpedantic -Wextra
)

add_library(no_gcc_compat_flag INTERFACE)
target_compile_options(no_gcc_compat_flag INTERFACE
$<$<CXX_COMPILER_ID:Clang,AppleClang>:-Wno-gcc-compat>
)

add_library(core
matrix.cpp
vector_2d.cpp)
target_link_libraries(core PUBLIC cxx_opts no_gcc_compat_flag)

add_executable(
test_core
matrix_test.cpp
vector_2d_test.cpp
)
target_link_libraries(test_core PRIVATE core gtest_main)

include(GoogleTest)
gtest_discover_tests(test_core)
20 changes: 20 additions & 0 deletions homeworks/homework_snake/snake/snake/core/matrix.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include <core/matrix.h>

#include <cstdint>

namespace snake::core {

template class Matrix<std::int8_t>;
template class Matrix<std::int16_t>;
template class Matrix<std::int32_t>;
template class Matrix<std::int64_t>;

template class Matrix<std::uint8_t>;
template class Matrix<std::uint16_t>;
template class Matrix<std::uint32_t>;
template class Matrix<std::uint64_t>;

template class Matrix<float>;
template class Matrix<double>;

} // namespace snake::core
71 changes: 71 additions & 0 deletions homeworks/homework_snake/snake/snake/core/matrix.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#ifndef SNAKE_CORE_MATRIX_H_
#define SNAKE_CORE_MATRIX_H_

#include <cstdint>
#include <vector>

namespace snake::core {

template <typename T>
class Matrix {
public:
Matrix() = default;
explicit Matrix(std::int32_t rows, std::int32_t cols, T init_value = T{})
: rows_{rows}, cols_{cols}, data_(rows * cols, init_value) {}

[[nodiscard]] inline std::int32_t rows() const noexcept { return rows_; }
[[nodiscard]] inline std::int32_t cols() const noexcept { return cols_; }

inline void resize(std::int32_t rows, std::int32_t cols) {
rows_ = rows;
cols_ = cols;
data_.resize(rows * cols);
}

// Checked access with at(row, col)
[[nodiscard]] inline T& at(std::int32_t row, std::int32_t col) {
return data_.at(index(row, col));
}
[[nodiscard]] inline const T& at(std::int32_t row, std::int32_t col) const {
return data_.at(index(row, col));
}

// Fast unchecked access with operator()
[[nodiscard]] inline T& operator()(std::int32_t row,
std::int32_t col) noexcept {
return data_[index(row, col)];
}
[[nodiscard]] inline const T& operator()(std::int32_t row,
std::int32_t col) const noexcept {
return data_[index(row, col)];
}

[[nodiscard]] inline std::vector<T>& data() noexcept { return data_; }
[[nodiscard]] inline const std::vector<T>& data() const noexcept {
return data_;
}

// Iterators for range-based for loops
[[nodiscard]] inline auto begin() { return data_.begin(); }
[[nodiscard]] inline auto begin() const { return data_.begin(); }
[[nodiscard]] inline auto cbegin() const { return data_.cbegin(); }
[[nodiscard]] inline auto end() { return data_.end(); }
[[nodiscard]] inline auto end() const { return data_.end(); }
[[nodiscard]] inline auto cend() const { return data_.cend(); }

private:
[[nodiscard]] inline std::int32_t index(std::int32_t,
std::int32_t) const noexcept {
// TODO(student): Flatten 2D coordinate into 1D array index
// Hint: use row-major notation: (row * number_of_columns + col)
return 0;
}

std::int32_t rows_{};
std::int32_t cols_{};
std::vector<T> data_{};
};

} // namespace snake::core

#endif // SNAKE_CORE_MATRIX_H_
16 changes: 16 additions & 0 deletions homeworks/homework_snake/snake/snake/core/matrix_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include <core/matrix.h>
#include <gtest/gtest.h>

using snake::core::Matrix;

TEST(Matrix, InitDefault) {
Matrix<int> matrix_empty;
ASSERT_EQ(0, matrix_empty.rows());
ASSERT_EQ(0, matrix_empty.cols());
}

// TODO(student): Write unit tests for:
// - Initialization with dimensions and default values
// - Modifying and reading values with .at() and operator()
// - Out-of-bounds error handling (verifying std::out_of_range is thrown by .at())
// - Iterating over matrix elements using range-based for loops
9 changes: 9 additions & 0 deletions homeworks/homework_snake/snake/snake/core/vector_2d.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#include <core/vector_2d.h>

namespace snake::core {

template struct Vector2D<std::int32_t>;
template struct Vector2D<float>;
template struct Vector2D<double>;

} // namespace snake::core
Loading
Loading