2048.cpp是一个开源的游戏项目,通过键盘的操纵进行数据的合并最终达到2048的积分。在源代码中涉及了基本的C++知识,函数对象,lambda表达式,容器(vector,map)以及类模版和函数模版。处于C++入门阶段且阅读了《C++ Primer》的同学是能够将整个项目通读下来的,能够帮助查漏补缺,积累代码量和C++编程的知识。阅读完2048.cpp后可以阅读另一个开源项目 jsoncpp,在笔者的另一篇博客中有提及。阅读完这两个开源项目基本上C++的主要知识其实掌握的也就差不多了。
游戏截图如下:

1、下载和编译
1、项目介绍
克隆此项目:git clone https://github.com/plibither8/2048.cpp
进入项目文件夹:cd 2048.cpp
使用CMake构建,构建可执行文件并运行测试
ctest -S setup.cmake 或者ctest -VV -S setup.cmake:显示详细编译过程
安装程序(可选)
cmake --install build
运行程序,开始游戏吧!
2048 # 如果未安装游戏则运行 `build/2048`
2、游戏规则:
2048.cpp的规则如下:
游戏界面:2048.cpp在一个4x4的方格中进行,玩家需要通过键盘操作合并相同的数字方块。
初始状态:游戏开始时,方格中会随机出现两个数字,通常是2或4。
操作方式:玩家可以通过键盘的W、A、S、D键分别控制上、左、下、右移动方块。当两个相同的数字方块相遇时,它们会合并成一个新的方块,新方块的大小是原来两个方块大小的和。
数字生成:每次滑动方块后,系统会在空白格中随机生成一个新的数字,通常是2或4。
游戏结束:如果方格被数字填满且无法再进行任何操作,或者合并出2048这个数字,游戏结束。
游戏策略和技巧:
合并策略:尽量让相同的数字方块靠近并合并,避免方格被填满。
数字生成规律:每次滑动后,系统在空白格随机生成数字,通常是2或4,玩家需要合理利用这个机制来创造合并机会。
终局条件:当4x4网格填满且无相邻相同数字时游戏结束;当界面中最大数字达到2048时,游戏胜利。
3、源码分析
2048.cpp这个项目的代码量还是有的。其中涉及到棋盘,棋子的绘制, 棋子的移动,得分的统计,颜色的绘制等等。这个项目的作者故意在源码中使用了很多C++11的特性以便增加读者对C++11的熟悉度。比如函数对象,就是一个类实现了函数调用运算符(),那么使用这个类就可以向使用函数一样调用该类,在函数调用运算符的实现中来处理涉及本类的业务,源码中使用了很多的函数对象。作者也使用了大量的tuple来组织元数据,这样就避免了定义过多的数据结构以及大量的lambda表达式来完成局部的计算的作用避免定义过多的函数。
因为这是开源的项目,大家可以随意的修改后进行编译来解决阅读过程中的疑惑。
下面张贴部分关键的注释过的源码:
game-graphics.cpp内容如下:
cpp
#include "game-graphics.hpp"
#include "color.hpp"
#include "global.hpp"
#include <array>
#include <iomanip>
#include <sstream>
/*两个命名空间*/
namespace Game {
namespace Graphics {
std::string AsciiArt2048() {
/*
constexpr表示编译期常量
R"(...)"是原始字符串语法,保留所有格式(包括换行和反斜杠),避免转义产生意想不到的结果
*/
constexpr auto title_card_2048 = R"(
/\\\\\\\\\ /\\\\\\\ /\\\ /\\\\\\\\\
/\\\///////\\\ /\\\/////\\\ /\\\\\ /\\\///////\\\
\/// \//\\\ /\\\ \//\\\ /\\\/\\\ \/\\\ \/\\\
/\\\/ \/\\\ \/\\\ /\\\/\/\\\ \///\\\\\\\\\/
/\\\// \/\\\ \/\\\ /\\\/ \/\\\ /\\\///////\\\
/\\\// \/\\\ \/\\\ /\\\\\\\\\\\\\\\\ /\\\ \//\\\
/\\\/ \//\\\ /\\\ \///////////\\\// \//\\\ /\\\
/\\\\\\\\\\\\\\\ \///\\\\\\\/ \/\\\ \///\\\\\\\\\/
\/////////////// \/////// \/// \/////////
)";
/*使用ostringstream来处理string字符串*/
std::ostringstream title_card_richtext;
title_card_richtext << green << bold_on << title_card_2048 << bold_off << def;
title_card_richtext << "\n\n\n"; /*添加3个换行字符*/
return title_card_richtext.str(); /*返回ostringstream流中的内容*/
}
std::string BoardInputPrompt() {
const auto board_size_prompt_text = {
"(NOTE: Scores and statistics will be saved only for the 4x4 gameboard)\n",
"Enter gameboard size - (Enter '0' to go back): "};
constexpr auto sp = " ";
std::ostringstream board_size_prompt_richtext;
board_size_prompt_richtext
<< bold_on << sp << std::begin(board_size_prompt_text)[0] << sp
<< std::begin(board_size_prompt_text)[1] << bold_off;
return board_size_prompt_richtext.str();
}
std::string YouWinPrompt() {
constexpr auto win_game_text = "You win! Congratulations!";
constexpr auto sp = " ";
std::ostringstream win_richtext;
win_richtext << green << bold_on << sp << win_game_text << def << bold_off
<< "\n\n\n";
return win_richtext.str();
}
std::string GameOverPrompt() {
constexpr auto lose_game_text = "Game over! You lose.";
constexpr auto sp = " ";
std::ostringstream lose_richtext;
lose_richtext << red << bold_on << sp << lose_game_text << def << bold_off
<< "\n\n\n";
return lose_richtext.str();
}
std::string EndOfEndlessPrompt() {
constexpr auto endless_mode_text =
"End of endless mode! Thank you for playing!";
constexpr auto sp = " ";
std::ostringstream endless_mode_richtext;
endless_mode_richtext << red << bold_on << sp << endless_mode_text << def
<< bold_off << "\n\n\n";
return endless_mode_richtext.str();
}
std::string QuestionEndOfWinningGamePrompt() {
constexpr auto win_but_what_next =
"You Won! Continue playing current game? [y/n]";
constexpr auto sp = " ";
std::ostringstream win_richtext;
win_richtext << green << bold_on << sp << win_but_what_next << def << bold_off
<< ": ";
return win_richtext.str();
}
std::string GameStateNowSavedPrompt() {
constexpr auto state_saved_text =
"The game has been saved. Feel free to take a break.";
constexpr auto sp = " ";
std::ostringstream state_saved_richtext;
state_saved_richtext << green << bold_on << sp << state_saved_text << def
<< bold_off << "\n\n";
return state_saved_richtext.str();
}
std::string GameBoardNoSaveErrorPrompt() {
constexpr auto no_save_found_text =
"No saved game found. Starting a new game.";
constexpr auto sp = " ";
std::ostringstream no_save_richtext;
no_save_richtext << red << bold_on << sp << no_save_found_text << def
<< bold_off << "\n\n";
return no_save_richtext.str();
}
std::string InvalidInputGameBoardErrorPrompt() {
constexpr auto invalid_prompt_text = "Invalid input. Please try again.";
constexpr auto sp = " ";
std::ostringstream invalid_prompt_richtext;
invalid_prompt_richtext << red << sp << invalid_prompt_text << def << "\n\n";
return invalid_prompt_richtext.str();
}
std::string BoardSizeErrorPrompt() {
const auto invalid_prompt_text = {
"Invalid input. Gameboard size should range from ", " to ", "."};
// constexpr auto num_of_invalid_prompt_text = 3;
constexpr auto sp = " ";
std::ostringstream error_prompt_richtext;
error_prompt_richtext << red << sp << std::begin(invalid_prompt_text)[0]
<< MIN_GAME_BOARD_PLAY_SIZE
<< std::begin(invalid_prompt_text)[1]
<< MAX_GAME_BOARD_PLAY_SIZE
<< std::begin(invalid_prompt_text)[2] << def << "\n\n";
return error_prompt_richtext.str();
}
/**
* @brief Generates a string prompt listing the available input commands.
*
* This function creates a formatted string that lists the available input commands for the game.
* The commands include movements (Up, Left, Down, Right), saving the game, and returning to the menu.
* The prompt is formatted with indentation for readability.
*
*
*
* @return std::string A formatted string containing the list of input commands.
*/
std::string InputCommandListPrompt() {
constexpr auto sp = " ";
const auto input_commands_list_text = {
"W or K or ↑ => Up", "A or H or ← => Left", "S or J or ↓ => Down",
"D or L or → => Right", "Z or P => Save", "M => Return to menu"};
std::ostringstream str_os;
for (const auto txt : input_commands_list_text) {
str_os << sp << txt << "\n";
}
/*
显示控制按键:
W or K or ↑ => Up
A or H or ← => Left
S or J or ↓ => Down
D or L or → => Right
Z or P => Save
M => Return to menu
*/
return str_os.str();
}
std::string EndlessModeCommandListPrompt() {
constexpr auto sp = " ";
const auto endless_mode_list_text = {"X => Quit Endless Mode"};
std::ostringstream str_os;
for (const auto txt : endless_mode_list_text) {
str_os << sp << txt << "\n";
}
return str_os.str();
}
std::string InputCommandListFooterPrompt() {
constexpr auto sp = " ";
const auto input_commands_list_footer_text = {
"", "Press the keys to start and continue.", "\n"};
std::ostringstream str_os;
for (const auto txt : input_commands_list_footer_text) {
str_os << sp << txt << "\n";
}
/*
显示:
Press the keys to start and continue.
*/
return str_os.str();
}
/*得分板显示*/
std::string GameScoreBoardBox(scoreboard_display_data_t scdd) {
std::ostringstream str_os;
constexpr auto score_text_label = "SCORE:";
constexpr auto bestscore_text_label = "BEST SCORE:";
constexpr auto moves_text_label = "MOVES:";
// * border padding: vvv
// | l-outer: 2, r-outer: 0
// | l-inner: 1, r-inner: 1
// * top border / bottom border: vvv
// | tl_corner + horizontal_sep + tr_corner = length: 1 + 27 + 1
// | bl_corner + horizontal_sep + br_corner = length: 1 + 27 + 1
enum {
UI_SCOREBOARD_SIZE = 27,
UI_BORDER_OUTER_PADDING = 2,
UI_BORDER_INNER_PADDING = 1
}; // length of horizontal board - (corners + border padding)
constexpr auto border_padding_char = ' ';
constexpr auto vertical_border_pattern = "│";
constexpr auto top_board =
"┌───────────────────────────┐"; // Multibyte character set
constexpr auto bottom_board =
"└───────────────────────────┘"; // Multibyte character set
const auto outer_border_padding =
std::string(UI_BORDER_OUTER_PADDING, border_padding_char);
const auto inner_border_padding =
std::string(UI_BORDER_INNER_PADDING, border_padding_char);
const auto inner_padding_length =
UI_SCOREBOARD_SIZE - (std::string{inner_border_padding}.length() * 2);
enum ScoreBoardDisplayDataFields {
IDX_COMPETITION_MODE,
IDX_GAMEBOARD_SCORE,
IDX_BESTSCORE,
IDX_MOVECOUNT,
MAX_SCOREBOARDDISPLAYDATA_INDEXES
};
const auto competition_mode = std::get<IDX_COMPETITION_MODE>(scdd);
const auto gameboard_score = std::get<IDX_GAMEBOARD_SCORE>(scdd);
const auto temp_bestscore = std::get<IDX_BESTSCORE>(scdd);
const auto movecount = std::get<IDX_MOVECOUNT>(scdd);
str_os << outer_border_padding << top_board << "\n";
str_os << outer_border_padding << vertical_border_pattern
<< inner_border_padding << bold_on << score_text_label << bold_off
<< std::string(inner_padding_length -
std::string{score_text_label}.length() -
gameboard_score.length(),
border_padding_char)
<< gameboard_score << inner_border_padding << vertical_border_pattern
<< "\n";
if (competition_mode) {
str_os << outer_border_padding << vertical_border_pattern
<< inner_border_padding << bold_on << bestscore_text_label
<< bold_off
<< std::string(inner_padding_length -
std::string{bestscore_text_label}.length() -
temp_bestscore.length(),
border_padding_char)
<< temp_bestscore << inner_border_padding << vertical_border_pattern
<< "\n";
}
str_os << outer_border_padding << vertical_border_pattern
<< inner_border_padding << bold_on << moves_text_label << bold_off
<< std::string(inner_padding_length -
std::string{moves_text_label}.length() -
movecount.length(),
border_padding_char)
<< movecount << inner_border_padding << vertical_border_pattern
<< "\n";
str_os << outer_border_padding << bottom_board << "\n \n";
return str_os.str();
}
std::string GameScoreBoardOverlay(scoreboard_display_data_t scdd) {
std::ostringstream str_os;
DrawAlways(str_os, DataSuppliment(scdd, GameScoreBoardBox));
return str_os.str();
}
std::string GameEndScreenOverlay(end_screen_display_data_t esdd) {
enum EndScreenDisplayDataFields {
IDX_FLAG_WIN,
IDX_FLAG_ENDLESS_MODE,
MAX_ENDSCREENDISPLAYDATA_INDEXES
};
const auto did_win = std::get<IDX_FLAG_WIN>(esdd);
const auto is_endless_mode = std::get<IDX_FLAG_ENDLESS_MODE>(esdd);
std::ostringstream str_os;
const auto standardWinLosePrompt = [=] {
std::ostringstream str_os;
DrawOnlyWhen(str_os, did_win, YouWinPrompt);
// else..
DrawOnlyWhen(str_os, !did_win, GameOverPrompt);
return str_os.str();
};
DrawOnlyWhen(str_os, !is_endless_mode, standardWinLosePrompt);
// else..
DrawOnlyWhen(str_os, is_endless_mode, EndOfEndlessPrompt);
return str_os.str();
}
std::string GameInputControlsOverlay(input_controls_display_data_t gamestatus) {
const auto is_in_endless_mode = std::get<0>(gamestatus);
const auto is_in_question_mode = std::get<1>(gamestatus);
std::ostringstream str_os;
/*lambda表达式,值捕获*/
const auto InputControlLists = [=] {
std::ostringstream str_os;
/*
显示控制按键:
W or K or ↑ => Up
A or H or ← => Left
S or J or ↓ => Down
D or L or → => Right
Z or P => Save
M => Return to menu
*/
DrawAlways(str_os, Graphics::InputCommandListPrompt);
/*
X => Quit Endless Mode
*/
DrawOnlyWhen(str_os, is_in_endless_mode,
Graphics::EndlessModeCommandListPrompt);
/*
显示开始游戏提示:
Press the keys to start and continue.
*/
DrawAlways(str_os, Graphics::InputCommandListFooterPrompt);
return str_os.str();
};
// When game is paused to ask a question, hide regular inut prompts..
/*执行lambda表达式显示其他内容*/
DrawOnlyWhen(str_os, !is_in_question_mode, InputControlLists);
return str_os.str();
}
} // namespace Graphics
} // namespace Game
game-pregamemenu.cpp内容如下:
cpp
#include "game-pregamemenu.hpp"
#include "game-graphics.hpp"
#include "game-input.hpp"
#include "game.hpp"
#include "gameboard.hpp"
#include "global.hpp"
#include "loadresource.hpp"
#include "menu.hpp"
#include <array>
#include <iostream>
#include <limits>
#include <sstream>
namespace Game {
namespace PreGameSetup {
namespace {
enum PreGameSetupStatusFlag {
FLAG_NULL,
FLAG_START_GAME,
FLAG_RETURN_TO_MAIN_MENU,
MAX_NO_PREGAME_SETUP_STATUS_FLAGS
};
using pregamesetup_status_t =
std::array<bool, MAX_NO_PREGAME_SETUP_STATUS_FLAGS>;
pregamesetup_status_t pregamesetup_status{};
ull stored_game_size{1};
/*全局变量*/
bool FlagInputErrornousChoice{};
bool noSave{};
void process_PreGameMenu() {
if (pregamesetup_status[FLAG_START_GAME]) {
playGame(PlayGameFlag::BrandNewGame, GameBoard{stored_game_size}/*调用GameBoard的委托构造函数初始化棋盘*/,
stored_game_size /*棋盘大小*/);
}
if (pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU]) {
/*清屏,重新显示开始界面*/
Menu::startMenu();
}
}
/*处理用户输入的gameboard size*/
int Receive_Input_Playsize(std::istream &is) {
int userInput_PlaySize{};
if (!(is >> userInput_PlaySize)) {
/*输入出错的情况*/
constexpr auto INVALID_INPUT_VALUE_FLAG = -1;
userInput_PlaySize = INVALID_INPUT_VALUE_FLAG;
is.clear(); /*清除错误*/
is.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); /*忽略剩下的内容*/
}
return userInput_PlaySize;
}
void receive_input_flags(std::istream &is) {
using namespace Input::Keypress::Code;
// Reset ErrornousChoice flag...
FlagInputErrornousChoice = bool{}; /*false*/
// If not number, emits -1.
// it should emit bool (valid / invalid value) and a valid number if ok input.
// WARN:: "0-3" breaks this code!
/*返回用户输入的棋盘尺寸*/
const auto c = Receive_Input_Playsize(is);
const auto is_valid_game_size =
(c >= MIN_GAME_BOARD_PLAY_SIZE) && (c <= MAX_GAME_BOARD_PLAY_SIZE);
// Regular case;
if (is_valid_game_size) {
stored_game_size = c;
pregamesetup_status[FLAG_START_GAME] = true;
}
// Special Case;
bool is_not_special_case{};
switch (c) {
case CODE_HOTKEY_PREGAMEMENU_BACK_TO_MAINMENU:
pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU] = true;
break;
default:
is_not_special_case = true;
break;
}
/*1,1*/
std::cout << is_valid_game_size << ',' << is_not_special_case << std::endl;
if (!is_valid_game_size && is_not_special_case) {
FlagInputErrornousChoice = true;
}
}
bool soloLoop() {
bool invalidInputValue = FlagInputErrornousChoice;
/*这里是一个lambda表达式,用于处理gameboard size的输入*/
const auto QuestionAboutBoardSizePrompt = [&invalidInputValue]() {
std::ostringstream str_os;
/*
只有invalidInputValue为真的情况下才输出Invalid input. Gameboard size should range from 3 to 10.
*/
DrawOnlyWhen(str_os, invalidInputValue, Graphics::BoardSizeErrorPrompt);
/*
输出提示内容如下:
(NOTE: Scores and statistics will be saved only for the 4x4 gameboard)
Enter gameboard size - (Enter '0' to go back):
*/
DrawAlways(str_os, Graphics::BoardInputPrompt);
return str_os.str();
};
pregamesetup_status = pregamesetup_status_t{};
/*清屏幕,因为用户的输入有效,需要展现新的界面*/
clearScreen();
/*打印2048艺术字*/
DrawAlways(std::cout, Game::Graphics::AsciiArt2048);
/*打印 No saved game found. Starting a new game.*/
DrawAsOneTimeFlag(std::cout, noSave, Graphics::GameBoardNoSaveErrorPrompt);
/*执行lambda表达式,打印输入gameboard size*/
DrawAlways(std::cout, QuestionAboutBoardSizePrompt);
/*接受棋盘尺寸输入*/
receive_input_flags(std::cin);
process_PreGameMenu();
return FlagInputErrornousChoice;
}
void endlessLoop() {
while (soloLoop())
;
}
enum class NewGameFlag { NewGameFlagNull, NoPreviousSaveAvailable };
void SetUpNewGame(NewGameFlag ns) {
noSave = (ns == NewGameFlag::NoPreviousSaveAvailable) ? true : false;
endlessLoop();
}
/**
* @brief Initializes a GameBoard object by loading game data from a specified file.
*
* This function constructs the full path to the game data file by prepending "../data/" to the given filename.
* It then calls the `load_game` function to load the game data into a new GameBoard object. The function returns
* a tuple containing the status of the game load operation (true if successful, false otherwise) and the initialized
* GameBoard object.
*
* @param filename The name of the file from which to read the game data.
* @return A tuple containing a boolean indicating the success of the load operation and the initialized GameBoard object.
*/
load_gameboard_status_t initialiseContinueBoardArray(const std::string& filename)
{
using namespace Loader;
// const auto gameboard_data_filename = "../data/" + filename;
auto gb = GameBoard{1};
auto loaded_game = load_game(filename, gb);
return std::make_tuple(loaded_game, gb);
}
/**
* @brief Continues a previously saved game from a specified file.
*
* This function attempts to load the game state from the provided filename.
* If successful, it continues the game using the loaded state. If the loading
* fails, it sets up a new game indicating that no previous save is available.
*
* @param filename The name of the file from which to load the previous game state.
*/
void DoContinueOldGame(const std::string& filename) {
bool load_old_game_ok;
GameBoard oldGameBoard;
std::tie(load_old_game_ok, oldGameBoard) = initialiseContinueBoardArray(filename);
if (load_old_game_ok) {
playGame(PlayGameFlag::ContinuePreviousGame, oldGameBoard);
} else {
SetUpNewGame(NewGameFlag::NoPreviousSaveAvailable);
}
}
} // namespace
void SetUpNewGame() {
/*重载函数*/
SetUpNewGame(NewGameFlag::NewGameFlagNull);
}
/**
* @brief Continue a previously saved game.
*
* The ContinueOldGame function has been updated to accept a filename directly.
* This allows the user to load a specific save file instead of only the last saved game.
*
* @param filename The name of the file containing the saved game to load.
*/
void ContinueOldGame(const std::string& filename) {
DoContinueOldGame(filename);
}
} // namespace PreGameSetup
} // namespace Game
gameboard.cpp内容如下:
cpp
#include "gameboard.hpp"
#include "gameboard-graphics.hpp"
#include "point2d.hpp"
#include <algorithm>
#include <array>
#include <chrono>
#include <random>
#include <sstream>
#include <utility>
namespace Game {
namespace {
/*在范围内产生均匀的随机数*/
class RandInt {
/*chrono是C++中的一个时间库*/
public:
using clock = std::chrono::system_clock;
/*无参构造函数,从0到整形最大数之间*/
RandInt() : dist{0, std::numeric_limits<int>::max()} {
seed(clock::now().time_since_epoch().count());
}
RandInt(const int low, const int high) : dist{low, high} {
seed(clock::now().time_since_epoch().count());
}
/*实现了函数调用运算符,那么该类就是一个函数对象*/
int operator()() { return dist(re); }
void seed(const unsigned int s) { re.seed(s); }
private:
/*
std::minstd_rand 是C++ <random>库中的一个伪随机数生成引擎,实现了线性同余生成器(LCG),是简单高效的轻量级随机数生成器。
*/
std::minstd_rand re;
/*
std::uniform_int_distribution,用于生成指定范围内的均匀分布随机整数,比传统rand() % N更均匀(避免模偏差)
template<class IntType = int>
class uniform_int_distribution;
*/
std::uniform_int_distribution<> dist;
};
using gameboard_data_array_t = GameBoard::gameboard_data_array_t;
enum gameboard_data_array_fields { IDX_PLAYSIZE, IDX_BOARD, MAX_NO_INDEXES };
/*这个结果体中全部都是成员函数*/
struct gameboard_data_point_t {
/*从坐标值转换为在vector<tile_t>中的下标位置[]*/
static int point2D_to_1D_index(gameboard_data_array_t gbda, point2D_t pt) {
int x, y;
std::tie(x, y) = pt.get();
return x + getPlaySizeOfGameboardDataArray(gbda) * y;
}
/*
实现函数调用运算法,函数对象.
返回坐标位置对应的vector<tile_t>中位置
*/
/*注意,实现了两个版本的函数调用运算符*/
tile_t operator()(gameboard_data_array_t gbda, point2D_t pt) const {
return std::get<IDX_BOARD>(gbda)[point2D_to_1D_index(gbda, pt)];
}
tile_t &operator()(gameboard_data_array_t &gbda, point2D_t pt) {
return std::get<IDX_BOARD>(gbda)[point2D_to_1D_index(gbda, pt)];
}
};
/*下面的两个函数非常的巧妙*/
void setTileOnGameboardDataArray(gameboard_data_array_t &gbda, point2D_t pt,
tile_t tile) {
/*
使用匿名对象来调用该类类型的函数调用运算符,设置坐标位置棋子的值
*/
gameboard_data_point_t{}(gbda, pt) = tile;
}
ull getTileValueOnGameboardDataArray(gameboard_data_array_t gbda,
point2D_t pt) {
/*
使用匿名对象来调用该类类型的函数调用运算符,返回坐标位置棋子的值
*/
return gameboard_data_point_t{}(gbda, pt).value;
}
/*设置pt对应棋子的值*/
void setTileValueOnGameboardDataArray(gameboard_data_array_t &gbda,
point2D_t pt, ull value) {
gameboard_data_point_t{}(gbda, pt).value = value;
}
bool getTileBlockedOnGameboardDataArray(gameboard_data_array_t gbda,
point2D_t pt) {
return gameboard_data_point_t{}(gbda, pt).blocked;
}
/**
* @brief Generates a string representation of the game board data array.
*
* This function creates a formatted string that represents the current state of the game board.
* It includes the tile values and their blocked status for each position on the board.
* The string representation ends with a "[" character to indicate the end of the data.
*
* @param gbda The game board data array to be printed.
* @return std::string A formatted string representing the game board state.
*
* @note Changes in the new version:
* - Added a "[" character at the end of the string to indicate the end of the data.
*/
std::string printStateOfGameBoardDataArray(gameboard_data_array_t gbda) {
const int playsize = getPlaySizeOfGameboardDataArray(gbda);
std::ostringstream os;
for (auto y = 0; y < playsize; y++) {
for (auto x = 0; x < playsize; x++) {
const auto current_point = point2D_t{x, y};
os << getTileValueOnGameboardDataArray(gbda, current_point) << ":"
<< getTileBlockedOnGameboardDataArray(gbda, current_point) << ",";
}
os << "\n";
}
os << "["; // Indicates the end of the game board data
return os.str();
}
bool is_point_in_board_play_area(point2D_t pt, int playsize) {
int x, y;
std::tie(x, y) = pt.get();
return !(y < 0 || y > playsize - 1 || x < 0 || x > playsize - 1);
}
using delta_t = std::pair<point2D_t, point2D_t>;
// NOTE: delta_t.first = focal point, delta_t.second = offset distance
bool check_recursive_offset_in_game_bounds(delta_t dt_point, int playsize) {
int x, y, x2, y2;
std::tie(x, y) = dt_point.first.get(); /*当前棋子位置*/
std::tie(x2, y2) = dt_point.second.get(); /*移动位置*/
const auto positive_direction = (y2 + x2 == 1); /*下或者右移动*/
const auto negative_direction = (y2 + x2 == -1); /*上或者左移动*/
const auto is_positive_y_direction_flagged = (y2 == 1);
const auto is_negative_y_direction_flagged = (y2 == -1);
const auto is_inside_outer_bounds =
(positive_direction &&
(is_positive_y_direction_flagged ? y : x) < playsize - 2); /*下或者右移动时x或者y的值在变大,所以要判断其值*/
const auto is_inside_inner_bounds =
(negative_direction && (is_negative_y_direction_flagged ? y : x) > 1); /*上或者左移动时x或者y的值在变小,所以要判断其值*/
return (is_inside_outer_bounds || is_inside_inner_bounds);
}
gameboard_data_array_t
unblockTilesOnGameboardDataArray(gameboard_data_array_t gbda) {
using tile_data_array_t = GameBoard::tile_data_array_t;
/*新的vector<tile_t>类型,注意这是个局部变量*/
auto new_board_data_array =
tile_data_array_t(std::get<IDX_BOARD>(gbda).size()); /*使用棋盘宽度来初始化vector<tile_t>*/
/*
std::transform是C++标准库<algorithm>头文件中的一个重要算法,用于对容器中的元素进行转换操作
将gbda棋盘中的棋子tile_t通过lambda表达式将blocked设置为false处理后存放到新的vector<tile_t>中
为什么要这么操作呢?????
*/
std::transform(std::begin(std::get<IDX_BOARD>(gbda)),
std::end(std::get<IDX_BOARD>(gbda)),
std::begin(new_board_data_array), [](const tile_t t)/*lambda表达式*/ {
return tile_t{t.value, false};
});
/*返回新组成的棋盘*/
return gameboard_data_array_t{std::get<IDX_PLAYSIZE>(gbda),
new_board_data_array};
}
/*棋盘中是否存在可以移动的空位*/
bool canMoveOnGameboardDataArray(gameboard_data_array_t gbda) {
auto index_counter{0};
/*
lambda表达式,这个表达式很长
index_counter为引用捕获,其他为值捕获
针对棋盘中的每个棋子调用can_move_to_offset 表达式
*/
const auto can_move_to_offset = [=, &index_counter](const tile_t t/*传入的棋盘棋子*/) {
const int playsize = getPlaySizeOfGameboardDataArray(gbda); /*棋盘尺寸*/
/*根据index_counter构造一个棋子current_point*/
const auto current_point =
point2D_t{index_counter % playsize, index_counter / playsize};
index_counter++;
/*x轴,y轴变化一个位置*/
const auto list_of_offsets = {point2D_t{1, 0}, point2D_t{0, 1}};
const auto current_point_value = t.value;
/*
labda表达式
=,值捕获
current_point在offset范围内的棋子的值是否等于 current_point_value 的值
*/
const auto offset_in_range_with_same_value = [=](const point2D_t offset) {
const auto offset_check = {
current_point + offset, // Positive adjacent check
current_point - offset}; // Negative adjacent Check
// Use reference to avoid unnecessary copying of complex structures
for (const auto ¤t_offset : offset_check) {
/*当前位置是否在棋盘范围内*/
if (is_point_in_board_play_area(current_offset, playsize)) {
/*获取棋子在棋盘中的值*/
return getTileValueOnGameboardDataArray(gbda, current_offset) ==
current_point_value;
}
}
return false;
};
return ((current_point_value == 0u) /*存在空位,肯定可以移动*/ ||
std::any_of(std::begin(list_of_offsets), std::end(list_of_offsets),
offset_in_range_with_same_value));
};
/*
std::any_of是C++标准库<algorithm>头文件中的一个算法,用于检查指定范围内是否至少有一个元素满足给定条件。
基本语法:
template< class InputIt, class UnaryPredicate >
bool any_of( InputIt first, InputIt last, UnaryPredicate p );
核心功能:
检查范围 [first, last) 中是否存在至少一个元素使谓词p返回true
一旦找到符合条件的元素立即返回 true(短路求值)
如果范围为空或没有元素满足条件,则返回 false
*/
/*针对棋盘中的所有棋子调用lambda表达式 can_move_to_offset*/
return std::any_of(std::begin(std::get<IDX_BOARD>(gbda)),
std::end(std::get<IDX_BOARD>(gbda)), can_move_to_offset);
}
/*收集棋盘中空余棋子位置*/
std::vector<size_t>
collectFreeTilesOnGameboardDataArray(gameboard_data_array_t gbda) {
std::vector<size_t> freeTiles;
auto index_counter{0}; //int类型
/*
获取tuple中的成员使用get函数模板来实现
注意,这里的代码中get<>返回的是vector<tile_t>,所以t是vector<tile_t>中的内容
vector<tile_t>里面的内容暂时应该全部是0
*/
for (const auto t : std::get<IDX_BOARD>(gbda)) { /*vector<tile_t>*/
if (!t.value) { //值为0就将index_counter放入到freeTiles中
freeTiles.push_back(index_counter);
}
index_counter++;
}
return freeTiles; /*注意,这里返回的是局部变量*/
}
bool addTileOnGameboardDataArray(gameboard_data_array_t &gbda) {
constexpr auto CHANCE_OF_VALUE_FOUR_OVER_TWO = 89; // Percentage
const auto index_list_of_free_tiles = /*将棋盘方阵中value为0的编号放入到vector<int>中*/
collectFreeTilesOnGameboardDataArray(gbda);
if (!index_list_of_free_tiles.size()) { /*棋盘中无剩于位置则返回*/
return true;
}
/*获取设置的棋盘大小*/
const int playsize = getPlaySizeOfGameboardDataArray(gbda);
/*选择随机位置*/
const int rand_selected_index = index_list_of_free_tiles.at(
RandInt{}()/*使用匿名对象来调用函数调用运算符*/ % index_list_of_free_tiles.size());
/*计算产生的随机数在棋盘中的位置*/
const auto rand_index_as_point_t =
point2D_t{rand_selected_index % playsize, rand_selected_index / playsize};
/*棋子上的数值为4还是2*/
const auto value_four_or_two =
RandInt{}() % 100 > CHANCE_OF_VALUE_FOUR_OVER_TWO ? 4 : 2;
/*设置pt对应棋子的值*/
setTileValueOnGameboardDataArray(gbda, rand_index_as_point_t,value_four_or_two);
return false;
}
/*合并棋子*/
bool collaspeTilesOnGameboardDataArray(gameboard_data_array_t &gbda,
delta_t dt_point) {
tile_t currentTile = getTileOnGameboardDataArray(gbda, dt_point.first);
tile_t targetTile =
getTileOnGameboardDataArray(gbda, dt_point.first + dt_point.second);
currentTile.value = 0;
targetTile.value *= 2; /*完成合并*/
targetTile.blocked = true;
/*更新棋子*/
setTileOnGameboardDataArray(gbda, dt_point.first, currentTile);
setTileOnGameboardDataArray(gbda, dt_point.first + dt_point.second,
targetTile);
return true;
}
/*移动棋子*/
bool shiftTilesOnGameboardDataArray(gameboard_data_array_t &gbda,
delta_t dt_point) {
tile_t currentTile = getTileOnGameboardDataArray(gbda, dt_point.first);
tile_t targetTile =
getTileOnGameboardDataArray(gbda, dt_point.first + dt_point.second);
targetTile.value = currentTile.value;
currentTile.value = 0;
/*更新棋子*/
setTileOnGameboardDataArray(gbda, dt_point.first, currentTile);
setTileOnGameboardDataArray(gbda, dt_point.first + dt_point.second,
targetTile);
return true;
}
enum class COLLASPE_OR_SHIFT_T {
ACTION_NONE,
ACTION_COLLASPE,
ACTION_SHIFT,
MAX_NUM_OF_ACTIONS
};
using bool_collaspe_shift_t = std::tuple<bool, COLLASPE_OR_SHIFT_T>;
bool_collaspe_shift_t
collasped_or_shifted_tilesOnGameboardDataArray(gameboard_data_array_t gbda,
delta_t dt_point) {
/*获取棋子的值*/
const auto currentTile = getTileOnGameboardDataArray(gbda, dt_point.first);
/*获取目标位置棋子的值,用来判断目标位置是否有棋子,无棋子则移动到目标位置*/
const auto targetTile =
getTileOnGameboardDataArray(gbda, dt_point.first + dt_point.second);
const auto does_value_exist_in_target_point = targetTile.value;
const auto is_value_same_as_target_value =
(currentTile.value == targetTile.value);
const auto no_tiles_are_blocked =
(!currentTile.blocked && !targetTile.blocked);
const auto is_there_a_current_value_but_no_target_value =
(currentTile.value && !targetTile.value);
/*做合并操作,因为目标位置和当前棋子值一样可以进行合并*/
const auto do_collapse =
(does_value_exist_in_target_point/*目标位置存在棋子*/ && is_value_same_as_target_value/*目标位置棋子值和移动棋子值相同*/ &&
no_tiles_are_blocked/*未阻塞*/);
/*做移动操作*/
const auto do_shift = is_there_a_current_value_but_no_target_value;
/*采取动作*/
const auto action_taken = (do_collapse || do_shift);
/*返回采取动作的类型*/
if (do_collapse) {
return std::make_tuple(action_taken, COLLASPE_OR_SHIFT_T::ACTION_COLLASPE);
} else if (do_shift) {
return std::make_tuple(action_taken, COLLASPE_OR_SHIFT_T::ACTION_SHIFT);
}
return std::make_tuple(action_taken, COLLASPE_OR_SHIFT_T::ACTION_NONE);
}
bool updateGameBoardStats(GameBoard &gb, ull target_tile_value) {
gb.score += target_tile_value;
// Discover the largest tile value on the gameboard...
gb.largestTile = std::max(gb.largestTile, target_tile_value);
// Discover the winning tile value on the gameboard...
if (!hasWonOnGameboard(gb)) {
constexpr auto GAME_TILE_WINNING_SCORE = 2048;
if (target_tile_value == GAME_TILE_WINNING_SCORE) {
gb.win = true;
}
}
return true;
}
/*移动棋子*/
void moveOnGameboard(GameBoard &gb, delta_t dt_point) {
auto did_gameboard_collaspe_or_shift_anything = bool{};
auto action_was_taken = COLLASPE_OR_SHIFT_T::ACTION_NONE;
/*返回采取动作的类型:合并,移动还是什么都不做*/
std::tie(did_gameboard_collaspe_or_shift_anything, action_was_taken) =
collasped_or_shifted_tilesOnGameboardDataArray(gb.gbda, dt_point);
if (did_gameboard_collaspe_or_shift_anything/*采取行动*/) {
gb.moved = true;
if (action_was_taken == COLLASPE_OR_SHIFT_T::ACTION_COLLASPE) {
/*合并操作:合并后更新棋子位置*/
collaspeTilesOnGameboardDataArray(gb.gbda, dt_point);
/*获取移动后的棋子*/
const auto targetTile = getTileOnGameboardDataArray(
gb.gbda, dt_point.first + dt_point.second);
/*更新最高得分以及检查是否获得胜利(2048分)*/
updateGameBoardStats(gb, targetTile.value);
}
if (action_was_taken == COLLASPE_OR_SHIFT_T::ACTION_SHIFT) {
/*移动操作,将棋子移动到目标位置*/
shiftTilesOnGameboardDataArray(gb.gbda, dt_point);
}
}
/*检查是否能够继续移动*/
if (check_recursive_offset_in_game_bounds(
dt_point, getPlaySizeOfGameboardDataArray(gb.gbda))) {
moveOnGameboard(
gb, std::make_pair(dt_point.first + dt_point.second, dt_point.second));
}
}
/*上移*/
void doTumbleTilesUpOnGameboard(GameBoard &gb) {
const int playsize = getPlaySizeOfGameboardDataArray(gb.gbda);
for (auto x = 0; x < playsize; x++) {
auto y = 1; /*注意y的取值是1,从第二行开始往上移动,即(0,0),(0,1),(0,playsize-1)所在的行不用上移,因为该行就是顶行*/
while (y < playsize) {
const auto current_point = point2D_t{x, y};
/*获取棋子的值*/
if (getTileValueOnGameboardDataArray(gb.gbda, current_point)) {
moveOnGameboard(gb, std::make_pair(current_point, point2D_t{0, -1}));
}
y++;
}
}
}
/*下移*/
void doTumbleTilesDownOnGameboard(GameBoard &gb) {
const int playsize = getPlaySizeOfGameboardDataArray(gb.gbda);
for (auto x = 0; x < playsize; x++) {
auto y = playsize - 2; /*注意y的取值是playsize-2*/
while (y >= 0) {
const auto current_point = point2D_t{x, y};
if (getTileValueOnGameboardDataArray(gb.gbda, current_point)) {
moveOnGameboard(gb, std::make_pair(current_point, point2D_t{0, 1}));
}
y--;
}
}
}
/*左移*/
void doTumbleTilesLeftOnGameboard(GameBoard &gb) {
const int playsize = getPlaySizeOfGameboardDataArray(gb.gbda);
for (auto y = 0; y < playsize; y++) {
auto x = 1;
while (x < playsize) {
const auto current_point = point2D_t{x, y};
if (getTileValueOnGameboardDataArray(gb.gbda, current_point)) {
moveOnGameboard(gb, std::make_pair(current_point, point2D_t{-1, 0}));
}
x++;
}
}
}
/*右移*/
void doTumbleTilesRightOnGameboard(GameBoard &gb) {
const int playsize = getPlaySizeOfGameboardDataArray(gb.gbda);
for (auto y = 0; y < playsize; y++) {
auto x = playsize - 2;
while (x >= 0) {
const auto current_point = point2D_t{x, y};
if (getTileValueOnGameboardDataArray(gb.gbda, current_point)) {
moveOnGameboard(gb, std::make_pair(current_point, point2D_t{1, 0}));
}
x--;
}
}
}
} // namespace
/*委托构造函数:一个形参的构造函数在其参数列表中调用了另一个有两个形参的构造函数*/
GameBoard::GameBoard(ull playsize)
: GameBoard{playsize, tile_data_array_t(playsize * playsize/*这是设置的vector<>的值还是大小数量,这里使用的是()设置的大小数量*/)} {
}
/*该构造函数使用传入的参数来初始化数据成员gbda*/
GameBoard::GameBoard(ull playsize, tile_data_array_t prempt_board)
: gbda{playsize, prempt_board} {
}
/*获取棋盘大小*/
size_t getPlaySizeOfGameboardDataArray(gameboard_data_array_t gbda) {
return std::get<IDX_PLAYSIZE>(gbda);
}
/*返回棋子*/
tile_t getTileOnGameboardDataArray(gameboard_data_array_t gbda, point2D_t pt) {
return gameboard_data_point_t{}(gbda, pt);
}
bool hasWonOnGameboard(GameBoard gb) {
return gb.win;
}
long long MoveCountOnGameBoard(GameBoard gb) {
return gb.moveCount;
}
/*将棋盘中所有棋子tile_t.blocked设置为false后组成新的棋盘*/
void unblockTilesOnGameboard(GameBoard &gb) {
/*重新为棋盘赋值,gb传递的是引用类型*/
gb.gbda = unblockTilesOnGameboardDataArray(gb.gbda);
}
bool canMoveOnGameboard(GameBoard &gb) {
return canMoveOnGameboardDataArray(gb.gbda);
}
/*棋盘中是否存在可以移动的空位*/
void registerMoveByOneOnGameboard(GameBoard &gb) {
gb.moveCount++;
gb.moved = false;
}
/*棋盘中随机放置棋子,随机设置其值*/
bool addTileOnGameboard(GameBoard &gb) {
return addTileOnGameboardDataArray(gb.gbda);
}
/*上*/
void tumbleTilesUpOnGameboard(GameBoard &gb) {
doTumbleTilesUpOnGameboard(gb);
}
/*下*/
void tumbleTilesDownOnGameboard(GameBoard &gb) {
doTumbleTilesDownOnGameboard(gb);
}
/*左*/
void tumbleTilesLeftOnGameboard(GameBoard &gb) {
doTumbleTilesLeftOnGameboard(gb);
}
/*右*/
void tumbleTilesRightOnGameboard(GameBoard &gb) {
doTumbleTilesRightOnGameboard(gb);
}
std::string printStateOfGameBoard(GameBoard gb) {
return printStateOfGameBoardDataArray(gb.gbda);
}
} // namespace Game