diff --git a/games/tetriz/.gitignore b/games/tetriz/.gitignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/games/tetriz/.gitignore @@ -0,0 +1 @@ +build diff --git a/games/tetriz/.vscode/settings.json b/games/tetriz/.vscode/settings.json new file mode 100644 index 00000000..aaaa7d78 --- /dev/null +++ b/games/tetriz/.vscode/settings.json @@ -0,0 +1,89 @@ +{ + "cmake.generator": "Ninja", + "cmake.outputLogEncoding": "auto", + "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", + "files.associations": { + "iostream": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "cctype": "cpp", + "charconv": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "istream": "cpp", + "limits": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "semaphore": "cpp", + "span": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "thread": "cpp", + "typeinfo": "cpp", + "variant": "cpp", + "codecvt": "cpp", + "map": "cpp", + "ranges": "cpp", + "fstream": "cpp", + "cstring": "cpp", + "format": "cpp", + "forward_list": "cpp", + "ios": "cpp", + "list": "cpp", + "locale": "cpp", + "queue": "cpp", + "xfacet": "cpp", + "xhash": "cpp", + "xiosbase": "cpp", + "xlocale": "cpp", + "xlocbuf": "cpp", + "xlocinfo": "cpp", + "xlocmes": "cpp", + "xlocmon": "cpp", + "xlocnum": "cpp", + "xloctime": "cpp", + "xmemory": "cpp", + "xstring": "cpp", + "xtr1common": "cpp", + "xtree": "cpp", + "xutility": "cpp" + } +} \ No newline at end of file diff --git a/games/tetriz/CMakeLists.txt b/games/tetriz/CMakeLists.txt new file mode 100644 index 00000000..bca1eae5 --- /dev/null +++ b/games/tetriz/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.22) +project(tetriz) + +set(CMAKE_CXX_STANDARD 20) + +configure_file( ${PROJECT_SOURCE_DIR}/tetriz.map ${PROJECT_BINARY_DIR}) + +aux_source_directory(. src) +add_executable(tetriz ${src}) + +if(MSVC) + target_compile_options(tetriz PRIVATE "/utf-8") + set_property(TARGET tetriz PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +endif() diff --git a/games/tetriz/Tetriz.png b/games/tetriz/Tetriz.png new file mode 100644 index 00000000..f6dadf31 Binary files /dev/null and b/games/tetriz/Tetriz.png differ diff --git a/games/tetriz/color.h b/games/tetriz/color.h new file mode 100644 index 00000000..9f39153d --- /dev/null +++ b/games/tetriz/color.h @@ -0,0 +1,16 @@ + +#pragma once + +enum class Color +{ + Cyan = 14, + Red = 9, + Orange = 214, + Yellow = 11, + Green = 2, + Blue = 12, + Purple = 5, + White = 15, + Black = 0, + Gray = 8 +}; \ No newline at end of file diff --git a/games/tetriz/config.txt b/games/tetriz/config.txt new file mode 100644 index 00000000..1bd69496 --- /dev/null +++ b/games/tetriz/config.txt @@ -0,0 +1,50 @@ +#-- ARCADE MACHINE GAME CONFIG FILE --# +#-- For THOTH TECH - Splashkit Crew - Applications Team +#-------------------------------------------------------------------- +#-- File: config.txt +#-- Description: Configuration data for Arcade Machine games +#-------------------------------------------------------------------- +#-- Maintenance Log -- +#-------------------------------------------------------------------- +# Task | Who | Date | Comments +#-----------+---------+----------+----------------------------------- +# Sprint 1 | AGEORGE | 20220326 | Created +#-----------+---------+----------+----------------------------------- +# Sprint 2 | AGEORGE | 20220511 | Added Description +#-----------+---------+----------+----------------------------------- +# Sprint 3 | AGEORGE | 20220828 | Added win, linux and macos params +#-----------+---------+----------+----------------------------------- + +#-------------------------------------------------------------------- +# PLEASE ONLY EDIT CONTENT AFTER THE "=" SIGN FOR EACH CONFIGURATION SETTING +#-------------------------------------------------------------------- + +# The title of your game (eg. Super Mario) +title=Tetriz + +# The name of the author (eg. Jane Doe) +author=WEI ZHANG + +# The genre of your game (eg. Platformer) +genre=Puzzle + +# A Description of your game +description=A modern terminal-based Tetris game built with C++20. Features classic Tetris gameplay with hold system, next piece preview, pause/resume functionality, and responsive arcade-style controls. Cross-platform support for Linux and Windows. + +# A classification rating to advise the nature of the content (eg. MA 15+) +rating=E + +# Programming language the game is written in (eg. C++) +language=C++ + +# Path to your game thumbnail image (eg. images/icon.png) +image=Tetriz.png + +# Location of git repo (eg. https://github.com/thoth-tech/arcade-games) +repository=https://github.com/ZGT23/arcade-games + +# [Optional] Uncomment to include path to the game executable (game.exe) + + +# [Optional] Uncomment to include specific compile commands - if you have a specific compile command (eg. 'make', 'skm g++ *.cpp -lstd++') +#compile-command=skm g++ *.cpp \ No newline at end of file diff --git a/games/tetriz/control.cpp b/games/tetriz/control.cpp new file mode 100644 index 00000000..79d1ee3e --- /dev/null +++ b/games/tetriz/control.cpp @@ -0,0 +1,109 @@ +#include "control.h" +#include "define.h" +#include "game.h" +namespace gm +{ + char command; + + std::map> comm_func{ + {KEY_ESCAPE, command_quit}, + {KEY_W, command_pause}, + {KEY_S, command_down}, + {KEY_A, command_left}, + {KEY_D, command_right}, + {KEY_R, command_reset}, + {KEY_F, command_drop}, + {KEY_T, command_rotate_L}, + {KEY_G, command_rotate_2}, + {KEY_Y, command_hold}, + {KEY_H, command_help}, + }; +#ifdef __linux__ + char getch() + { + char c; + struct termios old, cur; + tcgetattr(0, &cur); + old = cur; + cfmakeraw(&cur); + tcsetattr(0, 0, &cur); + c = getchar(); + tcsetattr(0, 0, &old); + return c; + } +#endif + + void key_event() + { + while (running) + { + command = getch(); + if (comm_func.find(command) != comm_func.end()) + comm_func[command](); + } + } + + void start_listener() + { + static std::jthread t(key_event); + } + + void command_quit() + { + quit(); + } + + void command_rotate_R() + { + rotate(1); + } + void command_rotate_L() + { + rotate(3); + } + void command_rotate_2() + { + rotate(2); + } + + void command_left() + { + left(); + } + + void command_right() + { + right(); + } + + void command_down() + { + down(); + } + + void command_drop() + { + drop(); + } + + void command_hold() + { + hold(); + } + + void command_reset() + { + reset(); + } + + void command_help() + { + help(); + } + + void command_pause() + { + pause(); + } + +} // namespace gm \ No newline at end of file diff --git a/games/tetriz/control.h b/games/tetriz/control.h new file mode 100644 index 00000000..ff9d3961 --- /dev/null +++ b/games/tetriz/control.h @@ -0,0 +1,24 @@ + +#pragma once + +namespace gm +{ + extern char command; + void key_event(); + void start_listener(); + + // 键盘命令函数 + void command_quit(); + void command_rotate_R(); + void command_rotate_L(); + void command_rotate_2(); + void command_left(); + void command_right(); + void command_down(); + void command_drop(); + void command_hold(); + void command_reset(); + void command_help(); + void command_pause(); + +} // namespace gm diff --git a/games/tetriz/define.h b/games/tetriz/define.h new file mode 100644 index 00000000..dc08914d --- /dev/null +++ b/games/tetriz/define.h @@ -0,0 +1,64 @@ + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#include +#elif _WIN32 +#include +#include +#endif + +using namespace std::chrono_literals; + +#define KEY_W 'w' +#define KEY_S 's' +#define KEY_A 'a' +#define KEY_D 'd' +#define KEY_R 'r' +#define KEY_F 'f' +#define KEY_T 't' +#define KEY_G 'g' +#define KEY_Y 'y' +#define KEY_H 'h' + + +#ifdef __linux__ +#define KEY_UP 65 +#define KEY_DOWN 66 +#define KEY_LEFT 68 +#define KEY_RIGHT 67 +#elif _WIN32 +#define KEY_UP 72 +#define KEY_DOWN 80 +#define KEY_LEFT 75 +#define KEY_RIGHT 77 +#endif + +#define KEY_ENTER 13 +#define KEY_BACKSPACE 127 +#define KEY_ESCAPE 27 +#define KEY_SPACE 32 + +using Matrix = std::vector>; +using Tetromino = std::vector>>; +using Offset = std::vector>>; +using std::ranges::views::iota; diff --git a/games/tetriz/draw.cpp b/games/tetriz/draw.cpp new file mode 100644 index 00000000..cc03beb8 --- /dev/null +++ b/games/tetriz/draw.cpp @@ -0,0 +1,204 @@ + +#include "draw.h" +#include "terminal.h" +#include +#include "utils.h" +#include "piece.h" +#include "game.h" +/** + * 0 1 2 3 4 5 6 7 8 9 A B C D E F +U+250x ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ +U+251x ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ +U+252x ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ +U+253x ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ +U+254x ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ +U+255x ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ +U+256x ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ +U+257x ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ + * +*/ + +namespace dw +{ + // 0123456 + const std::u32string style1 = U" ┌┐└┘│─"; + const std::u32string style2 = U" ╔╗╚╝║═"; + const std::u32string style3 = U" ┏┓┗┛┃━"; + const std::u32string style4 = U" ╭╮╰╯│─"; + std::u32string cur_style = style4; + + std::map map_style{ + {1, style1}, + {2, style2}, + {3, style3}, + {4, style4}, + }; + + // 1,10,12,22 + void window(int top, int left, int width, int height, std::string title, int style, int color, std::ostream &os) + { + if (style) + cur_style = map_style[style]; + + tc::set_fore_color(color, os); + for (int r = 0; r < height; ++r) + { + tc::move_to(top + r, ut::b2c(left), os); + for (int c = 0; c < width; ++c) + { + if (r == 0) // 第一行 + { + if (c == 0) // 第一列 + { + os << ut::utf32_to_utf8({cur_style[0], cur_style[1]}); + } + else if (c == width - 1) // 最后一列 + { + os << ut::utf32_to_utf8({cur_style[2]}); + } + else + { + os << ut::utf32_to_utf8({cur_style[6], cur_style[6]}); // 2个— + } + } + else if (r == height - 1) // 最后一行 + { + if (c == 0) // 第一列 + { + os << ut::utf32_to_utf8({cur_style[0], cur_style[3]}); + } + else if (c == width - 1) // 最后一列 + { + os << ut::utf32_to_utf8({cur_style[4]}); + } + else + { + os << ut::utf32_to_utf8({cur_style[6], cur_style[6]}); // 2个— + } + } + else + { + if (c == 0) // 第一列 + { + os << ut::utf32_to_utf8({cur_style[0], cur_style[5]}); + } + else if (c == width - 1) // 最后一列 + { + os << ut::utf32_to_utf8({cur_style[5]}); + } + else + { + os << " "; + } + } + } + } + + // title + tc::move_to(top, ut::b2c(left) + (width * 2 - title.length()) / 2, os); + os << title; + } + + void frame(Matrix &frame, int top, int left) + { + static Matrix buffer(frame.size(), std::vector(frame[0].size(), -1)); + static Matrix buffer_top2(2, std::vector(frame[0].size(), -1)); + + if (gm::reseting) + { + buffer = Matrix(frame.size(), std::vector(frame[0].size(), -1)); + buffer_top2 = Matrix(2, std::vector(frame[0].size(), -1)); + } + + Matrix f(frame.begin(), frame.begin() + 20); + matrix(f, top + 2, left, &buffer, 1); + + Matrix v(frame.begin() + 20, frame.end()); + matrix(v, top, left, &buffer_top2, 2); + } + + void next(std::queue next5, int top, int left) + { + static Matrix buffer(15, std::vector(6, -1)); + if (gm::reseting) + { + buffer = Matrix(15, std::vector(6, -1)); + } + Matrix next_field(15, std::vector(6, 0)); + for (int y = 12; next5.size() > 0; y -= 3) + { + gm::Piece p(next5.front(), 2, y, 0); + gm::merge(next_field, p); + next5.pop(); + } + + matrix(next_field, top, left, &buffer); + } + + void hold(Tetromino &h, int top, int left) + { + static Matrix buffer(4, std::vector(7, -1)); + if (gm::reseting) + { + buffer = Matrix(4, std::vector(7, -1)); + } + Matrix hold_field(4, std::vector(7, 0)); + if (!h.empty()) + { + gm::Piece p(h, 3, 1, 0); + if (gm::holding) + { + p.set_disable(); + } + gm::merge(hold_field, p); + matrix(hold_field, top, left, &buffer); + } + } + + void matrix(Matrix &m, int top, int left, Matrix *buffer, int style) + { + std::string blank = " "; + if (style == 1) + blank = "\u30fb"; + std::ostringstream oss; + // frame xy ----> row/col + int row, col; + for (int y = 0; y < m.size(); ++y) + { + for (int x = 0; x < m[0].size(); ++x) + { + if (buffer != nullptr) + { + if ((*buffer)[y][x] == m[y][x]) + continue; + (*buffer)[y][x] = m[y][x]; + } + + row = top + m.size() - y - 1; + col = left + x; + tc::move_to(row, ut::b2c(col), oss); + if (m[y][x] > 0) // 正常块 + { + tc::reset_color(oss); + tc::set_back_color(m[y][x], oss); + oss << " "; + } + else if (m[y][x] < 0) // 阴影块 + { + tc::reset_color(oss); + tc::set_fore_color(0 - m[y][x], oss); + oss << "\u25e3\u25e5"; + } + else // 空白区 + { + tc::reset_color(oss); + if (style == 2 && y == 0) + tc::set_underline(oss); + oss << blank; // "\u30FB"; + } + } + } + std::cout << oss.str(); + } + +} // namespace dw \ No newline at end of file diff --git a/games/tetriz/draw.h b/games/tetriz/draw.h new file mode 100644 index 00000000..12e68262 --- /dev/null +++ b/games/tetriz/draw.h @@ -0,0 +1,21 @@ +#pragma once +#include "define.h" +#include "tetromino.h" + +namespace dw +{ + void window(int top, int left, int width, int height, std::string title="",int style=0,int color=(int)Color::White,std::ostream& os=std::cout); + void frame(Matrix& frame,int top,int left); + void next(std::queue next5,int top,int left); + void hold(Tetromino &h, int top, int left); + //----------------------------------------------------------- + /** + * style: 风格标签: + * 0. 默认风格,无风格 + * 1. 白点打底 + * 2. 下划线 + * + */ + void matrix(Matrix& m,int top,int left,Matrix* buffer=nullptr,int style=0); + +} // namespace dw diff --git a/games/tetriz/game.cpp b/games/tetriz/game.cpp new file mode 100644 index 00000000..a8f4e382 --- /dev/null +++ b/games/tetriz/game.cpp @@ -0,0 +1,290 @@ +#include "game.h" +#include "tetromino.h" +#include "utils.h" +#include "control.h" +namespace gm +{ + //---------------变量定义区--------------- + bool locking; + bool running; + bool holding; + bool ending; + bool reseting; + bool helping; + bool pausing; + Piece one_piece; + Matrix playfield; + std::chrono::microseconds duration; + Matrix frame; + std::queue next; + std::vector bag; + Tetromino hold_piece; + int score, level, lines; + std::chrono::microseconds time; + //----------------------------------- + void init() + { + srand(std::time(0)); + locking = false; + running = true; + holding = false; + ending = false; + reseting = false; + helping = false; + pausing = false; + score = lines = 0; + levelup(); + playfield = Matrix(22, std::vector(10, 0)); + load(); + bag.clear(); + preview(); + one_piece = pick(); + frame = playfield; + hold_piece.clear(); + time = 0ms; + } + + void process() + { + if (ending || pausing) + return; + + render(); + if (ut::timer(duration)) + { + // 如果无法继续下落,就锁定在游戏区 + if (one_piece.down()) + return; + + if (locking) + { + // 锁定 + lock(); + // 消行 + clear(); + levelup(); + one_piece = pick(); + locking = false; + holding = false; + reseting = false; + } + else + { + locking = true; + } + } + } + + void render() + { + frame = playfield; + // 正常块 + merge(frame, one_piece); + // 阴影块 + Piece ghost = one_piece; + ghost.set_ghost(); + while (ghost.down()) + ; + + merge(frame, ghost); + } + Piece pick() + { + assert(next.size() > 0); + Piece p(next.front(), 4, 20, 0); + next.pop(); + + if (!p.test(4, 20)) + { + // game over! + ending = true; + } + + preview(); + + return std::move(p); + } + void lock() + { + merge(playfield, one_piece); + } + void clear() + { + int count = 0; + for (auto it = playfield.begin(); it != playfield.end();) + { + bool full = true; + for (auto cell : *it) + { + if (cell == 0) + { + full = false; + break; + } + } + if (full) + { + // 消行 [1,2,3,4] -->[1,3,4] + it = playfield.erase(it); + playfield.push_back(std::vector(it->size(), 0)); + count++; + } + else + ++it; + } + /** + Single 100 x level 100 x level + Double 300 x level 300 x level + Triple 500 x level 500 x level + Tetris 800 x level 800 x level + Soft drop 1 point per cell 1 point per cell + Hard drop 2 points per cell 2 point per cell + */ + if (count == 1) + score += 100 * level; + if (count == 2) + score += 300 * level; + if (count == 3) + score += 500 * level; + if (count == 4) + score += 800 * level; + lines += count; + } + void quit() + { + running = false; + } + void rotate(int i) + { + if (pausing) + return; + one_piece.rotate(i); + } + + void left() + { + if (pausing) + return; + one_piece.left(); + } + + void right() + { + if (pausing) + return; + one_piece.right(); + } + + void down() + { + if (pausing) + return; + if (one_piece.down()) + score += 1; + } + + void drop() + { + if (pausing) + return; + while (one_piece.down()) + score += 2; + locking = true; + // 直落后马上生效 + duration = 0s; + } + + void preview() + { + int index; + while (next.size() < 5) + { + if (bag.size() == 0) + bag = {I, J, L, O, S, T, Z}; + index = rand() % bag.size(); + next.push(bag[index]); + bag.erase(bag.begin() + index); + } + } + + void load() + { + std::ifstream fs("tetriz.map"); + assert(fs.is_open()); + std::string line; + // 22*10 + for (auto &row : playfield | std::ranges::views::take(20) | std::ranges::views::reverse) + { + getline(fs, line); + for (auto i : iota(0, 10)) + { + if (line[i] == '1') + { + row[i] = (int)Color::Gray; + } + } + } + fs.close(); + } + + void hold() + { + if (pausing) + return; + if (holding) + return; + + if (hold_piece.empty()) + { + hold_piece = one_piece.get_tetromino(); + one_piece = pick(); + } + else + { + auto tmp = hold_piece; + hold_piece = one_piece.get_tetromino(); + one_piece = Piece(tmp, 4, 20, 0); + } + + holding = true; + } + + void levelup() + { + // 每消除10行 升1级 + level = lines / 10 + 1; + // Time = (0.8-((Level-1)*0.007))^(Level-1) + duration = std::chrono::milliseconds(int(pow((0.8 - ((level - 1) * 0.007)), level - 1) * 1000)); + } + + void reset() + { + init(); + reseting = true; + } + + void help() + { + helping = !helping; + reseting = !helping; + pausing = helping; + } + + void pause() + { + pausing = !pausing; + reseting = !pausing; + } + + void merge(Matrix &m, const Piece &p) + { + auto [x, y] = p.get_xy(); + for (auto i : iota(0, 4)) + { + auto [dx, dy] = p.get_mino(i); + if (m[y + dy][x + dx] == 0) + m[y + dy][x + dx] = p.get_color(); + } + } + +} // namespace gm \ No newline at end of file diff --git a/games/tetriz/game.h b/games/tetriz/game.h new file mode 100644 index 00000000..e6887743 --- /dev/null +++ b/games/tetriz/game.h @@ -0,0 +1,84 @@ +#pragma once +#include "tetromino.h" +#include "piece.h" +namespace gm +{ + + //======================================= + // 游戏变量定义 + //======================================= + // 游戏运行状态 + extern bool running; + // 锁定标志 + extern bool locking; + // 暂存标志 + extern bool holding; + // 重置标志 + extern bool reseting; + // 终结标志 + extern bool ending; + // 帮助标志 + extern bool helping; + // 暂停标志 + extern bool pausing; + // 当前掉落的块 + extern Piece one_piece; + // 游戏场地 + extern Matrix playfield; + // 每帧时间间隔 + extern std::chrono::microseconds duration; + // 当前渲染帧 + extern Matrix frame; + // 5格预览队列 + extern std::queue next; + // 方块口袋 + extern std::vector bag; + // 暂存块 + extern Tetromino hold_piece; + // 计分、等级、消行 + extern int score, level, lines; + // 总耗时 + extern std::chrono::microseconds time; + //======================================= + // 游戏逻辑 + //======================================= + // 游戏主逻辑 + void process(); + // 游戏初始化 + void init(); + // 渲染当前帧 + void render(); + // 获取一个块 + Piece pick(); + // 锁定 + void lock(); + // 消行 + void clear(); + // 退出 + void quit(); + void rotate(int i); + void left(); + void right(); + void down(); + // 直落 + void drop(); + // 生成预览队列 + void preview(); + // 载入预设地图 + void load(); + // 暂存 + void hold(); + // 升级 + void levelup(); + // 重置游戏 + void reset(); + // 帮助 + void help(); + // 暂停 + void pause(); + // 回显命令 + std::string echo(); + //----------------------- + void merge(Matrix &m, const Piece &p); + +} // namespace gm diff --git a/games/tetriz/main.cpp b/games/tetriz/main.cpp new file mode 100644 index 00000000..4871f5d2 --- /dev/null +++ b/games/tetriz/main.cpp @@ -0,0 +1,44 @@ +#include "define.h" +#include "terminal.h" +#include "utils.h" +#include "draw.h" +#include "control.h" +#include "game.h" +#include "window.h" + +void init() +{ + + ui::window_init(); + ui::show_windows(); + ui::show_help(); + gm::init(); + gm::start_listener(); +} + +void loop() +{ + while (gm::running) + { + gm::process(); + ui::show_info(); + ui::show_game(); + ui::window_resize(); + std::this_thread::sleep_for(10ms); + } +} + +void exit() +{ + ui::show_exit(); + ui::window_exit(); +} + +int main() +{ + init(); + loop(); + exit(); + + return 0; +} \ No newline at end of file diff --git a/games/tetriz/piece.cpp b/games/tetriz/piece.cpp new file mode 100644 index 00000000..0ab2b2ef --- /dev/null +++ b/games/tetriz/piece.cpp @@ -0,0 +1,119 @@ +#include "piece.h" +#include "game.h" +namespace gm +{ + Piece::Piece(Tetromino &t, int x0, int y0, int i) + : tetro_set(t), + x(x0), y(y0), + index(i), + sp_playfield(std::make_shared(playfield)), + status(1) + { + if (get_type() == 'I') + { + offset = gm::offset_i; + } + else if (get_type() == 'O') + { + offset = gm::offset_o; + } + else + { + offset = gm::offset; + } + } + bool Piece::down() + { + return move(0, -1); + } + bool Piece::left() + { + return move(-1, 0); + } + bool Piece::right() + { + return move(1, 0); + } + bool Piece::rotate(int i) + { + assert(i >= 1 && i <= 3); + int new_index = (index + i) % 4; + for (auto i : iota(0, (int)offset[0].size())) + { + auto [dx_0, dy_0] = offset[index][i]; + auto [dx_1, dy_1] = offset[new_index][i]; + + auto dx = dx_0 - dx_1; + auto dy = dy_0 - dy_1; + Piece new_piece(tetro_set, x, y, new_index); + if (new_piece.test(x + dx, y + dy)) + { + index = new_index; + x += dx; + y += dy; + return true; + } + } + + return false; + } + + bool Piece::test(int ox, int oy) const + { + assert(sp_playfield != nullptr); + for (int i = 0; i < 4; ++i) + { + auto [dx, dy] = get_mino(i); + // 1. 越界 + if (ox + dx < 0 || ox + dx > (*sp_playfield)[0].size() - 1 || oy + dy < 0 || oy + dy > (*sp_playfield).size() - 1) + return false; + // 2. 有块 + if ((*sp_playfield)[oy + dy][ox + dx] > 0) + return false; + } + return true; + } + void Piece::set_ghost() + { + status = 0; + } + void Piece::set_disable() + { + status = 2; + } + Tetromino Piece::get_tetromino() const + { + return tetro_set; + } + bool Piece::move(int dx, int dy) + { + if (test(x + dx, y + dy)) + { + x += dx; + y += dy; + return true; + } + return false; + } + char Piece::get_type() const + { + return tetro_set[index][0].first; + } + std::pair Piece::get_mino(int i) const + { + assert(i >= 0 && i <= 3); + + if (i == 0) + return {0, 0}; + return tetro_set[index][i]; + } + std::pair Piece::get_xy() const + { + return {x, y}; + } + int Piece::get_color() const + { + if(status==2) return (int)Color::White; + return status ? tetro_set[index][0].second : 0 - tetro_set[index][0].second; + } +} // namespace gm diff --git a/games/tetriz/piece.h b/games/tetriz/piece.h new file mode 100644 index 00000000..ce9349fd --- /dev/null +++ b/games/tetriz/piece.h @@ -0,0 +1,34 @@ +#pragma once +#include "tetromino.h" +#include "define.h" + +namespace gm +{ + class Piece + { + public: + Piece(Tetromino &t, int x0, int y0, int i); + Piece() = default; + bool down(); + bool left(); + bool right(); + bool rotate(int i);//1:R 2:180 3:L + std::pair get_mino(int i) const; + std::pair get_xy() const; + int get_color() const; + bool test(int x, int y) const; + void set_ghost(); + void set_disable(); + Tetromino get_tetromino() const; + private: + bool move(int dx,int dy); + char get_type() const; + + Tetromino tetro_set; + int index; // [0 R 2 L] + int x, y; // 正交直角坐标系 + std::shared_ptr sp_playfield; + int status; //0:阴影, 1:正常 2:不可用 + Offset offset;//踢墙表 + }; +} // namespace gm diff --git a/games/tetriz/readme.md b/games/tetriz/readme.md new file mode 100644 index 00000000..64318423 --- /dev/null +++ b/games/tetriz/readme.md @@ -0,0 +1,189 @@ +# Tetriz + +A modern terminal-based Tetris game written in C++20 with cross-platform support. + +## 🎮 Features + +- **Classic Tetris Gameplay**: Complete with all standard Tetris mechanics +- **Cross-Platform**: Runs on both Linux and Windows +- **Terminal UI**: Beautiful terminal-based interface with colors and animations +- **Modern C++**: Built with C++20 features and best practices +- **Real-time Controls**: Responsive keyboard controls +- **Game Statistics**: Score, level, and line tracking +- **Hold System**: Store pieces for later use +- **Next Piece Preview**: See upcoming pieces +- **Pause/Resume**: Full game state management +- **Help System**: Built-in controls reference + +## 🎯 Game Controls + +| Action | Key(s) | +|--------|--------| +| Pause/Resume | `W` | +| Rotate Left | `T` | +| Rotate 180° | `G` | +| Move Left | `A` | +| Move Right | `D` | +| Soft Drop | `S` | +| Hard Drop | `F` | +| Hold Piece | `Y` | +| Reset Game | `R` | +| Help | `H` | +| Quit | `ESC` | + +## 🚀 Quick Start + +### Prerequisites + +- C++20 compatible compiler (GCC 10+, Clang 12+, or MSVC 2019+) +- CMake 3.22 or later + +### Linux + +```bash +# Install dependencies (Ubuntu/Debian) +sudo apt install cmake g++ + +# Clone and build +git clone +cd tetriz +mkdir build && cd build +cmake .. +make + +# Run the game +./tetriz +``` + +### Windows + +```bash +# Using Visual Studio Developer Command Prompt +git clone +cd tetriz +mkdir build && cd build +cmake .. +cmake --build . --config Release + +# Run the game +.\Release\tetriz.exe +``` + +## 🏗️ Building from Source + +1. **Clone the repository**: + ```bash + git clone + cd tetriz + ``` + +2. **Create build directory**: + ```bash + mkdir build + cd build + ``` + +3. **Configure with CMake**: + ```bash + cmake .. + ``` + +4. **Build the project**: + ```bash + # Linux/macOS + make + + # Windows (Visual Studio) + cmake --build . --config Release + ``` + +5. **Run the executable**: + ```bash + # Linux/macOS + ./tetriz + + # Windows + .\Release\tetriz.exe + ``` + +## 📁 Project Structure + +``` +tetriz/ +├── CMakeLists.txt # Build configuration +├── main.cpp # Entry point +├── game.h/cpp # Core game logic +├── piece.h/cpp # Piece management +├── tetromino.h/cpp # Tetromino definitions +├── control.h/cpp # Input handling +├── terminal.h/cpp # Terminal control +├── window.h/cpp # UI rendering +├── draw.h/cpp # Drawing utilities +├── utils.h/cpp # Utility functions +├── color.h # Color definitions +├── define.h # Global definitions +└── tetriz.map # Game map data +``` + +## 🎨 Game Features + +- **7 Standard Tetrominoes**: I, O, T, S, Z, J, L pieces +- **Line Clearing**: Complete lines disappear and award points +- **Level Progression**: Game speed increases with level +- **Scoring System**: Points for line clears and drops +- **Hold Mechanism**: Store one piece for later use +- **Next Queue**: Preview upcoming pieces +- **Game States**: Pause, help, and game over screens +- **Responsive UI**: Adapts to terminal size changes + +## 🔧 Technical Details + +- **Language**: C++20 +- **Build System**: CMake +- **Dependencies**: Standard library only +- **Platform Support**: Linux, Windows +- **Terminal Support**: ANSI escape sequences +- **Threading**: C++20 std::jthread for input handling + +## 📋 Requirements + +### Minimum System Requirements +- **OS**: Linux (any modern distribution) or Windows 10+ +- **RAM**: 64 MB +- **Storage**: 1 MB +- **Terminal**: ANSI-compatible terminal emulator + +### Development Requirements +- **Compiler**: C++20 compatible (GCC 10+, Clang 12+, MSVC 2019+) +- **CMake**: 3.22 or later +- **Build Tools**: Make or Visual Studio Build Tools + +## 🎯 Game Rules + +- **Objective**: Clear horizontal lines by filling them with tetromino pieces +- **Scoring**: + - Single line: 100 × level + - Double lines: 300 × level + - Triple lines: 500 × level + - Tetris (4 lines): 800 × level +- **Level Up**: Every 10 lines cleared increases the level +- **Game Over**: When pieces reach the top of the playfield + +## 🐛 Troubleshooting + +### Common Issues + +1. **Terminal not responding**: Ensure your terminal supports ANSI escape sequences +2. **Build errors**: Check that you have C++20 compatible compiler +3. **Input not working**: Make sure terminal is in focus and supports raw input + +### Getting Help + +If you encounter issues: +1. Check the terminal compatibility +2. Verify build requirements +3. Open an issue with system details + +--- + +**Enjoy playing Tetriz!** 🎮 \ No newline at end of file diff --git a/games/tetriz/terminal.cpp b/games/tetriz/terminal.cpp new file mode 100644 index 00000000..c61fef0f --- /dev/null +++ b/games/tetriz/terminal.cpp @@ -0,0 +1,76 @@ +#include "terminal.h" +#include "define.h" + +#define CSI "\033[" + +namespace tc +{ + + int cols, rows; + int top, left; + + void move_to(int row, int col, std::ostream &os) + { + os << CSI << top + row << ';' << left + col << 'H'; + } + + void set_fore_color(int id, std::ostream &os) + { + os << CSI << "38;5;" << id << 'm'; + } + + void set_back_color(int id, std::ostream &os) + { + os << CSI << "48;5;" << id << 'm'; + } + + void clean_screen(std::ostream &os) + { + os << CSI << "2J"; + } + + void reset_color(std::ostream &os) + { + os << CSI << "0m"; + } + + void hide_cursor(std::ostream &os) + { + os << CSI << "?25l"; + } + + void show_cursor(std::ostream &os) + { + os << CSI << "?25h"; + } + + void set_blod(std::ostream &os) + { + os << CSI << "1m"; + } + + void set_underline(std::ostream &os) + { + os << CSI << "4m"; + } + + std::pair get_size() + { + int columns = 0, rows = 0; +#ifdef _WIN32 + CONSOLE_SCREEN_BUFFER_INFO csbi; + + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); + columns = csbi.srWindow.Right - csbi.srWindow.Left + 1; + rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; +#elif __linux__ + struct winsize w; + ioctl(fileno(stdout), TIOCGWINSZ, &w); + columns = (int)(w.ws_col); + rows = (int)(w.ws_row); +#endif + + return {rows, columns}; + } + +} // namespace tc \ No newline at end of file diff --git a/games/tetriz/terminal.h b/games/tetriz/terminal.h new file mode 100644 index 00000000..36374b45 --- /dev/null +++ b/games/tetriz/terminal.h @@ -0,0 +1,23 @@ +#pragma once +#include "define.h" + +namespace tc +{ // terminal control + + //==================================== + // 屏幕长宽rows,cols + extern int cols, rows; + // 起点坐标top,left + extern int top, left; + //==================================== + void move_to(int row, int col, std::ostream &os = std::cout); + void set_fore_color(int id, std::ostream &os = std::cout); + void set_back_color(int id, std::ostream &os = std::cout); + void clean_screen(std::ostream &os = std::cout); + void reset_color(std::ostream &os = std::cout); + void hide_cursor(std::ostream &os = std::cout); + void show_cursor(std::ostream &os = std::cout); + void set_blod(std::ostream &os = std::cout); + void set_underline(std::ostream &os = std::cout); + std::pair get_size(); +} \ No newline at end of file diff --git a/games/tetriz/tetriz.map b/games/tetriz/tetriz.map new file mode 100644 index 00000000..40d0b9ab --- /dev/null +++ b/games/tetriz/tetriz.map @@ -0,0 +1,20 @@ +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 +0000000000 \ No newline at end of file diff --git a/games/tetriz/tetromino.cpp b/games/tetriz/tetromino.cpp new file mode 100644 index 00000000..e2deee21 --- /dev/null +++ b/games/tetriz/tetromino.cpp @@ -0,0 +1,76 @@ +#include "tetromino.h" + +namespace gm +{ + //------------------------------------------------------------------ + Tetromino I{{ + {{{'I', (int)Color::Cyan}, {-1, 0}, {1, 0}, {2, 0}}}, // 0 + {{{'I', (int)Color::Cyan}, {0, 1}, {0, -1}, {0, -2}}}, // R + {{{'I', (int)Color::Cyan}, {-2, 0}, {-1, 0}, {1, 0}}}, // 2 + {{{'I', (int)Color::Cyan}, {0, 2}, {0, 1}, {0, -1}}}, // L + }}; + Tetromino J{{ + {{{'J', (int)Color::Blue}, {-1, 1}, {-1, 0}, {1, 0}}}, // 0 + {{{'J', (int)Color::Blue}, {1, 1}, {0, 1}, {0, -1}}}, // R + {{{'J', (int)Color::Blue}, {-1, 0}, {1, 0}, {1, -1}}}, // 2 + {{{'J', (int)Color::Blue}, {0, 1}, {-1, -1}, {0, -1}}}, // L + }}; + + Tetromino L{{ + {{{'L', (int)Color::Orange}, {-1, 0}, {1, 0}, {1, 1}}}, // 0 + {{{'L', (int)Color::Orange}, {0, 1}, {0, -1}, {1, -1}}}, // R + {{{'L', (int)Color::Orange}, {-1, -1}, {-1, 0}, {1, 0}}}, // 2 + {{{'L', (int)Color::Orange}, {-1, 1}, {0, 1}, {0, -1}}}, // L + }}; + + Tetromino O{{ + {{{'O', (int)Color::Yellow}, {0, 1}, {1, 1}, {1, 0}}}, // 0 + {{{'O', (int)Color::Yellow}, {0, -1}, {1, 0}, {1, -1}}}, // R + {{{'O', (int)Color::Yellow}, {-1, -1}, {-1, 0}, {0, -1}}}, // 2 + {{{'O', (int)Color::Yellow}, {-1, 1}, {-1, 0}, {0, 1}}}, // L + }}; + + Tetromino S{{ + {{{'S', (int)Color::Green}, {-1, 0}, {0, 1}, {1, 1}}}, // 0 + {{{'S', (int)Color::Green}, {0, 1}, {1, 0}, {1, -1}}}, // R + {{{'S', (int)Color::Green}, {-1, -1}, {0, -1}, {1, 0}}}, // 2 + {{{'S', (int)Color::Green}, {-1, 1}, {-1, 0}, {0, -1}}}, // L + }}; + + Tetromino T{{ + {{{'T', (int)Color::Purple}, {-1, 0}, {0, 1}, {1, 0}}}, // 0 + {{{'T', (int)Color::Purple}, {0, 1}, {1, 0}, {0, -1}}}, // R + {{{'T', (int)Color::Purple}, {-1, 0}, {1, 0}, {0, -1}}}, // 2 + {{{'T', (int)Color::Purple}, {-1, 0}, {0, 1}, {0, -1}}}, // L + }}; + + Tetromino Z{{ + {{{'Z', (int)Color::Red}, {-1, 1}, {0, 1}, {1, 0}}}, // 0 + {{{'Z', (int)Color::Red}, {1, 1}, {1, 0}, {0, -1}}}, // R + {{{'Z', (int)Color::Red}, {-1, 0}, {0, -1}, {1, -1}}}, // 2 + {{{'Z', (int)Color::Red}, {-1, -1}, {-1, 0}, {0, 1}}}, // L + }}; + + + Offset offset{{ + {{{0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}}}, // 0 + {{{0, 0}, {+1, 0}, {+1, -1}, {0, +2}, {+1, +2}}}, // R + {{{0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}}}, // 2 + {{{0, 0}, {-1, 0}, {-1, -1}, {0, +2}, {-1, +2}}}, // L + }}; + + Offset offset_i{{ + {{{0, 0}, {-1, 0}, {+2, 0}, {-1, 0}, {+2, 0}}}, // 0 + {{{-1, 0}, {0, 0}, {0, 0}, {0, +1}, {0, -2}}}, // R + {{{-1, +1}, {+1, +1}, {-2, +1}, {+1, 0}, {-2, 0}}}, // 2 + {{{0, +1}, {0, +1}, {0, +1}, {0, -1}, {0, +2}}}, // L + }}; + + Offset offset_o{{ + {{{0, 0}}}, // 0 + {{{0, -1}}}, // R + {{{-1, -1}}}, // 2 + {{{-1, 0}}}, // L + }}; + +} // namespace gm diff --git a/games/tetriz/tetromino.h b/games/tetriz/tetromino.h new file mode 100644 index 00000000..fae62e98 --- /dev/null +++ b/games/tetriz/tetromino.h @@ -0,0 +1,9 @@ +#pragma once +#include "define.h" +#include "color.h" +namespace gm +{ + extern Tetromino I, J, L, O, S, T, Z; + extern Offset offset,offset_i,offset_o; + +} // namespace gm diff --git a/games/tetriz/utils.cpp b/games/tetriz/utils.cpp new file mode 100644 index 00000000..ea688948 --- /dev/null +++ b/games/tetriz/utils.cpp @@ -0,0 +1,42 @@ +#include "utils.h" +#include "define.h" + +namespace ut +{ + int fps() + { + static auto start = std::chrono::steady_clock::now(); + static int frame_count = 0; + static int fps = 0; + + auto end = std::chrono::steady_clock::now(); + frame_count++; + if (end - start > 1s) + { + fps = frame_count; + + frame_count = 0; + start = end; + } + return fps; + } + + std::string utf32_to_utf8(std::u32string str) + { + static std::wstring_convert, char32_t> convert; + return convert.to_bytes(str); + } + + bool timer(std::chrono::microseconds sec) + { + static auto start = std::chrono::steady_clock::now(); + auto end = std::chrono::steady_clock::now(); + if (end - start > sec) + { + start = end; + return true; + } + return false; + } + +} // namespace ut \ No newline at end of file diff --git a/games/tetriz/utils.h b/games/tetriz/utils.h new file mode 100644 index 00000000..00c0635f --- /dev/null +++ b/games/tetriz/utils.h @@ -0,0 +1,14 @@ +#pragma once +#include "define.h" +namespace ut{ + + int fps(); + std::string utf32_to_utf8(std::u32string str); + + inline int b2c(int b) + { + return 2 * b - 1; + } + + bool timer(std::chrono::microseconds sec); +} \ No newline at end of file diff --git a/games/tetriz/window.cpp b/games/tetriz/window.cpp new file mode 100644 index 00000000..5b5af98a --- /dev/null +++ b/games/tetriz/window.cpp @@ -0,0 +1,150 @@ +#include "define.h" +#include "window.h" +#include "draw.h" +#include "game.h" +#include "terminal.h" +#include "utils.h" +#include "control.h" + +namespace ui +{ + + void window_init() + { +#ifdef _WIN32 + system("chcp 65001"); +#endif + + tc::hide_cursor(); + tc::clean_screen(); + + auto [r, c] = tc::get_size(); + tc::cols = c; + tc::rows = r; + tc::top = (tc::rows - 24) / 2; + tc::left = (tc::cols - 29 * 2) / 2; + } + + void window_exit() + { + tc::show_cursor(); + tc::reset_color(); + } + + void show_windows() + { + dw::window(1, 1, 9, 6, "Hold"); + dw::window(1, 10, 12, 24, "Tetriz"); + dw::window(7, 1, 9, 16, "Status"); + dw::window(19, 22, 8, 4, "Info"); + dw::window(1, 22, 8, 18, "Next"); + } + void show_info() + { + std::ostringstream oss; + tc::reset_color(oss); + + tc::move_to(10, 4, oss); + oss << "Level:" <