2048游戏项目源码阅读、学习、分析、复现
前言 写作目的 记录我对2048游戏项目的学习过程。本项目源自github(传送门 )。
我的工作 阅读项目源码,学习规范编程风格与技巧,梳理代码逻辑,在理解原项目内容后模仿原项目写一个自己的2048游戏。
注意 :后续文章里所涉及代码为按我理解修改后的版本,相较于原项目代码,我的版本部分函数、变量名称不同,注释更多,自认为我的版本更适合阅读学习。
项目介绍 本项目实现2048小游戏。
游戏介绍 玩家每次可以选择上下左右其中一个方向去滑动,每滑动一次,所有的数字方块都会往滑动的方向靠拢外,系统也会在空白的地方随机出现一个数字方块,相同数字的方块在靠拢、相撞时会相加。不断的叠加最终拼凑出2048这个数字就算成功。
游戏特色
提供3 x 3 到 10 x 10 不同规格的棋盘尺寸。
游戏提供保存功能,玩家可随时从之前保存的对局继续游戏。
游戏提供排行榜功能,记录玩家历史最佳成绩(仅限4 x 4规格棋盘对局),统计多项游戏数据。
游戏结束条件 当棋盘被铺满(任意方块均有一个数字值,且所有相邻方块之间数值均不同)时,游戏结束。
胜利条件 游戏结束时,若场上数值最大的方块的值大于等于2048,则玩家胜利,否则失败。
游戏胜利后,玩家可选择继续(进入无尽模式)或是退出。
项目成果预览 静态 菜单
游戏过程
排行榜
动态 新游戏
继续游戏
排行榜
项目构建 阶段一 构建 既然是2048游戏,咱们的项目自然从2048.cpp的main()函数处开始。
1 2 3 4 5 6 7 8 9 #include "menu.hpp" int main () { Menu::startMenu(); return 0 ; }
本.cpp文件包含头文件menu.hpp,其内容为:
1 2 3 4 5 6 7 8 9 #ifndef MENU_H #define MENU_H namespace Menu { void startMenu () ; } #endif
需注意的是,所有的头文件都需要使用上述这种防卫式声明。
我们的menu只向外提供startMenu()一个函数,即提供游戏菜单。于是需要在menu.cpp中实现基础菜单功能。
menu.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include "my_menu.hpp" namespace { bool soloLoop () { } void endlessLoop () { while (soloLoop()) ; } } namespace Menu{ void startMenu () { endlessLoop(); } }
游戏逻辑十分清晰简单:startMenu()函数调用endlessLoop()函数,后者通过while循环无尽地调用soloLoop()函数,而soloLoop()里会做一些事情,例如要求玩家进行输入,做完后返回一个布尔值,决定endlessLoop()是否继续循环下去。例如玩家输入非预期值,则soloLoop()返回true值,循环继续,再次要求玩家输入,否则调用输入对应函数,提供服务。
我们希望能够在soloLoop()中实现以下几个功能:
取一个布尔变量,将其作为函数返回值,该变量记录玩家输入是否为非法值
清空屏幕,供后续打印游戏画面
打印游戏画面,提供选项供用户选择
处理选项
为实现清屏、打印画面等功能,我们设计实现global.hpp,该文件提供一些通用功能供其余所有文件使用。
global.hpp实现如下。
首先,提供一个unsigned long long的缩写ull,供将来使用:
1 using ull = unsigned long long ;
然后是三个与画面呈现相关的函数模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 template <typename T>void DrawAlways (std ::ostream& os, T f) { os << f(); } template <typename T>void DrawOnlyWhen (std ::ostream& os, bool trigger, T f) { if (trigger) { DrawAlways(os, f); } } template <typename T>void DrawAsOneTimeFlag (std ::ostream& os, bool & trigger, T f) { if (trigger) { DrawAlways(os, f); trigger = !trigger; } }
DrawAlways()需要两个参数,一个为输出流os,另一个为一函数f。DrawAlways()调用函数f并将其返回值输出至os。
DrawOnlyWhen()相较于前者多了一个trigger条件值。只有在满足条件的情况下才调用DrawAlways()。
DrawAsOneTimeFlag()同样只在满足条件时调用DrawAlways(),并且在调用后翻转条件值。
再接着,是下面这坨东西:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 template <typename suppliment_t >struct DataSupplimentInternalType { suppliment_t suppliment_data; template <typename function_t > std ::string operator () (function_t f) const { return f(suppliment_data); } }; template <typename suppliment_t , typename function_t >auto DataSuppliment (suppliment_t needed_data, function_t f) { using dsit_t = DataSupplimentInternalType<suppliment_t >; const auto lambda_f_to_return = [=]() { const dsit_t depinject_func = dsit_t { needed_data }; return depinject_func(f); }; return lambda_f_to_return; }
初看有点复杂,分析下来这里其实是使用模板提供了一个通用的接口。DataSuppliment()需要两个参数,第二个参数是一个函数f,第一个参数是f需要的参数。分析知参数f的返回类型必须是std::string。
如果存在某含一个参数的函数如:
通常调用如下:
而通过上述接口可写为:
1 DataSuppliment(2022 , foo);
使用统一接口并没有效率上的提升(实际反而降低了效率),只是为了风格的统一?是否有更深层的含义?我暂时不太清楚,只是沿用原项目做法。
最后,global.hpp还需要提供一个清屏函数供项目其他部分使用:
该函数定义在global.cpp中:
1 2 3 4 5 6 7 8 void clearScreen () {#ifdef _WIN32 system("cls" ); #else system("clear" ); #endif }
由于global.hpp中涉及到了string和ostream,需在开头声明相应头文件。最终成品如下:
global.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #ifndef GLOBAL_H #define GLOBAL_H #include <iosfwd> #include <string> using ull = unsigned long long ;template <typename T>void DrawAlways (std ::ostream& os, T f) { os << f(); } template <typename T>void DrawOnlyWhen (std ::ostream& os, bool trigger, T f) { if (trigger) { DrawAlways(os, f); } } template <typename T>void DrawAsOneTimeFlag (std ::ostream& os, bool & trigger, T f) { if (trigger) { DrawAlways(os, f); trigger = !trigger; } } template <typename suppliment_t >struct DataSupplimentInternalType { suppliment_t suppliment_data; template <typename function_t > std ::string operator () (function_t f) const { return f(suppliment_data); } }; template <typename suppliment_t , typename function_t >auto DataSuppliment (suppliment_t needed_data, function_t f) { using dsit_t = DataSupplimentInternalType<suppliment_t >; const auto lambda_f_to_return = [=]() { const dsit_t depinject_func = dsit_t { needed_data }; return depinject_func(f); }; return lambda_f_to_return; } void clearScreen () ;#endif
global.cpp
1 2 3 4 5 6 7 8 9 10 #include "global.hpp" void clearScreen () {#ifdef _WIN32 system("cls" ); #else system("clear" ); #endif }
有了以上辅助函数、接口后,可进一步完善menu.cpp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 #include "menu.hpp" #include "global.hpp" #include <array> #include <iostream> namespace { enum MenuStatusFlag { FLAG_NULL, FLAG_START_GAME, FLAG_CONTINUE_GAME, FLAG_DISPLAY_HIGHSCORES, FLAG_EXIT_GAME, MAX_NO_MAIN_MENU_STATUS_FLAGS }; using menuStatus_t = std ::array <bool , MAX_NO_MAIN_MENU_STATUS_FLAGS>; menuStatus_t menustatus{}; bool flagInputErroneousChoice{ false }; void receiveInputFromPlayer (std ::istream& in_os) { flagInputErroneousChoice = bool {}; char c; in_os >> c; switch (c) { case '1' : menustatus[FLAG_START_GAME] = true ; break ; case '2' : menustatus[FLAG_CONTINUE_GAME] = true ; break ; case '3' : menustatus[FLAG_DISPLAY_HIGHSCORES] = true ; break ; case '4' : menustatus[FLAG_EXIT_GAME] = true ; break ; default : flagInputErroneousChoice = true ; break ; } } void processPlayerInput () { if (menustatus[FLAG_START_GAME]) { } if (menustatus[FLAG_CONTINUE_GAME]) { } if (menustatus[FLAG_DISPLAY_HIGHSCORES]) { } if (menustatus[FLAG_EXIT_GAME]) { exit (EXIT_SUCCESS); } } bool soloLoop () { menustatus = menuStatus_t{}; clearScreen(); receiveInputFromPlayer(std ::cin ); processPlayerInput(); return flagInputErroneousChoice; } void endlessLoop () { while (soloLoop()) ; } } namespace Menu{ void startMenu () { endlessLoop(); } }
我们使用枚举MenuStatusFlag记录所有可能的选择,该枚举类型有六个不同元素,中间四个元素:
1 2 3 4 FLAG_START_GAME FLAG_CONTINUE_GAME FLAG_DISPLAY_HIGHSCORES FLAG_EXIT_GAME
真正记录选择状态,MAX_NO_MAIN_MENU_STATUS_FLAGS的NO应理解为NO.1,NO.2的number含义,而非yes/no中no的含义。
用一个bool类型的数组menustatus记录玩家本轮选择;flagInputErroneousChoice记录玩家输入是否非法,若非法,则该值为真,soloLoop()返回该值,于是又进行下一轮的soloLoop()。直至用户输入1~4中某一数字,于是在processPlayerInput()中,或是跳转至对应函数执行后续功能,或是结束游戏退出程序。
需注意的是,processPlayerInput()中的几个判断不能写为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void processPlayerInput () { if (menustatus[FLAG_START_GAME]) { } else if (menustatus[FLAG_CONTINUE_GAME]) { } else if (menustatus[FLAG_DISPLAY_HIGHSCORES]) { } else (menustatus[FLAG_EXIT_GAME]) { exit (EXIT_SUCCESS); } }
如果写成上述形式,当用户输入错误时游戏直接结束,程序退出。
我们的soloLoop()还需最后一步完善——draw something.
需要在clearScreen()函数后加上这两句:
1 2 3 4 DrawAlways(std ::cout , Game::Graphics::AsciiArt2048); DrawAlways(std ::cout , DataSuppliment(flagInputErroneousChoice, Game::Graphics::Menu::MenuGraphicsOverlay));
其中AsciiArt2048()声明于game-graphics.hpp中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #ifndef GAMEGRAPHICS_H #define GAMEGRAPHICS_H #include <string> namespace Game{ namespace Graphics { std ::string AsciiArt2048 () ; } } #endif !GAMEGRAPHICS_H
定义于game-graphics.cpp中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include "game-graphics.hpp" #include <stringstream> namespace Game{ namespace Graphics { std ::string AsciiArt2048 () { constexpr auto title_card_2048 = R"( /\\\\\\\\\ /\\\\\\\ /\\\ /\\\\\\\\\ /\\\///////\\\ /\\\/////\\\ /\\\\\ /\\\///////\\\ \/// \//\\\ /\\\ \//\\\ /\\\/\\\ \/\\\ \/\\\ /\\\/ \/\\\ \/\\\ /\\\/\/\\\ \///\\\\\\\\\/ /\\\// \/\\\ \/\\\ /\\\/ \/\\\ /\\\///////\\\ /\\\// \/\\\ \/\\\ /\\\\\\\\\\\\\\\\ /\\\ \//\\\ /\\\/ \//\\\ /\\\ \///////////\\\// \//\\\ /\\\ /\\\\\\\\\\\\\\\ \///\\\\\\\/ \/\\\ \///\\\\\\\\\/ \/////////////// \/////// \/// \///////// )" ; std ::ostringstream title_card_richtext; title_card_richtext << title_card_2048 << "\n\n\n" ; return title_card_richtext.str(); } } }
AsciiArt2048()功能非常简单,只是单纯的输出艺术字字符串,其中字符串前的R表示后面被分隔符包围的整个字符串都是原生字符, 不用转义, 所见即所得。
而MenuGraphicsOverlay()则声明于menu-graphics.hpp中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #ifndef MENUGRAPHICS_H #define MENUGRAPHICS_H #include <string> namespace Game { namespace Graphics { namespace Menu { std ::string MenuGraphicsOverlay (bool ifInputInvalid) ; } } } #endif
该函数需要一个布尔值作为参数,该值为真则说明上一轮循环中用户输入非法,需提示其重新输入。
MenuGraphicsOverlay()定义于menu-graphics.cpp中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 namespace Game { namespace Graphics { namespace Menu { std ::string MenuGraphicsOverlay (bool input_error_choice_invalid) { std ::ostringstream str_os; DrawAlways(str_os, MenuTitlePrompt); DrawAlways(str_os, MenuOptionsPrompt); DrawOnlyWhen(str_os, input_error_choice_invalid, InputMenuErrorInvalidInputPrompt); DrawAlways(str_os, InputMenuPrompt); return str_os.str(); } } } }
MenuGraphicsOverlay()又分别调用了四个函数。MenuTitlePrompt()和MenuOptionsPrompt()分别负责标题与选项的呈现,InputMenuErrorInvalidInputPrompt()仅在用户输入非法时被调用,用以提示用户重新输入,InputMenuPrompt()提示用户键入选项。
以上四个函数均声明于menu-graphics.hpp,定义于menu-graphics.cpp。
menu-graphics.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #ifndef MENUGRAPHICS_H #define MENUGRAPHICS_H #include <string> namespace Game{ namespace Graphics { namespace Menu { std ::string MenuTitlePrompt () ; std ::string MenuOptionsPrompt () ; std ::string InputMenuErrorInvalidInputPrompt () ; std ::string InputMenuPrompt () ; std ::string MenuGraphicsOverlay (bool ifInputInvalid) ; } } } #endif
menu-graphics.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 #include "menu-graphics.hpp" #include "global.hpp" #include <sstream> namespace Game{ namespace Graphics { namespace Menu { std ::string MenuTitlePrompt () { constexpr auto greetings_text = "Welcome to " ; constexpr auto gamename_text = "2048!" ; constexpr auto sp = " " ; std ::ostringstream str_os; std ::ostringstream title_richtext; title_richtext << sp << greetings_text << gamename_text << "\n" ; str_os << title_richtext.str(); return str_os.str(); } std ::string MenuOptionsPrompt () { const auto menu_list_txt = { "1. Play a New Game" , "2. Continue Previous Game" , "3. View Highscores and Statistics" , "4. Exit" }; constexpr auto sp = " " ; std ::ostringstream str_os; str_os << "\n" ; for (const auto txt : menu_list_txt) { str_os << sp << txt << "\n" ; } str_os << "\n" ; return str_os.str(); } std ::string InputMenuErrorInvalidInputPrompt () { constexpr auto err_input_text = "Invalid input. Please try again." ; constexpr auto sp = " " ; std ::ostringstream str_os; std ::ostringstream err_input_richtext; err_input_richtext << sp << err_input_text << "\n\n" ; str_os << err_input_richtext.str(); return str_os.str(); } std ::string InputMenuPrompt () { constexpr auto prompt_choice_text = "Enter Choice: " ; constexpr auto sp = " " ; std ::ostringstream str_os; std ::ostringstream prompt_choice_richtext; prompt_choice_richtext << sp << prompt_choice_text; str_os << prompt_choice_richtext.str(); return str_os.str(); } std ::string MenuGraphicsOverlay (bool ifInputInvalid) { } } } }
至此,一个基础的框架就搭建起来了。
代码 2048.cpp 1 2 3 4 5 6 7 8 9 #include "menu.hpp" int main () { Menu::startMenu(); return 0 ; }
1 2 3 4 5 6 7 8 9 #ifndef MENU_H #define MENU_H namespace Menu { void startMenu () ; } #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 #include "menu.hpp" #include "menu-graphics.hpp" #include "global.hpp" #include "game-graphics.hpp" #include <array> #include <iostream> namespace { enum MenuStatusFlag { FLAG_NULL, FLAG_START_GAME, FLAG_CONTINUE_GAME, FLAG_DISPLAY_HIGHSCORES, FLAG_EXIT_GAME, MAX_NO_MAIN_MENU_STATUS_FLAGS }; using menuStatus_t = std ::array <bool , MAX_NO_MAIN_MENU_STATUS_FLAGS>; menuStatus_t menustatus{}; bool flagInputErroneousChoice{ false }; void receiveInputFromPlayer (std ::istream& in_os) { flagInputErroneousChoice = bool {}; char c; in_os >> c; switch (c) { case '1' : menustatus[FLAG_START_GAME] = true ; break ; case '2' : menustatus[FLAG_CONTINUE_GAME] = true ; break ; case '3' : menustatus[FLAG_DISPLAY_HIGHSCORES] = true ; break ; case '4' : menustatus[FLAG_EXIT_GAME] = true ; break ; default : flagInputErroneousChoice = true ; break ; } } void processPlayerInput () { if (menustatus[FLAG_START_GAME]) { } if (menustatus[FLAG_CONTINUE_GAME]) { } if (menustatus[FLAG_DISPLAY_HIGHSCORES]) { } if (menustatus[FLAG_EXIT_GAME]) { exit (EXIT_SUCCESS); } } bool soloLoop () { menustatus = menuStatus_t{}; clearScreen(); DrawAlways(std ::cout , Game::Graphics::AsciiArt2048); DrawAlways(std ::cout , DataSuppliment(flagInputErroneousChoice, Game::Graphics::Menu::MenuGraphicsOverlay)); receiveInputFromPlayer(std ::cin ); processPlayerInput(); return flagInputErroneousChoice; } void endlessLoop () { while (soloLoop()) ; } } namespace Menu{ void startMenu () { endlessLoop(); } }
global.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #ifndef GLOBAL_H #define GLOBAL_H #include <iosfwd> #include <string> using ull = unsigned long long ;template <typename T>void DrawAlways (std ::ostream& os, T f) { os << f(); } template <typename T>void DrawOnlyWhen (std ::ostream& os, bool trigger, T f) { if (trigger) { DrawAlways(os, f); } } template <typename T>void DrawAsOneTimeFlag (std ::ostream& os, bool & trigger, T f) { if (trigger) { DrawAlways(os, f); trigger = !trigger; } } template <typename suppliment_t >struct DataSupplimentInternalType { suppliment_t suppliment_data; template <typename function_t > std ::string operator () (function_t f) const { return f(suppliment_data); } }; template <typename suppliment_t , typename function_t >auto DataSuppliment (suppliment_t needed_data, function_t f) { using dsit_t = DataSupplimentInternalType<suppliment_t >; const auto lambda_f_to_return = [=]() { const dsit_t depinject_func = dsit_t { needed_data }; return depinject_func(f); }; return lambda_f_to_return; } void clearScreen () ;#endif
global.cpp 1 2 3 4 5 6 7 8 9 10 #include "global.hpp" void clearScreen () {#ifdef _WIN32 system("cls" ); #else system("clear" ); #endif }
game-graphics.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #ifndef GAME_GRAPHICS_H #define GAME_GRAPHICS_H #include <string> namespace Game{ namespace Graphics { std ::string AsciiArt2048 () ; } } #endif
game-graphics.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include "game-graphics.hpp" #include <sstream> namespace Game{ namespace Graphics { std ::string AsciiArt2048 () { constexpr auto title_card_2048 = R"( /\\\\\\\\\ /\\\\\\\ /\\\ /\\\\\\\\\ /\\\///////\\\ /\\\/////\\\ /\\\\\ /\\\///////\\\ \/// \//\\\ /\\\ \//\\\ /\\\/\\\ \/\\\ \/\\\ /\\\/ \/\\\ \/\\\ /\\\/\/\\\ \///\\\\\\\\\/ /\\\// \/\\\ \/\\\ /\\\/ \/\\\ /\\\///////\\\ /\\\// \/\\\ \/\\\ /\\\\\\\\\\\\\\\\ /\\\ \//\\\ /\\\/ \//\\\ /\\\ \///////////\\\// \//\\\ /\\\ /\\\\\\\\\\\\\\\ \///\\\\\\\/ \/\\\ \///\\\\\\\\\/ \/////////////// \/////// \/// \///////// )" ; std ::ostringstream title_card_richtext; title_card_richtext << title_card_2048 << "\n\n\n" ; return title_card_richtext.str(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #ifndef MENUGRAPHICS_H #define MENUGRAPHICS_H #include <string> namespace Game{ namespace Graphics { namespace Menu { std ::string MenuTitlePrompt () ; std ::string MenuOptionsPrompt () ; std ::string InputMenuErrorInvalidInputPrompt () ; std ::string InputMenuPrompt () ; std ::string MenuGraphicsOverlay (bool ifInputInvalid) ; } } } #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include "menu-graphics.hpp" #include "global.hpp" #include <sstream> namespace Game{ namespace Graphics { namespace Menu { std ::string MenuTitlePrompt () { constexpr auto greetings_text = "Welcome to " ; constexpr auto gamename_text = "2048!" ; constexpr auto sp = " " ; std ::ostringstream str_os; std ::ostringstream title_richtext; title_richtext << sp << greetings_text << gamename_text << "\n" ; str_os << title_richtext.str(); return str_os.str(); } std ::string MenuOptionsPrompt () { const auto menu_list_txt = { "1. Play a New Game" , "2. Continue Previous Game" , "3. View Highscores and Statistics" , "4. Exit" }; constexpr auto sp = " " ; std ::ostringstream str_os; str_os << "\n" ; for (const auto txt : menu_list_txt) { str_os << sp << txt << "\n" ; } str_os << "\n" ; return str_os.str(); } std ::string InputMenuErrorInvalidInputPrompt () { constexpr auto err_input_text = "Invalid input. Please try again." ; constexpr auto sp = " " ; std ::ostringstream str_os; std ::ostringstream err_input_richtext; err_input_richtext << sp << err_input_text << "\n\n" ; str_os << err_input_richtext.str(); return str_os.str(); } std ::string InputMenuPrompt () { constexpr auto prompt_choice_text = "Enter Choice: " ; constexpr auto sp = " " ; std ::ostringstream str_os; std ::ostringstream prompt_choice_richtext; prompt_choice_richtext << sp << prompt_choice_text; str_os << prompt_choice_richtext.str(); return str_os.str(); } std ::string MenuGraphicsOverlay (bool ifInputInvalid) { std ::ostringstream str_os; DrawAlways(str_os, MenuTitlePrompt); DrawAlways(str_os, MenuOptionsPrompt); DrawOnlyWhen(str_os, ifInputInvalid, InputMenuErrorInvalidInputPrompt); DrawAlways(str_os, InputMenuPrompt); return str_os.str(); } } } }
阶段二 构建 首先考虑丰富一下已有功能的视觉呈现——添加色彩。实现color.hpp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #ifndef COLOR_H #define COLOR_H #include <ostream> namespace Color { enum class Code { BOLD = 1 , RESET = 0 , BG_BLUE = 44 , BG_DEFAULT = 49 , BG_GREEN = 42 , BG_RED = 41 , FG_BLACK = 30 , FG_BLUE = 34 , FG_CYAN = 36 , FG_DARK_GRAY = 90 , FG_DEFAULT = 39 , FG_GREEN = 32 , FG_LIGHT_BLUE = 94 , FG_LIGHT_CYAN = 96 , FG_LIGHT_GRAY = 37 , FG_LIGHT_GREEN = 92 , FG_LIGHT_MAGENTA = 95 , FG_LIGHT_RED = 91 , FG_LIGHT_YELLOW = 93 , FG_MAGENTA = 35 , FG_RED = 31 , FG_WHITE = 97 , FG_YELLOW = 33 , }; class Modifier { Code code; public : Modifier(Code pCode) : code(pCode) {} friend std ::ostream& operator <<(std ::ostream& os, const Modifier& mod) { return os << "\033[" << static_cast <int >(mod.code) << "m" ; } }; } static Color::Modifier bold_off (Color::Code::RESET) ;static Color::Modifier bold_on (Color::Code::BOLD) ;static Color::Modifier def (Color::Code::FG_DEFAULT) ;static Color::Modifier red (Color::Code::FG_RED) ;static Color::Modifier green (Color::Code::FG_GREEN) ;static Color::Modifier yellow (Color::Code::FG_YELLOW) ;static Color::Modifier blue (Color::Code::FG_BLUE) ;static Color::Modifier magenta (Color::Code::FG_MAGENTA) ;static Color::Modifier cyan (Color::Code::FG_CYAN) ;static Color::Modifier lightGray (Color::Code::FG_LIGHT_GRAY) ;static Color::Modifier darkGray (Color::Code::FG_DARK_GRAY) ;static Color::Modifier lightRed (Color::Code::FG_LIGHT_RED) ;static Color::Modifier lightGreen (Color::Code::FG_LIGHT_GREEN) ;static Color::Modifier lightYellow (Color::Code::FG_LIGHT_YELLOW) ;static Color::Modifier lightBlue (Color::Code::FG_LIGHT_BLUE) ;static Color::Modifier lightMagenta (Color::Code::FG_LIGHT_MAGENTA) ;static Color::Modifier lightCyan (Color::Code::FG_LIGHT_CYAN) ;#endif
格式:
“\033[字背景颜色;字体颜色m字符串/033[0m”
例如: “\033[41;36m something here /033[0m”
其中41的位置代表底色, 36的位置是代表字的颜色。
在enum class Code中,FG开头的颜色代表设置字体颜色,BG开头的颜色代表设置背景颜色。
有了color.hpp后,给之前几个负责绘制图案的函数加上颜色:
game-graphics.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include "color.hpp" std ::string AsciiArt2048 () { constexpr auto title_card_2048 = R"( /\\\\\\\\\ /\\\\\\\ /\\\ /\\\\\\\\\ /\\\///////\\\ /\\\/////\\\ /\\\\\ /\\\///////\\\ \/// \//\\\ /\\\ \//\\\ /\\\/\\\ \/\\\ \/\\\ /\\\/ \/\\\ \/\\\ /\\\/\/\\\ \///\\\\\\\\\/ /\\\// \/\\\ \/\\\ /\\\/ \/\\\ /\\\///////\\\ /\\\// \/\\\ \/\\\ /\\\\\\\\\\\\\\\\ /\\\ \//\\\ /\\\/ \//\\\ /\\\ \///////////\\\// \//\\\ /\\\ /\\\\\\\\\\\\\\\ \///\\\\\\\/ \/\\\ \///\\\\\\\\\/ \/////////////// \/////// \/// \///////// )" ; std ::ostringstream title_card_richtext; title_card_richtext << green << bold_on << title_card_2048 << bold_off << def; title_card_richtext << "\n\n\n" ; return title_card_richtext.str(); }
menu-graphics.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include "color.hpp" std ::string MenuTitlePrompt () { constexpr auto greetings_text = "Welcome to " ; constexpr auto gamename_text = "2048!" ; constexpr auto sp = " " ; std ::ostringstream str_os; std ::ostringstream title_richtext; title_richtext << bold_on << sp << greetings_text << yellow << gamename_text << bold_off << def << "\n" ; str_os << title_richtext.str(); return str_os.str(); } std ::string InputMenuErrorInvalidInputPrompt () { constexpr auto err_input_text = "Invalid input. Please try again." ; constexpr auto sp = " " ; std ::ostringstream str_os; std ::ostringstream err_input_richtext; err_input_richtext << red << sp << err_input_text << def << "\n\n" ; str_os << err_input_richtext.str(); return str_os.str(); }
接着完善“开启一局新游戏”代码。
在menu.cpp中完善startGame()函数:
1 2 3 4 void startGame () { Game::startGame(); }
该函数调用Game名字空间里的同名函数,需创建game.hpp、game.cpp实现相应功能。
game.hpp
1 2 3 4 5 6 7 8 9 10 #ifndef GAME_H #define GAME_H namespace Game { void startGame () ; }; #endif
game.cpp
1 2 3 4 5 6 7 8 9 10 #include "game.hpp" #include "game-pregame.hpp" namespace Game{ void startGame () { PreGameSetup::SetupNewGame(); } }
在正式开始一局游戏前,需要做一些准备工作。比如设置游戏棋盘大小(我们最终实现的游戏支持从3 x 3到10 x 10大小的棋盘)等工作。故还需创建game-pregame.hpp与game-pregame.cpp两文件负责游戏正式开局前的准备工作。
game-pregame.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 #ifndef GAME_PREGAME_H #define GAME_PREGAME_H namespace Game{ namespace PreGameSetup { void SetupNewGame () ; } } #endif
game-pregame.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include "game-pregame.hpp" namespace Game{ namespace { enum class NewGameFlag { NewGameFlagNull, NoPreviousSaveAvailable }; bool noSave{ false }; bool soloLoop () { return false ; } void endlessLoop () { while (soloLoop()) ; } void SetupNewGame (NewGameFlag ns) { noSave = (ns == NewGameFlag::NoPreviousSaveAvailable) ? true : false ; endlessLoop(); } } namespace PreGameSetup { void SetupNewGame () { SetupNewGame(NewGameFlag::NewGameFlagNull); } } }
NewGameFlag::NoPreviousSaveAvailable和noSave都与主菜单界面选项二的continueGame相关,其逻辑为:若玩家在主菜单界面选择选项二:Continue Previous Game,需判断是否存在Previous Game数据。若存在,则加载之前的游戏数据(目前尚未实现),否则直接视为开启一轮新游戏。
现在考虑完善soloLoop()。因为我们的游戏能向玩家提供不同尺寸的规格,因此在正式游玩前需要:
输出一些基本的提示语,引导玩家输入棋盘尺寸
读入玩家输入
根据玩家输入,执行相应策略
若输入非法,提示玩家重新输入
具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 bool soloLoop () { bool invalidInputValue = flagInputErroneousChoice; const auto questionAboutGameBoardSizePrompt = [&invalidInputValue]() { std ::ostringstream str_os; DrawOnlyWhen(str_os, invalidInputValue, Graphics::BoardSizeErrorPrompt); DrawAlways(str_os, Graphics::BoardInputPrompt); return str_os.str(); }; pregamesetup_status = pregameesetup_status_t {}; clearScreen(); DrawAlways(std ::cout , Game::Graphics::AsciiArt2048); DrawAsOneTimeFlag(std ::cout , noSave, Graphics::GameBoardNoSaveErrorPrompt); DrawAlways(std ::cout , questionAboutGameBoardSizePrompt); receiveInputFromPlayer(std ::cin ); processPreGame(); return flagInputErroneousChoice; }
逐行分析该函数。
1 bool invalidInputValue = flagInputErroneousChoice;
首先,我们使用布尔变量invalidInputValue记录用户输入是否非法。变量flagInputErroneousChoice定义在函数体外,初值为false,充当全局变量使用。在函数receiveInputFromPlayer()中,当用户输入非法时flagInputErroneousChoice被设为true。
1 2 3 4 5 6 7 8 9 const auto questionAboutGameBoardSizePrompt = [&invalidInputValue]() { std ::ostringstream str_os; DrawOnlyWhen(str_os, invalidInputValue, Graphics::BoardSizeErrorPrompt); DrawAlways(str_os, Graphics::BoardInputPrompt); return str_os.str(); };
该lambda表达式返回提示用户输入棋盘尺寸的字符串,当用户输入非法时(即invalidInputValue为真),BoardSizeErrorPrompt()提醒用户输入错误。
BoardSizeErrorPrompt()与BoardInputPrompt()均声明于game-graphics.hpp,定义于game-graphics.cpp。
game-graphics.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 std ::string BoardSizeErrorPrompt () { const auto invalid_prompt_text = { "Invalid input. Gameboard size should range from " , " to " , "." }; 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(); } 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(); }
常量MIN_GAME_BOARD_PLAY_SIZE和MAX_GAME_BOARD_PLAY_SIZE定义于game-graphics.hpp中。
game-graphics.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #ifndef GAME_GRAPHICS_H #define GAME_GRAPHICS_H #include <string> enum GameBoardDimensions { MIN_GAME_BOARD_PLAY_SIZE = 3 , MAX_GAME_BOARD_PLAY_SIZE = 10 }; namespace Game{ } #endif
BoardInputPrompt() 中board_size_prompt_text的内容指的是,虽然我们的游戏提供从3 x 3到10 x 10尺寸的规格,但最终只有4 x 4 规格棋盘大小的局次会记录分数。
soloLoop()的接下来两行:
1 2 pregamesetup_status = pregameesetup_status_t {};
pregameesetup_status_t定义于本文件的匿名namespace内,如下:
1 2 3 4 5 6 7 8 9 10 11 enum PreGameSetupStatusFlag { FLAG_NULL, FLAG_START_GAME, FLAG_RETURN_TO_MAIN_MENU, MAX_NO_PREGAME_SETUP_STATUS_FLAGS }; using pregameesetup_status_t = std ::array <bool , MAX_NO_PREGAME_SETUP_STATUS_FLAGS>; pregameesetup_status_t pregamesetup_status{};
至于soloLoop()的最后几行:
1 2 3 4 5 6 7 8 9 clearScreen(); DrawAlways(std ::cout , Game::Graphics::AsciiArt2048); DrawAsOneTimeFlag(std ::cout , noSave, Graphics::GameNoSaveErrorPrompt); DrawAlways(std ::cout , questionAboutBoardSizePrompt); receiveInputFromPlayer(std ::cin ); processPreGame(); return flagInputErroneousChoice;
清屏,输出必要信息,接受并处理用户输入,最后返回flagInputErroneousChoice,没什么值得特别说明的。GameNoSaveErrorPrompt()定义于game-graphics.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 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(); }
processPreGame()内容十分简单:
1 2 3 4 5 6 7 8 9 10 11 12 void processPreGame () { if (pregamesetup_status[FLAG_START_GAME]) { } if (pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU]) { Menu::startMenu(); } }
receiveInputFromPlayer()稍微复杂一些:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 int receiveGameBoardSize (std ::istream& is) { int playerInputBoardSize{ 0 }; if (!(is >> playerInputBoardSize)) { constexpr auto INVALID_INPUT_VALUE_FLAG = -1 ; playerInputBoardSize = INVALID_INPUT_VALUE_FLAG; is.clear(); is.ignore(std ::numeric_limits<std ::streamsize>::max(), '\n' ); } return playerInputBoardSize; } void receiveInputFromPlayer (std ::istream& is) { flagInputErroneousChoice = bool { false }; const auto gbsize = receiveGameBoardSize(is); const auto isValidBoardSize = (gbsize >= MIN_GAME_BOARD_PLAY_SIZE) && (gbsize <= MAX_GAME_BOARD_PLAY_SIZE); if (isValidBoardSize) { storedGameBoardSize = gbsize; pregamesetup_status[FLAG_START_GAME] = true ; } bool isSpecialCase{ true }; switch (gbsize) { case 0 : pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU] = true ; break ; default : isSpecialCase = false ; break ; } if (!isValidBoardSize && !isSpecialCase) { flagInputErroneousChoice = true ; } }
receiveInputFromPlayer()调用receiveGameBoardSize()来读取用户输入,然后处理输入数据。如果用户输入为3~10之间数字,满足isValidBoardSize条件(符合要求的棋盘大小),设置状态为FLAG_START_GAME;若用户输入为数字0,则代表回退到主界面菜单,此为special case,设置状态为FLAG_RETURN_TO_MAIN_MENU。以上两种外的任何情况均视为非法输入。
1 is.ignore(std ::numeric_limits<std ::streamsize>::max(), '\n' );
这行代码的作用为清空输入缓冲。
std::istream::ignore
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Extract and discard characters Extracts characters from the input sequence and discards them, until either n characters have been extracted, or one compares equal to delim. #include <iostream> // std::cin, std::cout int main () { char first, last; std ::cout << "Please, enter your first name followed by your surname: " ; first = std ::cin .get(); std ::cin .ignore(256 ,' ' ); last = std ::cin .get(); std ::cout << "Your initials are " << first << last << '\n' ; return 0 ; } Possible output: Please, enter your first name followed by your surname: John Smith Your initials are JS
std::streamsize
1 2 3 4 5 6 7 8 9 10 11 12 13 The type std ::streamsize is an implementation-defined signed integral type used to represent the number of characters transferred in an I/O operation or the size of an I/O buffer. It is used as a signed counterpart of std ::size_t , similar to the POSIX type ssize_t . #include <limits> #include <cstddef> #include <iostream> int main () { std ::cout << "streamsize: " << std ::dec << std ::numeric_limits<std ::streamsize>::max() << " or " << std ::hex << std ::numeric_limits<std ::streamsize>::max() << '\n' ; } Possible output: streamsize: 9223372036854775807 or 0x7fffffffffffffff
std::numeric_limits<T>::max
1 Returns the maximum finite value representable by the numeric type T. Meaningful for all bounded types.
至此,我们的游戏已经能够向玩家提供选取不同规格棋盘的功能。
代码 2048.cpp same as before
same as before
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include "menu.hpp" #include "menu-graphics.hpp" #include "game.hpp" #include "global.hpp" #include "game-graphics.hpp" #include <array> #include <iostream> namespace { enum MenuStatusFlag { FLAG_NULL, FLAG_START_GAME, FLAG_CONTINUE_GAME, FLAG_DISPLAY_HIGHSCORES, FLAG_EXIT_GAME, MAX_NO_MAIN_MENU_STATUS_FLAGS }; using menuStatus_t = std ::array <bool , MAX_NO_MAIN_MENU_STATUS_FLAGS>; menuStatus_t menustatus{}; bool flagInputErroneousChoice{ false }; void startGame () { Game::startGame(); } void receiveInputFromPlayer (std ::istream& in_os) { } void processPlayerInput () { if (menustatus[FLAG_START_GAME]) { startGame(); } if (menustatus[FLAG_CONTINUE_GAME]) { } if (menustatus[FLAG_DISPLAY_HIGHSCORES]) { } if (menustatus[FLAG_EXIT_GAME]) { exit (EXIT_SUCCESS); } } bool soloLoop () { } void endlessLoop () { } } namespace Menu{ void startMenu () { } }
global.hpp same as before
global.cpp same as before
game-graphics.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #ifndef GAME_GRAPHICS_H #define GAME_GRAPHICS_H #include <string> enum GameBoardDimensions { MIN_GAME_BOARD_PLAY_SIZE = 3 , MAX_GAME_BOARD_PLAY_SIZE = 10 }; namespace Game{ namespace Graphics { std ::string AsciiArt2048 () ; std ::string BoardSizeErrorPrompt () ; std ::string BoardInputPrompt () ; std ::string GameBoardNoSaveErrorPrompt () ; } } #endif
game-graphics.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #include "game-graphics.hpp" #include "color.hpp" #include <sstream> namespace Game{ namespace Graphics { std ::string AsciiArt2048 () { } std ::string BoardSizeErrorPrompt () { const auto invalid_prompt_text = { "Invalid input. Gameboard size should range from " , " to " , "." }; 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(); } 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 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(); } } }
same as before
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include "menu-graphics.hpp" #include "global.hpp" #include "color.hpp" #include <sstream> namespace Game{ namespace Graphics { namespace Menu { std ::string MenuTitlePrompt () { constexpr auto greetings_text = "Welcome to " ; constexpr auto gamename_text = "2048!" ; constexpr auto sp = " " ; std ::ostringstream str_os; std ::ostringstream title_richtext; title_richtext << bold_on << sp << greetings_text << yellow << gamename_text << bold_off << def << "\n" ; str_os << title_richtext.str(); return str_os.str(); } std ::string MenuOptionsPrompt () { } std ::string InputMenuErrorInvalidInputPrompt () { constexpr auto err_input_text = "Invalid input. Please try again." ; constexpr auto sp = " " ; std ::ostringstream str_os; std ::ostringstream err_input_richtext; err_input_richtext << red << sp << err_input_text << def << "\n\n" ; str_os << err_input_richtext.str(); return str_os.str(); } std ::string InputMenuPrompt () { } std ::string MenuGraphicsOverlay (bool ifInputInvalid) { } } } }
game.hpp 1 2 3 4 5 6 7 8 9 10 #ifndef GAME_H #define GAME_H namespace Game { void startGame () ; }; #endif
game.cpp 1 2 3 4 5 6 7 8 9 10 #include "game.hpp" #include "game-pregame.hpp" namespace Game{ void startGame () { PreGameSetup::SetupNewGame(); } }
game-pregame.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 #ifndef GAME_PREGAME_H #define GAME_PREGAME_H namespace Game{ namespace PreGameSetup { void SetupNewGame () ; } } #endif
game-pregame.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 #include "game-pregame.hpp" #include "game-graphics.hpp" #include "game-input.hpp" #include "menu.hpp" #include "global.hpp" #include <array> #include <sstream> #include <iostream> #include <limits> namespace Game{ namespace { enum PreGameSetupStatusFlag { FLAG_NULL, FLAG_START_GAME, FLAG_RETURN_TO_MAIN_MENU, MAX_NO_PREGAME_SETUP_STATUS_FLAGS }; using pregameesetup_status_t = std ::array <bool , MAX_NO_PREGAME_SETUP_STATUS_FLAGS>; pregameesetup_status_t pregamesetup_status{}; enum class NewGameFlag { NewGameFlagNull, NoPreviousSaveAvailable }; bool noSave{ false }; bool flagInputErroneousChoice{ false }; ull storedGameBoardSize{ 1 }; int receiveGameBoardSize (std ::istream& is) { int playerInputBoardSize{ 0 }; if (!(is >> playerInputBoardSize)) { constexpr auto INVALID_INPUT_VALUE_FLAG = -1 ; playerInputBoardSize = INVALID_INPUT_VALUE_FLAG; is.clear(); is.ignore(std ::numeric_limits<std ::streamsize>::max(), '\n' ); } return playerInputBoardSize; } void receiveInputFromPlayer (std ::istream& is) { using namespace Input::Keypress::Code; flagInputErroneousChoice = bool { false }; const auto gbsize = receiveGameBoardSize(is); const auto isValidBoardSize = (gbsize >= MIN_GAME_BOARD_PLAY_SIZE) && (gbsize <= MAX_GAME_BOARD_PLAY_SIZE); if (isValidBoardSize) { storedGameBoardSize = gbsize; pregamesetup_status[FLAG_START_GAME] = true ; } bool goBackToMainMenu{ true }; switch (gbsize) { case CODE_HOTKEY_PREGAME_BACK_TO_MENU: pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU] = true ; break ; default : goBackToMainMenu = false ; break ; } if (!isValidBoardSize && !goBackToMainMenu) { flagInputErroneousChoice = true ; } } void processPreGame () { if (pregamesetup_status[FLAG_START_GAME]) { } if (pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU]) { Menu::startMenu(); } } bool soloLoop () { bool invalidInputValue = flagInputErroneousChoice; const auto questionAboutGameBoardSizePrompt = [&invalidInputValue]() { std ::ostringstream str_os; DrawOnlyWhen(str_os, invalidInputValue, Graphics::BoardSizeErrorPrompt); DrawAlways(str_os, Graphics::BoardInputPrompt); return str_os.str(); }; pregamesetup_status = pregameesetup_status_t {}; clearScreen(); DrawAlways(std ::cout , Game::Graphics::AsciiArt2048); DrawAsOneTimeFlag(std ::cout , noSave, Graphics::GameBoardNoSaveErrorPrompt); DrawAlways(std ::cout , questionAboutGameBoardSizePrompt); receiveInputFromPlayer(std ::cin ); processPreGame(); return flagInputErroneousChoice; } void endlessLoop () { while (soloLoop()) ; } void SetupNewGame (NewGameFlag ns) { noSave = (ns == NewGameFlag::NoPreviousSaveAvailable) ? true : false ; endlessLoop(); } } namespace PreGameSetup { void SetupNewGame () { SetupNewGame(NewGameFlag::NewGameFlagNull); } } }
color.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #ifndef COLOR_H #define COLOR_H #include <ostream> namespace Color { enum class Code { BOLD = 1 , RESET = 0 , BG_BLUE = 44 , BG_DEFAULT = 49 , BG_GREEN = 42 , BG_RED = 41 , FG_BLACK = 30 , FG_BLUE = 34 , FG_CYAN = 36 , FG_DARK_GRAY = 90 , FG_DEFAULT = 39 , FG_GREEN = 32 , FG_LIGHT_BLUE = 94 , FG_LIGHT_CYAN = 96 , FG_LIGHT_GRAY = 37 , FG_LIGHT_GREEN = 92 , FG_LIGHT_MAGENTA = 95 , FG_LIGHT_RED = 91 , FG_LIGHT_YELLOW = 93 , FG_MAGENTA = 35 , FG_RED = 31 , FG_WHITE = 97 , FG_YELLOW = 33 , }; class Modifier { Code code; public : Modifier(Code pCode) : code(pCode) {} friend std ::ostream& operator <<(std ::ostream& os, const Modifier& mod) { return os << "\033[" << static_cast <int >(mod.code) << "m" ; } }; } static Color::Modifier bold_off (Color::Code::RESET) ;static Color::Modifier bold_on (Color::Code::BOLD) ;static Color::Modifier def (Color::Code::FG_DEFAULT) ;static Color::Modifier red (Color::Code::FG_RED) ;static Color::Modifier green (Color::Code::FG_GREEN) ;static Color::Modifier yellow (Color::Code::FG_YELLOW) ;static Color::Modifier blue (Color::Code::FG_BLUE) ;static Color::Modifier magenta (Color::Code::FG_MAGENTA) ;static Color::Modifier cyan (Color::Code::FG_CYAN) ;static Color::Modifier lightGray (Color::Code::FG_LIGHT_GRAY) ;static Color::Modifier darkGray (Color::Code::FG_DARK_GRAY) ;static Color::Modifier lightRed (Color::Code::FG_LIGHT_RED) ;static Color::Modifier lightGreen (Color::Code::FG_LIGHT_GREEN) ;static Color::Modifier lightYellow (Color::Code::FG_LIGHT_YELLOW) ;static Color::Modifier lightBlue (Color::Code::FG_LIGHT_BLUE) ;static Color::Modifier lightMagenta (Color::Code::FG_LIGHT_MAGENTA) ;static Color::Modifier lightCyan (Color::Code::FG_LIGHT_CYAN) ;#endif
阶段三 构建 首先考虑改善一下上一阶段的部分代码。
game-pregame.cpp
1 2 3 4 5 6 7 8 9 10 switch (gbsize){ case 0 : pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU] = true ; break ; default : goBackToMainMenu = false ; break ; }
我们的设计思路是,当玩家键入数字0时,返回主菜单。但也可以将数字0改为字母’A’, ‘B’, ‘C’…等其他任意字符。一个更好的方式是创建game-input.hpp文件,将不同按键对应的功能统一定义在此文件中。
game-input.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #ifndef GAME_INPUT_H #define GAME_INPUT_H namespace Game{ namespace Input { namespace Keypress { namespace Code { enum { CODE_HOTKEY_PREGAME_BACK_TO_MENU = 0 , }; } } } } #endif
然后修改game-graphics.cpp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include "game-input.hpp" void receiveInputFromPlayer (std ::istream& is) { using namespace Input::Keypress::Code; switch (gbsize) { case CODE_HOTKEY_PREGAME_BACK_TO_MENU: pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU] = true ; break ; default : goBackToMainMenu = false ; break ; } }
经过之前的努力,棋盘尺寸选好了,应该可以正式开始游戏了。但光有尺寸还不够,还需要有对应尺寸的棋盘才行,于是创建文件gameboard.hpp负责棋盘相关工作。但在这之前,需要先创立tile.hpp负责棋盘创建前的准备工作。
tile.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #ifndef TILE_H #define TILE_H #include "global.hpp" namespace Game{ struct tile_t { ull value{ 0 }; bool blocked{ false }; }; } #endif
tile,即地砖。我们的棋盘就是用一片片地砖铺出来的。每一块砖有两个属性,分别是此砖的对应的分值value和是布尔值blocked判断是否被阻挡。
有了地砖之后,才能搭建起棋盘。
gameboard.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #ifndef GAMEBOARD_H #define GAMEBOARD_H #include "tile.hpp" #include <tuple> #include <vector> namespace Game{ struct GameBoard { using tile_data_array_t = std ::vector <tile_t >; using gameboard_data_array_t = std ::tuple<size_t , tile_data_array_t >; gameboard_data_array_t gbda; bool win{ false }; bool moved{ false }; ull score{ 0 }; ull largestTile{ 2 }; long long moveCount{ -1 }; GameBoard() = default ; explicit GameBoard (ull boardsize) ; explicit GameBoard (ull boardsize, tile_data_array_t existboard) ; }; } #endif
gameboard.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 #include "gameboard.hpp" namespace Game{ GameBoard::GameBoard(ull boardsize) : GameBoard{ boardsize, tile_data_array_t (boardsize * boardsize)} { } GameBoard::GameBoard(ull boardsize, tile_data_array_t existboard) : gbda{ boardsize, existboard } { } }
现在,假设玩家Mike选择开始一局新游戏,棋盘选择4 x 4规格的。我们为其创建一个GameBoard对象,该棋盘含16个tile对象。它们呈现的布局应如下:
1 2 3 4 (0 , 0 ) (0 , 1 ) (0 , 2 ) (0 , 3 ) (1 , 0 ) (1 , 1 ) (1 , 2 ) (1 , 3 ) (2 , 0 ) (2 , 1 ) (2 , 2 ) (2 , 3 ) (3 , 0 ) (3 , 1 ) (3 , 2 ) (3 , 3 )
我们需要将16个tile对象的每一个与其在棋盘中对应的(x, y)坐标对应起来,比如在该例中,第0个tile对象的坐标为(0, 0),第4个tile对象的坐标为(1, 0)等等。这就需要一个负责将对象数组索引与坐标相互转换的文件,point2d.hpp。
point2d.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #ifndef POINT2D_H #define POINT2D_H #include <tuple> class point2D_t { using point_datatype_t = typename std ::tuple<int , int >; point_datatype_t pointVector{ 0 , 0 }; explicit point2D_t (const point_datatype_t pt) : pointVector { pt } { } public : enum class PointCoord { COORD_X, COORD_Y, }; point2D_t() = default ; point2D_t(const int x, const int y) : point2D_t(std ::make_tuple(x, y)) { } template <PointCoord dimension> int get () const { return std ::get<static_cast <int >(dimension)>(pointVector); } template <PointCoord dimension> void set (int val) { std ::get<static_cast <int >(dimension)>(pointVector) = val; } point_datatype_t get () const { return pointVector; } void set (point_datatype_t val) { pointVector = val; } void set (const int x, const int y) { set (std ::make_tuple(x, y)); } point2D_t& operator +=(const point2D_t& rhs) { this ->pointVector = std ::make_tuple( get<PointCoord::COORD_X>() + rhs.get<PointCoord::COORD_X>(), get<PointCoord::COORD_Y>() + rhs.get<PointCoord::COORD_Y>()); return *this ; } point2D_t& operator -=(const point2D_t& rhs) { this ->pointVector = std ::make_tuple( get<PointCoord::COORD_X>() - rhs.get<PointCoord::COORD_X>(), get<PointCoord::COORD_Y>() - rhs.get<PointCoord::COORD_Y>()); return *this ; } }; inline point2D_t operator +(point2D_t lhs, const point2D_t& rhs) { lhs += rhs; return lhs; } inline point2D_t operator -(point2D_t lhs, const point2D_t& rhs) { lhs -= rhs; return lhs; } #endif
以上内容都比较简单,不过多介绍。
现在,我们进一步丰富gameboard.*,添加一些必要的函数供后续使用。
首先考虑在gameboard.hpp中声明以下两个函数:
1 2 size_t getSizeOfGameboard (GameBoard::gameboard_data_array_t gbda) ;tile_t getTileOnGameboard (GameBoard::gameboard_data_array_t & gbda, point2D_t& pt) ;
前者返回棋盘尺寸多大,后者根据坐标获取相应的地砖。下面是gameboard.cpp新增内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #include "gameboard.hpp" #include "tile.hpp" #include "point2d.hpp" namespace Game{ namespace { using gameboard_data_array_t = GameBoard::gameboard_data_array_t ; enum gameboard_data_array_fields { IDX_BOARDSIZE, IDX_TILE_ARRAY, MAX_NO_INDEX }; struct gameboard_data_point_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 * getSizeOfGameboard(gbda) + y; } tile_t operator () (gameboard_data_array_t & gbda, point2D_t& pt) const { return std ::get<IDX_TILE_ARRAY>(gbda)[point2D_to_1D_index(gbda, pt)]; } tile_t & operator () (gameboard_data_array_t & gbda, point2D_t& pt) { return std ::get<IDX_TILE_ARRAY>(gbda)[point2D_to_1D_index(gbda, pt)]; } }; } GameBoard::GameBoard(ull boardsize) : GameBoard{ boardsize, tile_data_array_t (boardsize * boardsize)} { } GameBoard::GameBoard(ull boardsize, tile_data_array_t existboard) : gbda{ boardsize, existboard } { } size_t getSizeOfGameboard (gameboard_data_array_t & gbda) { return std ::get<IDX_BOARDSIZE>(gbda); } tile_t getTileOnGameboard (gameboard_data_array_t & gbda, point2D_t& pt) { return gameboard_data_point_t {}(gbda, pt); } }
struct gameboard_data_point_t中的point2D_to_1D_index()实现了从二维坐标到一维索引的转换。剩余新增内容较为简单,不再赘述。
接下来是两个获取棋盘基本信息的函数:
1 2 3 4 5 6 7 8 9 bool hasWonOnGameboard (GameBoard& gb) { return gb.win; } long long MoveCountOnGameBoard (GameBoard& gb) { return gb.moveCount; }
再往后是四个进阶函数:
1 2 3 4 void registerMoveByOneOnGameboard (GameBoard &gb) ;bool addTileOnGameboard (GameBoard &gb) ;void unblockTilesOnGameboard (GameBoard &gb) ;bool canMoveOnGameboard (GameBoard &gb) ;
我们一一分析之。
一、registerMoveByOneOnGameboard()
1 2 3 4 5 void registerMoveByOneOnGameboard (GameBoard& gb) { gb.moveCount++; gb.moved = false ; }
这个函数是十分易于理解的——每当玩家行动一次后,累加当前行动次数,复位布尔值moved。
二、addTileOnGameboard()
游戏刚开始时及每轮玩家行动后,需在棋盘上随机空余地砖生成数字4或2.当棋盘仍有空余地砖供生成新砖块时,函数返回false,否则返回true,此时游戏结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 bool addTileOnGameboardDataArray (gameboard_data_array_t & gbda) { constexpr auto CHANCE_OF_VALUE_FOUR_OVER_TWO = 75 ; const auto index_list_of_free_tiles = collectFreeTilesOnGameboardDataArray(gbda); if (!index_list_of_free_tiles.size()) { return true ; } const int boardSize = getSizeOfGameboard(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 / boardSize, rand_selected_index % boardSize }; const auto value_four_or_two = RandInt{}() % 100 > CHANCE_OF_VALUE_FOUR_OVER_TWO ? 4 : 2 ; setTileValueOnGameboardDataArray(gbda, rand_index_as_point_t , value_four_or_two); return false ; } bool addTileOnGameboard (GameBoard& gb) { return addTileOnGameboardDataArray(gb.gbda); }
此函数的辅助函数略长,看起来有些复杂,涉及到一些对象与函数暂时还未实现,不过不影响理解。
1 2 3 4 5 constexpr auto CHANCE_OF_VALUE_FOUR_OVER_TWO = 75 ;
通过修改该值,可左右数字生成概率。
1 2 const auto index_list_of_free_tiles = collectFreeTilesOnGameboardDataArray(gbda);
函数collectFreeTilesOnGameboardDataArray()返回类型为std::vector,该向量中的每一元素均为tile_t对象列表中空余地砖的索引。
RandInt暂未实现,其作用为生成随机数。
setTileValueOnGameboardDataArray()将最终取得的值置于随机所得的地砖上。
RandInt、setTileValueOnGameboardDataArray()与collectFreeTilesOnGameboardDataArray()均定义于gameboard.cpp的匿名空间中,具体内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class RandInt { public : using clock = std ::chrono::system_clock; 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 re; std ::uniform_int_distribution<> dist; }; void setTileValueOnGameboardDataArray (gameboard_data_array_t & gbda, point2D_t pt, ull value) { gameboard_data_point_t {}(gbda, pt).value = value; } std ::vector <size_t > collectFreeTilesOnGameboardDataArray(gameboard_data_array_t gbda) { std ::vector <size_t > freeTiles; auto index_counter{ 0 }; for (const auto t : std ::get<IDX_TILE_ARRAY>(gbda)) { if (!t.value) { freeTiles.push_back(index_counter); } index_counter++; } return freeTiles; }
三、unblockTilesOnGameboard()
该函数负责将棋盘所有地砖的blocked值全设为false。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 gameboard_data_array_t unblockTilesOnGameboardDataArray(gameboard_data_array_t gbda) { using tile_data_array_t = GameBoard::tile_data_array_t ; auto new_board_data_array = tile_data_array_t (std ::get<IDX_TILE_ARRAY>(gbda).size()); std ::transform(std ::begin(std ::get<IDX_TILE_ARRAY>(gbda)), std ::end(std ::get<IDX_TILE_ARRAY>(gbda)), std ::begin(new_board_data_array), [](const tile_t t) { return tile_t { t.value, false }; }); return gameboard_data_array_t { std ::get<IDX_BOARDSIZE>(gbda), new_board_data_array }; } void unblockTilesOnGameboard (GameBoard& gb) { gb.gbda = unblockTilesOnGameboardDataArray(gb.gbda); }
如果对std::transform不熟悉,请参考这里 。
四、canMoveOnGameboard()
本函数用以判断玩家是否还可继续行动。若是,返回true,游戏可以继续进行,否则返回false,游戏结束。判断的逻辑也比较简单:依次遍历所有tile_t对象,对于每一tile_t对象,检查其上下左右四个方向相邻的砖块是否为空(为空意味着canMove,游戏尚未结束)或是相邻砖块的值相同(值相同意味着两块砖的值可以合并,canMove,游戏尚未结束)。只要尚存上述两条件之一,游戏就可继续进行下去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 ull getTileValueOnGameboardDataArray (gameboard_data_array_t & gbda, point2D_t pt) { return gameboard_data_point_t {}(gbda, pt).value; } bool isPointInBoardArea (point2D_t pt, int boardSize) { int x, y; std ::tie(x, y) = pt.get(); return !(y < 0 || y > boardSize - 1 || x < 0 || x > boardSize - 1 ); } bool canMoveOnGameboardDataArray (gameboard_data_array_t & gbda) { auto indexCounter{ 0 }; const auto canMoveToOffset = [=, &indexCounter](const tile_t t) { const int boardSize = getSizeOfGameboard(gbda); const auto currentPoint = point2D_t{ indexCounter / boardSize, indexCounter % boardSize }; indexCounter++; const auto listOfOffsets = { point2D_t{1 , 0 }, point2D_t{0 , 1 } }; const auto currentPointValue = t.value; const auto offsetInRangeWithSameValue = [=](const point2D_t offset) { const auto offsetCheck = { currentPoint + offset, currentPoint - offset }; for (const auto currentOffset : offsetCheck) { if (isPointInBoardArea(currentOffset, boardSize)) { return getTileValueOnGameboardDataArray(gbda, currentOffset) == currentPointValue; } } return false ; }; return ((currentPointValue == 0u ) || std ::any_of(std ::begin(listOfOffsets), std ::end(listOfOffsets), offsetInRangeWithSameValue)); }; return std ::any_of(std ::begin(std ::get<IDX_TILE_ARRAY>(gbda)), std ::end(std ::get<IDX_TILE_ARRAY>(gbda)), canMoveToOffset); } bool canMoveOnGameboard (GameBoard& gb) { return canMoveOnGameboardDataArray(gb.gbda); }
canMoveOnGameboard()有三个辅助函数。getTileValueOnGameboardDataArray()与isPointInBoardArea()较为简单,不多赘述。重点说一下canMoveOnGameboardDataArray(),这个函数初看较为复杂——函数体内含lambda表达式,lambda表达式内又嵌套lambda表达式。共三层,我们从里往外分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const auto offsetInRangeWithSameValue = [=, &gbda](const point2D_t offset) { const auto offsetCheck = { currentPoint + offset, currentPoint - offset }; for (const auto currentOffset : offsetCheck) { if (isPointInBoardArea(currentOffset, boardSize)) { return getTileValueOnGameboardDataArray(gbda, currentOffset) == currentPointValue; } } return false ; };
最里边这层需结合第一行注释的语句理解,你给offsetInRangeWithSameValue()一个点(currentPoint)和一个偏移量({1, 0} or {0, 1}),它判断该点的上下/左右两侧是否存在与该点值相同的砖块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const auto canMoveToOffset = [=, &indexCounter, &gbda](const tile_t t) { const int boardSize = getSizeOfGameboard(gbda); const auto currentPoint = point2D_t{ indexCounter / boardSize, indexCounter % boardSize }; indexCounter++; const auto listOfOffsets = { point2D_t{1 , 0 }, point2D_t{0 , 1 } }; const auto currentPointValue = t.value; const auto offsetInRangeWithSameValue = [=](const point2D_t offset) { }; return ((currentPointValue == 0u ) || std ::any_of(std ::begin(listOfOffsets), std ::end(listOfOffsets), offsetInRangeWithSameValue)); };
这中间层嵌套需要一个tile_t对象作为参数,然后利用最里层嵌套检查该tile对象四周是否存在满足条件的对象。注意return语句中的条件:如果该tile_t对象本身处于空余状态,必然canMove。
1 2 3 4 5 6 7 8 9 10 11 12 bool canMoveOnGameboardDataArray (gameboard_data_array_t & gbda) { auto indexCounter{ 0 }; const auto canMoveToOffset = [=, &indexCounter, &gbda](const tile_t t) { }; return std ::any_of(std ::begin(std ::get<IDX_TILE_ARRAY>(gbda)), std ::end(std ::get<IDX_TILE_ARRAY>(gbda)), canMoveToOffset); }
最终,canMoveOnGameboardDataArray()使用std::any_of遍历所有tile_t对象,即可获知游戏结束与否。
接下来是一组关于游戏逻辑执行的函数。
1 2 3 4 void tumbleTilesUpOnGameboard (GameBoard& gb) ;void tumbleTilesDownOnGameboard (GameBoard& gb) ;void tumbleTilesLeftOnGameboard (GameBoard& gb) ;void tumbleTilesRightOnGameboard (GameBoard& gb) ;
游戏过程中,玩家选择一个方向后,将由这四个函数中对应方向者实现棋盘上全体砖块的移动逻辑。这四个函数的本质相同,我们挑一个来分析即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void doTumbleTilesUpOnGameboard (GameBoard& gb) { const int boardSize = getSizeOfGameboard(gb.gbda); for (auto x = 1 ; x < boardSize; x++) { auto y = 0 ; while (y < boardSize) { 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 })); } y++; } } }
以 向上移动为例,我们从上至下一层层遍历棋盘,如果当前坐标处的tile对象值非空,则通过moveOnGameboard()尝试将其上移。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 enum class COLLASPE_OR_SHIFT_T { ACTION_NONE, ACTION_COLLASPE, ACTION_SHIFT, MAX_NUM_OF_ACTIONS }; using delta_t = std ::pair <point2D_t, point2D_t>;void moveOnGameboard (GameBoard& gb, delta_t dt_point) { auto didGameboardCollaspeOrShift = bool {false }; auto actionTaken = COLLASPE_OR_SHIFT_T::ACTION_NONE; std ::tie(didGameboardCollaspeOrShift, actionTaken) = collaspedOrShiftedTilesOnGameboardDataArray(gb.gbda, dt_point); if (didGameboardCollaspeOrShift) { gb.moved = true ; if (actionTaken == COLLASPE_OR_SHIFT_T::ACTION_COLLASPE) { collaspeTilesOnGameboardDataArray(gb.gbda, dt_point); const auto targetTile = getTileOnGameboard( gb.gbda, dt_point.first + dt_point.second); updateGameBoardStats(gb, targetTile.value); } if (actionTaken == COLLASPE_OR_SHIFT_T::ACTION_SHIFT) { shiftTilesOnGameboardDataArray(gb.gbda, dt_point); } } if (checkRecursiveOffsetInGameBounds( dt_point, getSizeOfGameboard(gb.gbda))) { moveOnGameboard( gb, std ::make_pair (dt_point.first + dt_point.second, dt_point.second)); } }
moveOnGameboard()用到了几个暂时还未实现的函数,但并不影响我们理解该函数的作用。需要记住,moveOnGameboard()是针对单块砖而言的。其第一个参数是棋盘,第二个参数是个pair<point_2D, point_2D>类型对象(第一个分量代表该本砖块坐标,第二个分量代表所欲移动方向的偏移量)。
一块砖若要能够向指定方向移动一格,要么该方向上相邻的砖块value为0,则可直接移动过去(SHIFT);要么该方向上相邻的砖块value值与本砖块value相当,则可以合二为一(COLLASPE)。
本函数前三分之二篇幅的代码正是检测该砖是否满足上述两种情况之一,并对各情况执行相应行动,而余下三分之一代码:
1 2 3 4 5 6 if (checkRecursiveOffsetInGameBounds( dt_point, getSizeOfGameboard(gb.gbda))) { moveOnGameboard( gb, std ::make_pair (dt_point.first + dt_point.second, dt_point.second)); }
如果没有这段代码,我们的游戏逻辑将会有缺陷,举例如下:
1 2 3 4 (0 , 0 ) (0 , 1 ) (0 , 2 ) (0 , 3 ) (1 , 0 ) (1 , 1 ) (1 , 2 ) (1 , 3 ) (2 , 0 ) (2 , 1 ) (2 , 2 ) (2 , 3 ) (3 , 0 ) (3 , 1 ) (3 , 2 ) (3 , 3 )
假设用户Mike选择4 x 4规格棋盘,开局在坐标(0, 0)处有值2,其余位置value均为0。此时,Mike键入命令,让地砖向右移动。我们期待(0, 0)处的值能最终落位(0, 3)处,但实际其只停留在(0, 1)处。因为我们的moveOnGameboard()判断可以右移后只会移动一格而不再检测能否继续右移。需要补充一个机制:若地砖能向某方向移动,则不能只是走一个,而必须走到底,所谓“不撞南墙不回头”。
以下是checkRecursiveOffsetInGameBounds()定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 bool checkRecursiveOffsetInGameBounds (delta_t dt_point, int boardSize) { int focal_x, focal_y, offset_x, offset_y; std ::tie(focal_x, focal_y) = dt_point.first.get(); std ::tie(offset_x, offset_y) = dt_point.second.get(); const auto positiveDirection = (offset_y + offset_x == 1 ); const auto negativeDirection = (offset_y + offset_x == -1 ); const auto is_positive_y_direction_flagged = (offset_y == 1 ); const auto is_negative_y_direction_flagged = (offset_y == -1 ); const auto isInsideOuterBounds = (positiveDirection && (is_positive_y_direction_flagged ? focal_y : focal_x) < boardSize - 2 ); const auto isInsideInnerBounds = (negativeDirection && (is_negative_y_direction_flagged ? focal_y : focal_x) > 1 ); return (isInsideOuterBounds || isInsideInnerBounds); }
该函数检测一个tile_t对象是否能向指定方向移动,若是,则返回true,于是在moveOnGameboard()里递归地调用moveOnGameboard()让地砖移动下去,直至不可再移。如此一来游戏逻辑便如我们期待般一样。
moveOnGameboard()中所涉及的另几个函数定义如下,功能都相对简单,不多赘述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 using bool_collaspe_shift_t = std ::tuple<bool , COLLASPE_OR_SHIFT_T>;bool_collaspe_shift_t collaspedOrShiftedTilesOnGameboardDataArray(gameboard_data_array_t gbda, delta_t dt_point) { const auto currentTile = getTileOnGameboard(gbda, dt_point.first); const auto targetTile = getTileOnGameboard(gbda, dt_point.first + dt_point.second); const auto valueExistInTargetPoint = targetTile.value; const auto isValueSameAsTargetValue = (currentTile.value == targetTile.value); const auto noTilesAreBlocked = (!currentTile.blocked && !targetTile.blocked); const auto is_there_a_current_value_but_no_target_value = (currentTile.value && !targetTile.value); const auto doCollapse = (valueExistInTargetPoint && isValueSameAsTargetValue && noTilesAreBlocked); const auto doShift = is_there_a_current_value_but_no_target_value; const auto action_taken = (doCollapse || doShift); if (doCollapse) { return std ::make_tuple(action_taken, COLLASPE_OR_SHIFT_T::ACTION_COLLASPE); } else if (doShift) { 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 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 = getTileOnGameboard(gbda, dt_point.first); tile_t targetTile = getTileOnGameboard(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 ; } bool updateGameBoardStats (GameBoard& gb, ull target_tile_value) { gb.score += target_tile_value; gb.largestTile = std ::max(gb.largestTile, target_tile_value); if (!hasWonOnGameboard(gb)) { constexpr auto GAME_TILE_WINNING_SCORE = 2048 ; if (target_tile_value == GAME_TILE_WINNING_SCORE) { gb.win = true ; } } return true ; }
至此,游戏底层逻辑这一重要部分基本完结。
最后补充一个测试用函数printStateOfGameBoardDataArray(),该函数同样声明定义于gameboard.*内,用于开发过程中监控测试棋盘状态信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 std ::string printStateOfGameBoardDataArray (gameboard_data_array_t gbda) { const int boardSize = getSizeOfGameboard(gbda); std ::ostringstream os; for (auto x = 0 ; x < boardSize; x++) { for (auto y = 0 ; y < boardSize; y++) { const auto current_point = point2D_t{ x, y }; os << getTileValueOnGameboardDataArray(gbda, current_point) << ":" << getTileBlockedOnGameboardDataArray(gbda, current_point) << "," ; } os << "\n" ; } return os.str(); } std ::string printStateOfGameBoard (GameBoard gb) { return printStateOfGameBoardDataArray(gb.gbda); }
代码 由于现在项目愈加庞大,只展示本阶段新增、更新所涉及的文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #ifndef GAME_INPUT_H #define GAME_INPUT_H namespace Game{ namespace Input { namespace Keypress { namespace Code { enum { CODE_HOTKEY_PREGAME_BACK_TO_MENU = 0 , }; } } } } #endif
game-pregame.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 #include "game-pregame.hpp" #include "game-graphics.hpp" #include "game-input.hpp" #include "gameboard.hpp" #include "menu.hpp" #include "global.hpp" #include <array> #include <sstream> #include <iostream> #include <limits> namespace Game{ namespace { enum PreGameSetupStatusFlag { FLAG_NULL, FLAG_START_GAME, FLAG_RETURN_TO_MAIN_MENU, MAX_NO_PREGAME_SETUP_STATUS_FLAGS }; using pregameesetup_status_t = std ::array <bool , MAX_NO_PREGAME_SETUP_STATUS_FLAGS>; pregameesetup_status_t pregamesetup_status{}; enum class NewGameFlag { NewGameFlagNull, NoPreviousSaveAvailable }; bool noSave{ false }; bool flagInputErroneousChoice{ false }; ull storedGameBoardSize{ 1 }; int receiveGameBoardSize (std ::istream& is) { int playerInputBoardSize{ 0 }; if (!(is >> playerInputBoardSize)) { constexpr auto INVALID_INPUT_VALUE_FLAG = -1 ; playerInputBoardSize = INVALID_INPUT_VALUE_FLAG; is.clear(); is.ignore(std ::numeric_limits<std ::streamsize>::max(), '\n' ); } return playerInputBoardSize; } void receiveInputFromPlayer (std ::istream& is) { using namespace Input::Keypress::Code; flagInputErroneousChoice = bool { false }; const auto gbsize = receiveGameBoardSize(is); const auto isValidBoardSize = (gbsize >= MIN_GAME_BOARD_PLAY_SIZE) && (gbsize <= MAX_GAME_BOARD_PLAY_SIZE); if (isValidBoardSize) { storedGameBoardSize = gbsize; pregamesetup_status[FLAG_START_GAME] = true ; } bool goBackToMainMenu{ true }; switch (gbsize) { case CODE_HOTKEY_PREGAME_BACK_TO_MENU: pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU] = true ; break ; default : goBackToMainMenu = false ; break ; } if (!isValidBoardSize && !goBackToMainMenu) { flagInputErroneousChoice = true ; } } void processPreGame () { if (pregamesetup_status[FLAG_START_GAME]) { } if (pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU]) { Menu::startMenu(); } } bool soloLoop () { bool invalidInputValue = flagInputErroneousChoice; const auto questionAboutGameBoardSizePrompt = [&invalidInputValue]() { std ::ostringstream str_os; DrawOnlyWhen(str_os, invalidInputValue, Graphics::BoardSizeErrorPrompt); DrawAlways(str_os, Graphics::BoardInputPrompt); return str_os.str(); }; pregamesetup_status = pregameesetup_status_t {}; clearScreen(); DrawAlways(std ::cout , Game::Graphics::AsciiArt2048); DrawAsOneTimeFlag(std ::cout , noSave, Graphics::GameBoardNoSaveErrorPrompt); DrawAlways(std ::cout , questionAboutGameBoardSizePrompt); receiveInputFromPlayer(std ::cin ); processPreGame(); return flagInputErroneousChoice; } void endlessLoop () { while (soloLoop()) ; } void SetupNewGame (NewGameFlag ns) { noSave = (ns == NewGameFlag::NoPreviousSaveAvailable) ? true : false ; endlessLoop(); } } namespace PreGameSetup { void SetupNewGame () { SetupNewGame(NewGameFlag::NewGameFlagNull); } } }
game-graphics.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 #include "game-graphics.hpp" #include "color.hpp" #include <sstream> namespace Game{ namespace Graphics { std ::string AsciiArt2048 () { constexpr auto title_card_2048 = R"( /\\\\\\\\\ /\\\\\\\ /\\\ /\\\\\\\\\ /\\\///////\\\ /\\\/////\\\ /\\\\\ /\\\///////\\\ \/// \//\\\ /\\\ \//\\\ /\\\/\\\ \/\\\ \/\\\ /\\\/ \/\\\ \/\\\ /\\\/\/\\\ \///\\\\\\\\\/ /\\\// \/\\\ \/\\\ /\\\/ \/\\\ /\\\///////\\\ /\\\// \/\\\ \/\\\ /\\\\\\\\\\\\\\\\ /\\\ \//\\\ /\\\/ \//\\\ /\\\ \///////////\\\// \//\\\ /\\\ /\\\\\\\\\\\\\\\ \///\\\\\\\/ \/\\\ \///\\\\\\\\\/ \/////////////// \/////// \/// \///////// )" ; std ::ostringstream title_card_richtext; title_card_richtext << green << bold_on << title_card_2048 << bold_off << def; title_card_richtext << "\n\n\n" ; return title_card_richtext.str(); } std ::string BoardSizeErrorPrompt () { const auto invalid_prompt_text = { "Invalid input. Gameboard size should range from " , " to " , "." }; 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(); } 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 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(); } } }
tile.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #ifndef TILE_H #define TILE_H #include "global.hpp" namespace Game{ struct tile_t { ull value{ 0 }; bool blocked{ false }; }; } #endif
point2d.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #ifndef POINT2D_H #define POINT2D_H #include <tuple> class point2D_t { using point_datatype_t = typename std ::tuple<int , int >; point_datatype_t pointVector{ 0 , 0 }; explicit point2D_t (const point_datatype_t pt) : pointVector { pt } { } public : enum class PointCoord { COORD_X, COORD_Y, }; point2D_t() = default ; point2D_t(const int x, const int y) : point2D_t(std ::make_tuple(x, y)) { } template <PointCoord dimension> int get () const { return std ::get<static_cast <int >(dimension)>(pointVector); } template <PointCoord dimension> void set (int val) { std ::get<static_cast <int >(dimension)>(pointVector) = val; } point_datatype_t get () const { return pointVector; } void set (point_datatype_t val) { pointVector = val; } void set (const int x, const int y) { set (std ::make_tuple(x, y)); } point2D_t& operator +=(const point2D_t& rhs) { this ->pointVector = std ::make_tuple( get<PointCoord::COORD_X>() + rhs.get<PointCoord::COORD_X>(), get<PointCoord::COORD_Y>() + rhs.get<PointCoord::COORD_Y>()); return *this ; } point2D_t& operator -=(const point2D_t& rhs) { this ->pointVector = std ::make_tuple( get<PointCoord::COORD_X>() - rhs.get<PointCoord::COORD_X>(), get<PointCoord::COORD_Y>() - rhs.get<PointCoord::COORD_Y>()); return *this ; } }; inline point2D_t operator +(point2D_t lhs, const point2D_t& rhs) { lhs += rhs; return lhs; } inline point2D_t operator -(point2D_t lhs, const point2D_t& rhs) { lhs -= rhs; return lhs; } #endif
gameboard.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #ifndef GAMEBOARD_H #define GAMEBOARD_H #include "tile.hpp" #include <tuple> #include <vector> struct point2D_t ;namespace Game{ struct GameBoard { using tile_data_array_t = std ::vector <tile_t >; using gameboard_data_array_t = std ::tuple<size_t , tile_data_array_t >; gameboard_data_array_t gbda; bool win{ false }; bool moved{ false }; ull score{ 0 }; ull largestTile{ 2 }; long long moveCount{ -1 }; GameBoard() = default ; explicit GameBoard (ull boardsize) ; explicit GameBoard (ull boardsize, tile_data_array_t existboard) ; }; size_t getSizeOfGameboard (GameBoard::gameboard_data_array_t gbda) ; tile_t getTileOnGameboard (GameBoard::gameboard_data_array_t & gbda, point2D_t pt) ; bool hasWonOnGameboard (GameBoard& gb) ; long long MoveCountOnGameBoard (GameBoard& gb) ; void registerMoveByOneOnGameboard (GameBoard& gb) ; bool addTileOnGameboard (GameBoard& gb) ; void unblockTilesOnGameboard (GameBoard& gb) ; bool canMoveOnGameboard (GameBoard& gb) ; void tumbleTilesUpOnGameboard (GameBoard& gb) ; void tumbleTilesDownOnGameboard (GameBoard& gb) ; void tumbleTilesLeftOnGameboard (GameBoard& gb) ; void tumbleTilesRightOnGameboard (GameBoard& gb) ; std ::string printStateOfGameBoard (GameBoard gb) ; } #endif
gameboard.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 #include "gameboard.hpp" #include "tile.hpp" #include "point2d.hpp" #include <chrono> #include <random> #include <sstream> #include <algorithm> #include <array> namespace Game{ namespace { using gameboard_data_array_t = GameBoard::gameboard_data_array_t ; enum gameboard_data_array_fields { IDX_BOARDSIZE, IDX_TILE_ARRAY, MAX_NO_INDEX }; struct gameboard_data_point_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 * getSizeOfGameboard(gbda) + y; } tile_t operator () (gameboard_data_array_t & gbda, point2D_t& pt) const { return std ::get<IDX_TILE_ARRAY>(gbda)[point2D_to_1D_index(gbda, pt)]; } tile_t & operator () (gameboard_data_array_t & gbda, point2D_t& pt) { return std ::get<IDX_TILE_ARRAY>(gbda)[point2D_to_1D_index(gbda, pt)]; } }; bool getTileBlockedOnGameboardDataArray (gameboard_data_array_t gbda, point2D_t pt) { return gameboard_data_point_t {}(gbda, pt).blocked; } void setTileOnGameboardDataArray (gameboard_data_array_t & gbda, point2D_t pt, tile_t tile) { gameboard_data_point_t {}(gbda, pt) = tile; } void setTileValueOnGameboardDataArray (gameboard_data_array_t & gbda, point2D_t pt, ull value) { gameboard_data_point_t {}(gbda, pt).value = value; } ull getTileValueOnGameboardDataArray (gameboard_data_array_t & gbda, point2D_t pt) { return gameboard_data_point_t {}(gbda, pt).value; } std ::vector <size_t > collectFreeTilesOnGameboardDataArray(gameboard_data_array_t gbda) { std ::vector <size_t > freeTiles; auto index_counter{ 0 }; for (const auto t : std ::get<IDX_TILE_ARRAY>(gbda)) { if (!t.value) { freeTiles.push_back(index_counter); } index_counter++; } return freeTiles; } gameboard_data_array_t unblockTilesOnGameboardDataArray(gameboard_data_array_t gbda) { using tile_data_array_t = GameBoard::tile_data_array_t ; auto new_board_data_array = tile_data_array_t (std ::get<IDX_TILE_ARRAY>(gbda).size()); std ::transform(std ::begin(std ::get<IDX_TILE_ARRAY>(gbda)), std ::end(std ::get<IDX_TILE_ARRAY>(gbda)), std ::begin(new_board_data_array), [](const tile_t t) { return tile_t { t.value, false }; }); return gameboard_data_array_t { std ::get<IDX_BOARDSIZE>(gbda), new_board_data_array }; } bool isPointInBoardArea (point2D_t pt, int boardSize) { int x, y; std ::tie(x, y) = pt.get(); return !(y < 0 || y > boardSize - 1 || x < 0 || x > boardSize - 1 ); } bool canMoveOnGameboardDataArray (gameboard_data_array_t & gbda) { auto indexCounter{ 0 }; const auto canMoveToOffset = [=, &indexCounter, &gbda](const tile_t t) { const int boardSize = getSizeOfGameboard(gbda); const auto currentPoint = point2D_t{ indexCounter / boardSize, indexCounter % boardSize }; indexCounter++; const auto listOfOffsets = { point2D_t{1 , 0 }, point2D_t{0 , 1 } }; const auto currentPointValue = t.value; const auto offsetInRangeWithSameValue = [=, &gbda](const point2D_t offset) { const auto offsetCheck = { currentPoint + offset, currentPoint - offset }; for (const auto currentOffset : offsetCheck) { if (isPointInBoardArea(currentOffset, boardSize)) { return getTileValueOnGameboardDataArray(gbda, currentOffset) == currentPointValue; } } return false ; }; return ((currentPointValue == 0u ) || std ::any_of(std ::begin(listOfOffsets), std ::end(listOfOffsets), offsetInRangeWithSameValue)); }; return std ::any_of(std ::begin(std ::get<IDX_TILE_ARRAY>(gbda)), std ::end(std ::get<IDX_TILE_ARRAY>(gbda)), canMoveToOffset); } class RandInt { public : using clock = std ::chrono::system_clock; 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 re; std ::uniform_int_distribution<> dist; }; bool addTileOnGameboardDataArray (gameboard_data_array_t & gbda) { constexpr auto CHANCE_OF_VALUE_FOUR_OVER_TWO = 75 ; const auto index_list_of_free_tiles = collectFreeTilesOnGameboardDataArray(gbda); if (!index_list_of_free_tiles.size()) { return true ; } const int boardSize = getSizeOfGameboard(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 / boardSize, rand_selected_index % boardSize }; const auto value_four_or_two = RandInt{}() % 100 > CHANCE_OF_VALUE_FOUR_OVER_TWO ? 4 : 2 ; setTileValueOnGameboardDataArray(gbda, rand_index_as_point_t , value_four_or_two); return false ; } enum class COLLASPE_OR_SHIFT_T { ACTION_NONE, ACTION_COLLASPE, ACTION_SHIFT, MAX_NUM_OF_ACTIONS }; using delta_t = std ::pair <point2D_t, point2D_t>; using bool_collaspe_shift_t = std ::tuple<bool , COLLASPE_OR_SHIFT_T>; bool_collaspe_shift_t collaspedOrShiftedTilesOnGameboardDataArray(gameboard_data_array_t & gbda, delta_t dt_point) { const auto currentTile = getTileOnGameboard(gbda, dt_point.first); const auto targetTile = getTileOnGameboard(gbda, dt_point.first + dt_point.second); const auto valueExistInTargetPoint = targetTile.value; const auto isValueSameAsTargetValue = (currentTile.value == targetTile.value); const auto noTilesAreBlocked = (!currentTile.blocked && !targetTile.blocked); const auto is_there_a_current_value_but_no_target_value = (currentTile.value && !targetTile.value); const auto doCollapse = (valueExistInTargetPoint && isValueSameAsTargetValue && noTilesAreBlocked); const auto doShift = is_there_a_current_value_but_no_target_value; const auto action_taken = (doCollapse || doShift); if (doCollapse) { return std ::make_tuple(action_taken, COLLASPE_OR_SHIFT_T::ACTION_COLLASPE); } else if (doShift) { 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 collaspeTilesOnGameboardDataArray (gameboard_data_array_t & gbda, delta_t dt_point) { tile_t currentTile = getTileOnGameboard(gbda, dt_point.first); tile_t targetTile = getTileOnGameboard(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 = getTileOnGameboard(gbda, dt_point.first); tile_t targetTile = getTileOnGameboard(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 ; } bool updateGameBoardStats (GameBoard& gb, ull target_tile_value) { gb.score += target_tile_value; gb.largestTile = std ::max(gb.largestTile, target_tile_value); if (!hasWonOnGameboard(gb)) { constexpr auto GAME_TILE_WINNING_SCORE = 2048 ; if (target_tile_value == GAME_TILE_WINNING_SCORE) { gb.win = true ; } } return true ; } bool checkRecursiveOffsetInGameBounds (delta_t dt_point, int boardSize) { int focal_x, focal_y, offset_x, offset_y; std ::tie(focal_x, focal_y) = dt_point.first.get(); std ::tie(offset_x, offset_y) = dt_point.second.get(); const auto positiveDirection = (offset_y + offset_x == 1 ); const auto negativeDirection = (offset_y + offset_x == -1 ); const auto is_positive_y_direction_flagged = (offset_y == 1 ); const auto is_negative_y_direction_flagged = (offset_y == -1 ); const auto isInsideOuterBounds = (positiveDirection && (is_positive_y_direction_flagged ? focal_y : focal_x) < boardSize - 2 ); const auto isInsideInnerBounds = (negativeDirection && (is_negative_y_direction_flagged ? focal_y : focal_x) > 1 ); return (isInsideOuterBounds || isInsideInnerBounds); } void moveOnGameboard (GameBoard& gb, delta_t dt_point) { auto didGameboardCollaspeOrShift = bool {false }; auto actionTaken = COLLASPE_OR_SHIFT_T::ACTION_NONE; std ::tie(didGameboardCollaspeOrShift, actionTaken) = collaspedOrShiftedTilesOnGameboardDataArray(gb.gbda, dt_point); if (didGameboardCollaspeOrShift) { gb.moved = true ; if (actionTaken == COLLASPE_OR_SHIFT_T::ACTION_COLLASPE) { collaspeTilesOnGameboardDataArray(gb.gbda, dt_point); const auto targetTile = getTileOnGameboard( gb.gbda, dt_point.first + dt_point.second); updateGameBoardStats(gb, targetTile.value); } if (actionTaken == COLLASPE_OR_SHIFT_T::ACTION_SHIFT) { shiftTilesOnGameboardDataArray(gb.gbda, dt_point); } } if (checkRecursiveOffsetInGameBounds( dt_point, getSizeOfGameboard(gb.gbda))) { moveOnGameboard( gb, std ::make_pair (dt_point.first + dt_point.second, dt_point.second)); } } void doTumbleTilesUpOnGameboard (GameBoard& gb) { const int boardSize = getSizeOfGameboard(gb.gbda); for (auto x = 1 ; x < boardSize; x++) { auto y = 0 ; while (y < boardSize) { 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 })); } y++; } } } void doTumbleTilesDownOnGameboard (GameBoard& gb) { const int boardSize = getSizeOfGameboard(gb.gbda); for (auto x = boardSize - 2 ; x >= 0 ; x--) { auto y = 0 ; while (y < boardSize) { 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 })); } y++; } } } void doTumbleTilesLeftOnGameboard (GameBoard& gb) { const int boardSize = getSizeOfGameboard(gb.gbda); for (auto y = 1 ; y < boardSize; y++) { auto x = 0 ; while (x < boardSize) { 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 })); } x++; } } } void doTumbleTilesRightOnGameboard (GameBoard& gb) { const int boardSize = getSizeOfGameboard(gb.gbda); for (auto y = boardSize - 2 ; y >= 0 ; y--) { auto x = 0 ; while (x < boardSize) { 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 })); } x++; } } } std ::string printStateOfGameBoardDataArray (gameboard_data_array_t gbda) { const int boardSize = getSizeOfGameboard(gbda); std ::ostringstream os; for (auto x = 0 ; x < boardSize; x++) { for (auto y = 0 ; y < boardSize; y++) { const auto current_point = point2D_t{ x, y }; os << getTileValueOnGameboardDataArray(gbda, current_point) << ":" << getTileBlockedOnGameboardDataArray(gbda, current_point) << "," ; } os << "\n" ; } return os.str(); } } GameBoard::GameBoard(ull boardsize) : GameBoard{ boardsize, tile_data_array_t (boardsize * boardsize)} { } GameBoard::GameBoard(ull boardsize, tile_data_array_t existboard) : gbda{ boardsize, existboard } { } size_t getSizeOfGameboard (gameboard_data_array_t gbda) { return std ::get<IDX_BOARDSIZE>(gbda); } tile_t getTileOnGameboard (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; } void registerMoveByOneOnGameboard (GameBoard& gb) { gb.moveCount++; gb.moved = false ; } bool addTileOnGameboard (GameBoard& gb) { return addTileOnGameboardDataArray(gb.gbda); } void unblockTilesOnGameboard (GameBoard& gb) { gb.gbda = unblockTilesOnGameboardDataArray(gb.gbda); } bool canMoveOnGameboard (GameBoard& gb) { return canMoveOnGameboardDataArray(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); } }
阶段四 构建 解决了一系列游戏底层逻辑相关问题后,现在我们着手完善完整的游戏过程。
game-pregame.cpp
1 2 3 4 5 6 7 8 9 10 11 12 void processPreGame () { if (pregamesetup_status[FLAG_START_GAME]) { playGame(PlayGameFlag::BrandNewGame, GameBoard{ storedGameBoardSize }, storedGameBoardSize); } if (pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU]) { Menu::startMenu(); } }
终于,可以开始游戏啦!playGame()函数声明于game.hpp。
game.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #ifndef GAME_H #define GAME_H namespace Game { struct GameBoard ; enum class PlayGameFlag { BrandNewGame, ContinuePreviousGame }; void playGame (PlayGameFlag flag, GameBoard gb, unsigned long long userInput_PlaySize = 1 ) ; void startGame () ; }; #endif
接下来实现在game.cpp中定义实现playGame()函数功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include "game.hpp" #include "game-pregame.hpp" #include "gameboard.hpp" #include "global.hpp" #include "statistics.hpp" #include <chrono> namespace Game{ enum { COMPETITION_GAME_BOARD_SIZE = 4 }; namespace { using competition_mode_t = bool ; GameBoard endlessGameLoop (ull currentBestScore, competition_mode_t cm, GameBoard gb) { } } void playGame (PlayGameFlag flag, GameBoard gb, ull boardSize) { const auto isThisNewlyGame = (flag == PlayGameFlag::BrandNewGame); const auto isCompetitionMode = (boardSize == COMPETITION_GAME_BOARD_SIZE); const auto bestScore = Statistics::loadBestScore(); if (isThisNewlyGame) { gb = GameBoard(boardSize); addTileOnGameboard(gb); } const auto startTime = std ::chrono::high_resolution_clock::now(); gb = endlessGameLoop(bestScore, isCompetitionMode, gb); const auto finishTime = std ::chrono::high_resolution_clock::now(); const std ::chrono::duration<double > elapsed = finishTime - startTime; const auto duration = elapsed.count(); if (isThisNewlyGame) { } } void startGame () { PreGameSetup::SetupNewGame(); } }
我们从上至下逐步分析。
1 enum { COMPETITION_GAME_BOARD_SIZE = 4 };
4 x 4 规格的棋盘是我们的竞技专用规格,我们也只实现该规格棋盘的存储。
1 2 3 4 void playGame (PlayGameFlag flag, GameBoard gb, ull boardSize) { ... }
我们通过PlayGameFlag变量来区分这是一局新游戏还是继续之前的游戏。
1 const auto bestScore = Statistics::loadBestScore();
变量bestScore记录当前得分,如果是新游戏当前得分应为0,但若是继续老游戏,则应从本地数据库中加载之前的得分数据。loadBestScore()函数我们稍后实现。
1 2 3 4 5 const auto startTime = std ::chrono::high_resolution_clock::now();const auto finishTime = std ::chrono::high_resolution_clock::now();const std ::chrono::duration<double > elapsed = finishTime - startTime;const auto duration = elapsed.count();
这几行代码负责记录本次游戏花费的时间,endlessGameLoop()负责游戏过程逻辑实现。
在开始构建endlessGameLoop()之前,我们先将完善关于游戏数据相关的部分,实现statistics.*如下:
statistics.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #ifndef STATISTICS_H #define STATISTICS_H #include "global.hpp" #include <tuple> #include <string> #include <iosfwd> namespace Statistics{ struct total_game_stats_t { ull bestScore{}; ull totalMoveCount{}; int gameCount{}; double totalDuration{}; int winCount{}; }; using load_stats_status_t = std ::tuple<bool , total_game_stats_t >; load_stats_status_t loadFromFileStatistics (std ::string filename) ; ull loadBestScore () ; } std ::istream& operator >>(std ::istream& is, Statistics::total_game_stats_t & s);std ::ostream& operator <<(std ::ostream& os, Statistics::total_game_stats_t & s);#endif
statistics.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include "statistics.hpp" #include <fstream> namespace Statistics{ namespace { total_game_stats_t generateStatsFromInputData (std ::istream& is) { total_game_stats_t stats; is >> stats; return stats; } } load_stats_status_t loadFromFileStatistics (std ::string filename) { std ::ifstream statistics (filename) ; if (statistics) { total_game_stats_t state = generateStatsFromInputData(statistics); return load_stats_status_t { true , state }; } return load_stats_status_t { false , total_game_stats_t {} }; } ull loadBestScore () { total_game_stats_t stats; bool stats_file_loaded{ false }; ull tmpScore{ 0 }; std ::tie(stats_file_loaded, stats) = loadFromFileStatistics("../data/statistics.txt" ); if (stats_file_loaded) { tmpScore = stats.bestScore; } return tmpScore; } } using namespace Statistics;std ::istream& operator >>(std ::istream& is, total_game_stats_t & s) { is >> s.bestScore >> s.gameCount >> s.winCount >> s.totalMoveCount >> s.totalDuration; return is; } std ::ostream& operator <<(std ::ostream& os, total_game_stats_t & s) { os << s.bestScore << "\n" << s.gameCount << "\n" << s.winCount << "\n" << s.totalMoveCount << "\n" << s.totalDuration; return os; }
以上内容都较为简单易懂,不再赘述。
再回过头完善playGame()函数。
1 2 3 4 5 6 7 8 9 10 11 12 void playGame (PlayGameFlag flag, GameBoard gb, ull boardSize) { if (isThisNewlyGame) { const auto finalscore = makeFinalscoreFromGameSession(duration, gb); doPostGameSaveStuff(finalscore, isCompetitionMode); } }
这个判断语句游戏结束后执行,玩家完成一局新游戏后,doPostGameSaveStuff()记录玩家姓名、游戏得分得分等数据。
每次游戏时都需记录玩家得分、移动步数、总耗时等基本信息,因此我们需要一个计分板来负责计分事宜。
scoreboard.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #ifndef SCOREBOARD_H #define SCOREBOARD_H #include "global.hpp" #include <string> namespace Scoreboard{ struct Score { std ::string name; ull score; bool win; ull largestTile; long long moveCount; double duration; }; } #endif
makeFinalscoreFromGameSession()定义于game.cpp的匿名空间内,使用一个Scoreboard::Score对象记录对局信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 Scoreboard::Score makeFinalscoreFromGameSession (double duration, GameBoard gb) { Scoreboard::Score finalscore{}; finalscore.score = gb.score; finalscore.win = hasWonOnGameboard(gb); finalscore.moveCount = MoveCountOnGameBoard(gb); finalscore.largestTile = gb.largestTile; finalscore.duration = duration; return finalscore; }
而doPostGameSaveStuff()则负责询问玩家姓名、记录本轮游戏信息等事宜,该函数同样定义于game.cpp的匿名空间内。
1 2 3 4 5 6 7 8 void doPostGameSaveStuff (Scoreboard::Score finalscore, competition_mode_t cm) { if (cm) { Statistics::createFinalScoreAndEndGameDataFile(std ::cout , std ::cin , finalscore); } }
可以看到,只有在玩家选择竞技模式,即4 x 4大小情况下,才会调用createFinalScoreAndEndGameDataFile()记录本轮游戏数据。
我们接着前往statistics.cpp查看createFinalScoreAndEndGameDataFile()定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Scoreboard::Graphics::finalscore_display_data_t makeFinalScoreDisplayData(Scoreboard::Score finalscore) { const auto fsdd = std ::make_tuple( std ::to_string(finalscore.score), std ::to_string(finalscore.largestTile), std ::to_string(finalscore.moveCount), secondsFormat(finalscore.duration)); return fsdd; } void createFinalScoreAndEndGameDataFile (std ::ostream& os, std ::istream& is, Scoreboard::Score finalscore) { const auto finalscoreDisplayData = makeFinalScoreDisplayData(finalscore); DrawAlways(os, DataSuppliment(finalscoreDisplayData, Scoreboard::Graphics::EndGameStatisticsPrompt)); DrawAlways(os, Graphics::AskForPlayerNamePrompt); const auto playerName = receiveInputPlayerName(is); finalscore.name = playerName; Scoreboard::saveScore(finalscore); saveEndGameStats(finalscore); DrawAlways(os, Graphics::MessageScoreSavedPrompt); }
secondsFormat()定义于global.cpp,将游玩时间转化为hh : mm : ss 制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 std ::string secondsFormat (double sec) { double second = sec; int minute = second / 60 ; int hour = minute / 60 ; second -= minute * 60 ; minute %= 60 ; second = static_cast <int >(second); std ::ostringstream oss; if (hour) { oss << hour << "h " ; } if (minute) { oss << minute << "m " ; } oss << second << "s" ; return oss.str(); }
saveScore()函数定于于scoreboard.cpp内。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bool generateFilefromScoreData (std ::ostream& os, Score score) { os << score; return true ; } bool saveToFileScore (std ::string filename, Score s) { std ::ofstream os (filename, std ::ios_base::app) ; return generateFilefromScoreData(os, s); } void saveScore (Score finalscore) { saveToFileScore("../data/scores.txt" , finalscore); }
generateFilefromScoreData()涉及到<<运算符重载,我们稍后实现。
创建scoreboard-graphics.*负责计分板相关图像输出。
scoreboard-graphics.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #ifndef SCOREBOARD_GRAPHICS_H #define SCOREBOARD_GRAPHICS_H #include <string> #include <tuple> namespace Scoreboard{ namespace Graphics { using finalscore_display_data_t = std ::tuple<std ::string , std ::string , std ::string , std ::string >; std ::string EndGameStatisticsPrompt (finalscore_display_data_t finalscore) ; } } #endif
scoreboard-graphics.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include "scoreboard-graphics.hpp" #include "global.hpp" #include "color.hpp" #include <sstream> #include <iomanip> // std::setw #include <array> // std::begin namespace Scoreboard{ namespace Graphics { std ::string EndGameStatisticsPrompt (finalscore_display_data_t finalscore) { std ::ostringstream str_os; constexpr auto stats_title_text = "STATISTICS" ; constexpr auto divider_text = "──────────" ; constexpr auto sp = " " ; const auto stats_attributes_text = { "Final score:" , "Largest Tile:" , "Number of moves:" , "Time taken:" }; enum FinalScoreDisplayDataFields { IDX_FINAL_SCORE_VALUE, IDX_LARGEST_TILE, IDX_MOVE_COUNT, IDX_DURATION, MAX_NUM_OF_FINALSCOREDISPLAYDATA_INDEXES }; const auto data_stats = std ::array <std ::string , MAX_NUM_OF_FINALSCOREDISPLAYDATA_INDEXES>{ std ::get<IDX_FINAL_SCORE_VALUE>(finalscore), std ::get<IDX_LARGEST_TILE>(finalscore), std ::get<IDX_MOVE_COUNT>(finalscore), std ::get<IDX_DURATION>(finalscore) }; std ::ostringstream stats_richtext; stats_richtext << yellow << sp << stats_title_text << def << "\n" ; stats_richtext << yellow << sp << divider_text << def << "\n" ; auto counter{ 0 }; const auto populate_stats_info = [=, &counter, &stats_richtext](const std ::string ) { stats_richtext << sp << std ::left << std ::setw(19 ) << std ::begin(stats_attributes_text)[counter] << bold_on << std ::begin(data_stats)[counter] << bold_off << "\n" ; counter++; }; for (const auto s : stats_attributes_text) { populate_stats_info(s); } str_os << stats_richtext.str(); str_os << "\n\n" ; return str_os.str(); } } }
EndGameStatisticsPrompt()函数返回由玩家本轮游戏各项数据所组成的字符串。
继续分析createFinalScoreAndEndGameDataFile()函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void createFinalScoreAndEndGameDataFile (std ::ostream& os, std ::istream& is, Scoreboard::Score finalscore) { DrawAlways(os, Graphics::AskForPlayerNamePrompt); const auto playerName = receiveInputPlayerName(is); finalscore.name = playerName; Scoreboard::saveScore(finalscore); saveEndGameStats(finalscore); DrawAlways(os, Graphics::MessageScoreSavedPrompt); }
展示完本轮游戏各项数据后,请玩家输入其姓名以保存本轮游戏数据,然后将包含玩家姓名在内的数据保存本地。
receiveInputPlayerName()与saveEndGameStats()均定义在statistics.cpp。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 std ::string receiveInputPlayerName (std ::istream& is) { std ::string playerName; is >> playerName; return playerName; } bool generateFilefromStatsData (std ::ostream& os, total_game_stats_t stats) { os << stats; return true ; } bool saveToFileEndGameStatistics (std ::string filename, total_game_stats_t s) { std ::ofstream filedata (filename) ; return generateFilefromStatsData(filedata, s); } void saveEndGameStats (Scoreboard::Score finalscore) { total_game_stats_t stats; std ::tie(std ::ignore, stats) = loadFromFileStatistics("../data/statistics.txt" ); stats.bestScore = stats.bestScore < finalscore.score ? finalscore.score : stats.bestScore; stats.gameCount++; stats.winCount = finalscore.win ? stats.winCount + 1 : stats.winCount; stats.totalMoveCount += finalscore.moveCount; stats.totalDuration += finalscore.duration; saveToFileEndGameStatistics("../data/statistics.txt" , stats); }
generateFilefromStatsData()中语句:
stats为total_game_stats_t类型对象,需重载<<运算符,同时,从文件读入数据时亦需重载>>运算符,在此一并完成。两重载函数均声明定义于statistics.*。
statistics.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 using namespace Statistics;std ::istream& operator >>(std ::istream& is, total_game_stats_t & s) { is >> s.bestScore >> s.gameCount >> s.winCount >> s.totalMoveCount >> s.totalDuration; return is; } std ::ostream& operator <<(std ::ostream& os, total_game_stats_t & s) { os << s.bestScore << "\n" << s.gameCount << "\n" << s.winCount << "\n" << s.totalMoveCount << "\n" << s.totalDuration; return os; }
最后提示用户输入姓名的AskForPlayerNamePrompt()函数与显示保存数据成功信息的MessageScoreSavedPrompt()函数均声明定义于statistics-graphics.*。
statistics-graphics.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 std ::string AskForPlayerNamePrompt () { constexpr auto score_prompt_text = "Please enter your name to save this score: " ; constexpr auto sp = " " ; std ::ostringstream score_prompt_richtext; score_prompt_richtext << bold_on << sp << score_prompt_text << bold_off; return score_prompt_richtext.str(); } std ::string MessageScoreSavedPrompt () { constexpr auto score_saved_text = "Score saved!" ; constexpr auto sp = " " ; std ::ostringstream score_saved_richtext; score_saved_richtext << "\n" << green << bold_on << sp << score_saved_text << bold_off << def << "\n" ; return score_saved_richtext.str(); }
至此,关于统计数据存储的部分完结。
代码 global.hpp 1 2 3 4 5 6 7 8 #ifndef GLOBAL_H #define GLOBAL_H std ::string secondsFormat (double sec) ;#endif
global.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include "global.hpp" #include <sstream> std ::string secondsFormat (double sec) { double second = sec; int minute = second / 60 ; int hour = minute / 60 ; second -= minute * 60 ; minute %= 60 ; second = static_cast <int >(second); std ::ostringstream oss; if (hour) { oss << hour << "h " ; } if (minute) { oss << minute << "m " ; } oss << second << "s" ; return oss.str(); }
statistics.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #ifndef STATISTICS_H #define STATISTICS_H #include "global.hpp" #include <tuple> #include <string> #include <iosfwd> namespace Scoreboard { struct Score ; } namespace Statistics{ struct total_game_stats_t { ull bestScore{}; ull totalMoveCount{}; int gameCount{}; double totalDuration{}; int winCount{}; }; using load_stats_status_t = std ::tuple<bool , total_game_stats_t >; load_stats_status_t loadFromFileStatistics (std ::string filename) ; ull loadBestScore () ; void saveEndGameStats (Scoreboard::Score finalscore) ; void createFinalScoreAndEndGameDataFile (std ::ostream& os, std ::istream& is, Scoreboard::Score finalscore) ;} std ::istream& operator >>(std ::istream& is, Statistics::total_game_stats_t & s);std ::ostream& operator <<(std ::ostream& os, Statistics::total_game_stats_t & s);#endif
statistics.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 #include "statistics.hpp" #include "statistics-graphics.hpp" #include "scoreboard.hpp" #include "scoreboard-graphics.hpp" #include <fstream> namespace Statistics{ namespace { total_game_stats_t generateStatsFromInputData (std ::istream& is) { total_game_stats_t stats; is >> stats; return stats; } Scoreboard::Graphics::finalscore_display_data_t makeFinalScoreDisplayData(Scoreboard::Score finalscore) { const auto fsdd = std ::make_tuple( std ::to_string(finalscore.score), std ::to_string(finalscore.largestTile), std ::to_string(finalscore.moveCount), secondsFormat(finalscore.duration)); return fsdd; } std ::string receiveInputPlayerName (std ::istream& is) { std ::string playerName; is >> playerName; return playerName; } bool generateFilefromStatsData (std ::ostream& os, total_game_stats_t stats) { os << stats; return true ; } bool saveToFileEndGameStatistics (std ::string filename, total_game_stats_t s) { std ::ofstream filedata (filename) ; return generateFilefromStatsData(filedata, s); } } load_stats_status_t loadFromFileStatistics (std ::string filename) { std ::ifstream statistics (filename) ; if (statistics) { total_game_stats_t state = generateStatsFromInputData(statistics); return load_stats_status_t { true , state }; } return load_stats_status_t { false , total_game_stats_t {} }; } ull loadBestScore () { total_game_stats_t stats; bool stats_file_loaded{ false }; ull tmpScore{ 0 }; std ::tie(stats_file_loaded, stats) = loadFromFileStatistics("../data/statistics.txt" ); if (stats_file_loaded) { tmpScore = stats.bestScore; } return tmpScore; } void saveEndGameStats (Scoreboard::Score finalscore) { total_game_stats_t stats; std ::tie(std ::ignore, stats) = loadFromFileStatistics("../data/statistics.txt" ); stats.bestScore = stats.bestScore < finalscore.score ? finalscore.score : stats.bestScore; stats.gameCount++; stats.winCount = finalscore.win ? stats.winCount + 1 : stats.winCount; stats.totalMoveCount += finalscore.moveCount; stats.totalDuration += finalscore.duration; saveToFileEndGameStatistics("../data/statistics.txt" , stats); } void createFinalScoreAndEndGameDataFile (std ::ostream& os, std ::istream& is, Scoreboard::Score finalscore) { const auto finalscoreDisplayData = makeFinalScoreDisplayData(finalscore); DrawAlways(os, DataSuppliment(finalscoreDisplayData, Scoreboard::Graphics::EndGameStatisticsPrompt)); DrawAlways(os, Graphics::AskForPlayerNamePrompt); const auto playerName = receiveInputPlayerName(is); finalscore.name = playerName; Scoreboard::saveScore(finalscore); saveEndGameStats(finalscore); DrawAlways(os, Graphics::MessageScoreSavedPrompt); } } using namespace Statistics;std ::istream& operator >>(std ::istream& is, total_game_stats_t & s) { is >> s.bestScore >> s.gameCount >> s.winCount >> s.totalMoveCount >> s.totalDuration; return is; } std ::ostream& operator <<(std ::ostream& os, total_game_stats_t & s) { os << s.bestScore << "\n" << s.gameCount << "\n" << s.winCount << "\n" << s.totalMoveCount << "\n" << s.totalDuration; return os; }
statistics-graphics.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #ifndef SCOREBOARD_GRAPHICS_H #define SCOREBOARD_GRAPHICS_H #include <string> #include <tuple> namespace Scoreboard{ namespace Graphics { using finalscore_display_data_t = std ::tuple<std ::string , std ::string , std ::string , std ::string >; std ::string EndGameStatisticsPrompt (finalscore_display_data_t finalscore) ; } } #endif
statistics-graphics.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include "statistics-graphics.hpp" #include "color.hpp" #include <sstream> namespace Statistics{ namespace Graphics { std ::string AskForPlayerNamePrompt () { constexpr auto score_prompt_text = "Please enter your name to save this score: " ; constexpr auto sp = " " ; std ::ostringstream score_prompt_richtext; score_prompt_richtext << bold_on << sp << score_prompt_text << bold_off; return score_prompt_richtext.str(); } std ::string MessageScoreSavedPrompt () { constexpr auto score_saved_text = "Score saved!" ; constexpr auto sp = " " ; std ::ostringstream score_saved_richtext; score_saved_richtext << "\n" << green << bold_on << sp << score_saved_text << bold_off << def << "\n" ; return score_saved_richtext.str(); } } }
game-pregame.cpp 1 2 3 4 5 6 7 8 9 10 11 12 void processPreGame () { if (pregamesetup_status[FLAG_START_GAME]) { playGame(PlayGameFlag::BrandNewGame, GameBoard{ storedGameBoardSize }, storedGameBoardSize); } if (pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU]) { Menu::startMenu(); } }
game.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #ifndef GAME_H #define GAME_H namespace Game { struct GameBoard ; enum class PlayGameFlag { BrandNewGame, ContinuePreviousGame }; void playGame (PlayGameFlag flag, GameBoard gb, unsigned long long userInput_PlaySize = 1 ) ; void startGame () ; }; #endif
game.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include "game.hpp" #include "game-pregame.hpp" #include "gameboard.hpp" #include "global.hpp" #include "statistics.hpp" #include "scoreboard.hpp" #include <chrono> #include <iostream> namespace Game{ enum { COMPETITION_GAME_BOARD_SIZE = 4 }; namespace { using competition_mode_t = bool ; Scoreboard::Score makeFinalscoreFromGameSession (double duration, GameBoard gb) { Scoreboard::Score finalscore{}; finalscore.score = gb.score; finalscore.win = hasWonOnGameboard(gb); finalscore.moveCount = MoveCountOnGameBoard(gb); finalscore.largestTile = gb.largestTile; finalscore.duration = duration; return finalscore; } void doPostGameSaveStuff (Scoreboard::Score finalscore, competition_mode_t cm) { if (cm) { Statistics::createFinalScoreAndEndGameDataFile(std ::cout , std ::cin , finalscore); } } GameBoard endlessGameLoop (ull currentBestScore, competition_mode_t cm, GameBoard gb) { return GameBoard{ 3 }; } } void playGame (PlayGameFlag flag, GameBoard gb, ull boardSize) { const auto isThisNewlyGame = (flag == PlayGameFlag::BrandNewGame); const auto isCompetitionMode = (boardSize == COMPETITION_GAME_BOARD_SIZE); const auto bestScore = Statistics::loadBestScore(); if (isThisNewlyGame) { gb = GameBoard(boardSize); addTileOnGameboard(gb); } const auto startTime = std ::chrono::high_resolution_clock::now(); gb = endlessGameLoop(bestScore, isCompetitionMode, gb); const auto finishTime = std ::chrono::high_resolution_clock::now(); const std ::chrono::duration<double > elapsed = finishTime - startTime; const auto duration = elapsed.count(); if (isThisNewlyGame) { const auto finalscore = makeFinalscoreFromGameSession(duration, gb); doPostGameSaveStuff(finalscore, isCompetitionMode); } } void startGame () { PreGameSetup::SetupNewGame(); } }
scoreboard.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #ifndef SCOREBOARD_H #define SCOREBOARD_H #include "global.hpp" #include <string> #include <vector> namespace Scoreboard{ struct Score { std ::string name; ull score; bool win; ull largestTile; long long moveCount; double duration; }; using Scoreboard_t = std ::vector <Score>; void saveScore (Score finalscore) ; } std ::istream& operator >>(std ::istream& is, Scoreboard::Score& s);std ::ostream& operator <<(std ::ostream& os, Scoreboard::Score& s);#endif
scoreboard.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include "scoreboard.hpp" #include <fstream> namespace { using namespace Scoreboard; bool generateFilefromScoreData (std ::ostream& os, Score score) { os << score; return true ; } bool saveToFileScore (std ::string filename, Score s) { std ::ofstream os (filename, std ::ios_base::app) ; return generateFilefromScoreData(os, s); } } namespace Scoreboard{ void saveScore (Score finalscore) { saveToFileScore("../data/scores.txt" , finalscore); } } using namespace Scoreboard;std ::istream& operator >>(std ::istream& is, Score& s) { is >> s.name >> s.score >> s.win >> s.moveCount >> s.largestTile >> s.duration; return is; } std ::ostream& operator <<(std ::ostream& os, Score& s){ os << "\n" << s.name << " " << s.score << " " << s.win << " " << s.moveCount << " " << s.largestTile << " " << s.duration; return os; }
scoreboard-graphics.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #ifndef SCOREBOARD_GRAPHICS_H #define SCOREBOARD_GRAPHICS_H #include <string> #include <tuple> namespace Scoreboard{ namespace Graphics { using finalscore_display_data_t = std ::tuple<std ::string , std ::string , std ::string , std ::string >; std ::string EndGameStatisticsPrompt (finalscore_display_data_t finalscore) ; } } #endif
scoreboard-graphics.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include "scoreboard-graphics.hpp" #include "global.hpp" #include "color.hpp" #include <sstream> #include <iomanip> // std::setw #include <array> // std::begin namespace Scoreboard{ namespace Graphics { std ::string EndGameStatisticsPrompt (finalscore_display_data_t finalscore) { std ::ostringstream str_os; constexpr auto stats_title_text = "STATISTICS" ; constexpr auto divider_text = "──────────" ; constexpr auto sp = " " ; const auto stats_attributes_text = { "Final score:" , "Largest Tile:" , "Number of moves:" , "Time taken:" }; enum FinalScoreDisplayDataFields { IDX_FINAL_SCORE_VALUE, IDX_LARGEST_TILE, IDX_MOVE_COUNT, IDX_DURATION, MAX_NUM_OF_FINALSCOREDISPLAYDATA_INDEXES }; const auto data_stats = std ::array <std ::string , MAX_NUM_OF_FINALSCOREDISPLAYDATA_INDEXES>{ std ::get<IDX_FINAL_SCORE_VALUE>(finalscore), std ::get<IDX_LARGEST_TILE>(finalscore), std ::get<IDX_MOVE_COUNT>(finalscore), std ::get<IDX_DURATION>(finalscore) }; std ::ostringstream stats_richtext; stats_richtext << yellow << sp << stats_title_text << def << "\n" ; stats_richtext << yellow << sp << divider_text << def << "\n" ; auto counter{ 0 }; const auto populate_stats_info = [=, &counter, &stats_richtext](const std ::string ) { stats_richtext << sp << std ::left << std ::setw(19 ) << std ::begin(stats_attributes_text)[counter] << bold_on << std ::begin(data_stats)[counter] << bold_off << "\n" ; counter++; }; for (const auto s : stats_attributes_text) { populate_stats_info(s); } str_os << stats_richtext.str(); str_os << "\n\n" ; return str_os.str(); } } }
阶段五 构建 这一阶段,我们重点实现game-play部分,目标是搭建出一个切实可玩的游戏。
首先丰富game.cpp中endlessGameLoop()函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 enum GameStatusFlag { FLAG_WIN, FLAG_END_GAME, FLAG_ONE_SHOT, FLAG_SAVED_GAME, FLAG_INPUT_ERROR, FLAG_ENDLESS_MODE, FLAG_GAME_IS_ASKING_QUESTION_MODE, FLAG_QUESTION_STAY_OR_QUIT, MAX_NO_GAME_STATUS_FLAGS }; using gamestatus_t = std ::array <bool , MAX_NO_GAME_STATUS_FLAGS>;GameBoard endlessGameLoop (ull currentBestScore, competition_mode_t cm, GameBoard gb) { auto loop_again{ true }; auto currentgamestatus = std ::make_tuple(currentBestScore, cm, gamestatus_t {}, gb); while (loop_again) { std ::tie(loop_again, currentgamestatus) = soloGameLoop(currentgamestatus); } return gb; }
endlessGameLoop()将游戏初始各状态信息作为参数传递给soloGameLoop()函数,并通过后者的返回值决定是否继续持续循环下去。
接下来探索soloGameLoop()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 using current_game_session_t = std ::tuple<ull, competition_mode_t , gamestatus_t , GameBoard>; enum tuple_cgs_t_idx { IDX_BESTSCORE, IDX_COMP_MODE, IDX_GAMESTATUS, IDX_GAMEBOARD }; std::tuple<bool, current_game_session_t> soloGameLoop(current_game_session_t cgs) { using tup_idx = tuple_cgs_t_idx; const auto pGamestatus = std ::addressof(std ::get<tup_idx::IDX_GAMESTATUS>(cgs)); const auto pGameboard = std ::addressof(std ::get<tup_idx::IDX_GAMEBOARD>(cgs)); std ::tie(*pGamestatus, *pGameboard) = processGameLogic(std ::make_tuple(*pGamestatus, *pGameboard)); DrawAlways(std ::cout , DataSuppliment(cgs, drawGraphics)); }
soloGameLoop()可以说是整个项目的最为核心的函数之一,我们逐步将其完善。
1 2 3 4 5 6 const auto pGamestatus = std ::addressof(std ::get<tup_idx::IDX_GAMESTATUS>(cgs)); const auto pGameboard = std ::addressof(std ::get<tup_idx::IDX_GAMEBOARD>(cgs));std ::tie(*pGamestatus, *pGameboard) = processGameLogic(std ::make_tuple(*pGamestatus, *pGameboard));
首先,每轮soloGameLoop()循环一开始,我们先判断场上局势如何,以便后续应对。为此,创建函数processGameLogic(),该函数以当前游戏的Gamestatus和Gameboard作为参数,判断目前游戏状态满足哪些条件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 using gamestatus_gameboard_t = std ::tuple<gamestatus_t , GameBoard>;gamestatus_gameboard_t processGameLogic (gamestatus_gameboard_t gsgb) { gamestatus_t gamestatus; GameBoard gb; std ::tie(gamestatus, gb) = gsgb; unblockTilesOnGameboard(gb); if (gb.moved) { addTileOnGameboard(gb); registerMoveByOneOnGameboard(gb); } if (!gamestatus[FLAG_ENDLESS_MODE]) { if (hasWonOnGameboard(gb)) { gamestatus[FLAG_WIN] = true ; gamestatus[FLAG_GAME_IS_ASKING_QUESTION_MODE] = true ; gamestatus[FLAG_QUESTION_STAY_OR_QUIT] = true ; } } if (!canMoveOnGameboard(gb)) { gamestatus[FLAG_END_GAME] = true ; } return std ::make_tuple(gamestatus, gb); }
processGameLogic()根据此时场上局势所满足的条件将gamestatus_t对象的对应标志置位。
知晓场上局势相关信息后,便可呈现游戏画面,该功能由下述语句实现:
1 DrawAlways(std ::cout , DataSuppliment(cgs, drawGraphics));
接着进一步查看drawGraphics()定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 std ::string drawGraphics (current_game_session_t cgs) { using namespace Graphics; using namespace Gameboard::Graphics; using tup_idx = tuple_cgs_t_idx; const auto bestScore = std ::get<tup_idx::IDX_BESTSCORE>(cgs); const auto comp_mode = std ::get<tup_idx::IDX_COMP_MODE>(cgs); const auto gamestatus = std ::get<tup_idx::IDX_GAMESTATUS>(cgs); const auto gb = std ::get<tup_idx::IDX_GAMEBOARD>(cgs); std ::ostringstream str_os; clearScreen(); DrawAlways(str_os, AsciiArt2048); const auto scdd = make_scoreboard_display_data(bestScore, comp_mode, gb); DrawAlways(str_os, DataSuppliment(scdd, GameScoreBoardOverlay)); DrawAlways(str_os, DataSuppliment(gb, GameBoardTextOutput)); DrawOnlyWhen(str_os, gamestatus[FLAG_SAVED_GAME], GameStateNowSavedPrompt); DrawOnlyWhen(str_os, gamestatus[FLAG_GAME_IS_ASKING_QUESTION_MODE], DataSuppliment(gamestatus, DisplayGameQuestionsToPlayerPrompt)); const auto input_controls_display_data = make_input_controls_display_data(gamestatus); DrawAlways(str_os, DataSuppliment(input_controls_display_data, GameInputControlsOverlay)); DrawOnlyWhen(str_os, gamestatus[FLAG_INPUT_ERROR], InvalidInputGameBoardErrorPrompt); return str_os.str(); }
该函数分八步完成游戏各功能模块的呈现,最终效果可参见“项目成果预览——游戏过程”,下面一一介绍之。
前两步clear screen和打印Title Art较为基础,略。
第三步,打印计分板,辅助函数make_scoreboard_display_data()(定义于game.cpp的匿名空间中)用以生成计分板需要用到的数据,而GameScoreBoardOverlay()(定义于game-graphics.cpp中)负责计分板视觉呈现部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Graphics::scoreboard_display_data_t make_scoreboard_display_data(ull bestScore, competition_mode_t cm, GameBoard gb) { const auto gameboardScore = gb.score; const auto tmpBestScore = (bestScore < gb.score ? gb.score : bestScore); const auto comp_mode = cm; const auto movecount = MoveCountOnGameBoard(gb); const auto scdd = std ::make_tuple(comp_mode, std ::to_string(gameboardScore), std ::to_string(tmpBestScore), std ::to_string(movecount)); return scdd; };
注意,make_scoreboard_display_data()用到了competition_mode_t类型变量cm,通过该变量判断是否处于竞技模式(即所选择的棋盘是否为4 x 4规格)。非竞技模式下计分板只显示当前得分与移动步数,而竞技模式下额外显示历史最高得分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 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:" ; enum { UI_SCOREBOARD_SIZE = 27 , UI_BORDER_OUTER_PADDING = 2 , UI_BORDER_INNER_PADDING = 1 }; constexpr auto border_padding_char = ' ' ; constexpr auto vertical_border_pattern = "│" ; constexpr auto top_board = "┌───────────────────────────┐" ; constexpr auto bottom_board = "└───────────────────────────┘" ; 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(); }
GameScoreBoardOverlay()负责棋盘呈现工作,观其定义知,该函数实际是通过辅助函数GameScoreBoardBox()完成实际的呈现工作。
GameScoreBoardBox()是一个代码量较为庞大的函数,我们逐部分剖析该函数。
(此处强烈建议对照“项目成果展示——游戏过程”图片进行代码阅读与理解)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 enum { UI_SCOREBOARD_SIZE = 27 , UI_BORDER_OUTER_PADDING = 2 , UI_BORDER_INNER_PADDING = 1 };
第一个理解难点就是这段注释加代码。
请查看“项目成果展示——游戏过程”图片:我们的计分板左边(left)的竖线与屏幕的最左侧之间(计分板外部,outer)有两个字符的空隙(l-outer = 2),而右侧竖线与计分板外的距离对最终视觉呈现无影响,设其值为零(r-outer = 0);而两竖线间距为27个字符宽度(horizontal_sep = 27);计分板内部(inner)的单词与左侧(left)竖线间距为1个字符宽度(l-inner = 1),而数字与右侧(right)竖线间距同为1个字符宽度(r-inner = 1)。
1 2 3 4 5 6 7 8 9 10 11 12 constexpr auto border_padding_char = ' ' ;constexpr auto vertical_border_pattern = "│" ;constexpr auto top_board = "┌───────────────────────────┐" ; constexpr auto bottom_board = "└───────────────────────────┘" ; 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 );
字符串top_board与bottom_board长度均为29(两竖线各占1个字符宽度,横线占27个字符宽度)。
1 2 3 4 5 6 7 8 9 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" ;
上述规格信息说清楚后,这段代码就十分容易理解:打印计分板最顶端横线与SCORE行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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" ;
这两段如法炮制,无需多言。
解决了计分板,返回drawGraphics(),看看接下来还需要做些什么。
1 2 3 4 5 6 7 8 9 10 std ::string drawGraphics (current_game_session_t cgs) { ... DrawAlways(str_os, DataSuppliment(gb, GameBoardTextOutput)); ... }
有了计分板,接下来就要实现棋盘呈现了。GameBoardTextOutput()负责这方面的事宜。
(此处强烈建议对照“项目成果展示——游戏过程”图片进行代码阅读与理解)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 template <size_t num_of_bars>std::array<std::string, num_of_bars> makePatternedBars(int boardSize) { auto temp_bars = std ::array <std ::string , num_of_bars>{}; using bar_pattern_t = std ::tuple<std ::string , std ::string , std ::string >; const auto bar_pattern_list = { std ::make_tuple("┌" , "┬" , "┐" ), std ::make_tuple("├" , "┼" , "┤" ), std ::make_tuple("└" , "┴" , "┘" ) }; const auto generate_x_bar_pattern = [boardSize](const bar_pattern_t t) { enum { PATTERN_HEAD, PATTERN_MID, PATTERN_TAIL }; constexpr auto sp = " " ; constexpr auto separator = "──────" ; std ::ostringstream temp_richtext; temp_richtext << sp << std ::get<PATTERN_HEAD>(t); for (auto i = 0 ; i < boardSize; i++) { const auto is_not_last_column = (i < boardSize - 1 ); temp_richtext << separator << (is_not_last_column ? std ::get<PATTERN_MID>(t) : std ::get<PATTERN_TAIL>(t)); } temp_richtext << "\n" ; return temp_richtext.str(); }; std ::transform(std ::begin(bar_pattern_list), std ::end(bar_pattern_list), std ::begin(temp_bars), generate_x_bar_pattern); return temp_bars; } std ::string drawGameBoard (GameBoard::gameboard_data_array_t gbda) { enum { TOP_BAR, XN_BAR, BASE_BAR, MAX_TYPES_OF_BARS }; const int boardSize = getSizeOfGameboard(gbda); const auto vertibar = makePatternedBars<MAX_TYPES_OF_BARS>(boardSize); std ::ostringstream str_os; for (auto x = 0 ; x < boardSize; x++) { const auto is_first_row = (x == 0 ); str_os << (is_first_row ? std ::get<TOP_BAR>(vertibar) : std ::get<XN_BAR>(vertibar)); for (auto y = 0 ; y < boardSize; y++) { const auto is_first_col = (y == 0 ); const auto sp = (is_first_col ? " " : " " ); const auto tile = getTileOnGameboard(gbda, point2D_t{ x, y }); str_os << sp; str_os << "│ " ; str_os << drawTileString(tile); } str_os << " │" ; str_os << "\n" ; } str_os << std ::get<BASE_BAR>(vertibar); str_os << "\n" ; return str_os.str(); } } std ::string GameBoardTextOutput (GameBoard gb) { return drawGameBoard(gb.gbda); }
可以看到,真正负责棋盘图像绘制的是drawGameBoard()函数。
1 2 3 4 5 6 7 8 9 enum { TOP_BAR, XN_BAR, BASE_BAR, MAX_TYPES_OF_BARS }; const int boardSize = getSizeOfGameboard(gbda);const auto vertibar = makePatternedBars<MAX_TYPES_OF_BARS>(boardSize);
我们将棋盘条带分为最顶端条带、最底端条带和中间条带。三种不同条带将会用到的样式不尽相同,列举如下:
1 2 3 ("┌" , "┬" , "┐" ) ("├" , "┼" , "┤" ) ("└" , "┴" , "┘" )
1 2 const int boardSize = getSizeOfGameboard(gbda);const auto vertibar = makePatternedBars<MAX_TYPES_OF_BARS>(boardSize);
使用变量boardSize获取棋盘尺寸后,将其作为参数,调用makePatternedBars()获得对应尺寸的棋盘边界字符串数组。
makePatternedBars()比较简单,这里不再细说。
drawGameBoard()剩余部分同样只是按既定顺序绘制棋盘,其中用到了函数drawTileString(),该函数定义于tile-graphics.cpp。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Color::Modifier tileColor (ull value) { std ::vector <Color::Modifier> colors{ red, yellow, magenta, blue, cyan, yellow, red, yellow, magenta, blue, green }; int log = log2(value); int index = log < 12 ? log - 1 : 10 ; return colors[index]; } std ::string drawTileString (tile_t currentTile) { std ::ostringstream tile_richtext; if (!currentTile.value) { tile_richtext << " " ; } else { tile_richtext << tileColor(currentTile.value) << bold_on << std ::setw(4 ) << currentTile.value << bold_off << def; } return tile_richtext.str(); }
通过tileColor()实现了不同值的砖块能有不同的颜色。
1 2 3 4 5 6 7 8 9 10 11 std ::string drawGraphics (current_game_session_t cgs) { ... DrawOnlyWhen(str_os, gamestatus[FLAG_SAVED_GAME], GameStateNowSavedPrompt); ... }
第五步为条件绘制,只有当玩家选择保存游戏时,输出提示字符串,告知玩家保存成功。GameStateNowSavedPrompt()定义于game-graphics.cpp。
1 2 3 4 5 6 7 8 9 10 11 12 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(); }
分析略。
1 2 3 4 5 6 7 8 9 10 11 std ::string drawGraphics (current_game_session_t cgs) { ... DrawOnlyWhen(str_os, gamestatus[FLAG_GAME_IS_ASKING_QUESTION_MODE], DataSuppliment(gamestatus, DisplayGameQuestionsToPlayerPrompt)); ... }
第六步同为条件绘制,若游戏处于向玩家提问状态(例如玩家胜利时),则打印提示玩家输入字符串。
DisplayGameQuestionsToPlayerPrompt()定义于game.cpp中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Graphics::scoreboard_display_data_t make_scoreboard_display_data(ull bestScore, competition_mode_t cm, GameBoard gb) { const auto gameboardScore = gb.score; const auto tmpBestScore = (bestScore < gb.score ? gb.score : bestScore); const auto comp_mode = cm; const auto movecount = MoveCountOnGameBoard(gb); const auto scdd = std ::make_tuple(comp_mode, std ::to_string(gameboardScore), std ::to_string(tmpBestScore), std ::to_string(movecount)); return scdd; }; std ::string DisplayGameQuestionsToPlayerPrompt (gamestatus_t gamestatus) { using namespace Graphics; std ::ostringstream str_os; DrawOnlyWhen(str_os, gamestatus[FLAG_QUESTION_STAY_OR_QUIT], QuestionEndOfWinningGamePrompt); return str_os.str(); }
而QuestionEndOfWinningGamePrompt()则定义于game-graphics.cpp中。
1 2 3 4 5 6 7 8 9 10 11 12 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(); }
再次回到drawGraphics()。
1 2 3 4 5 6 7 8 9 10 11 12 13 std ::string drawGraphics (current_game_session_t cgs) { ... const auto input_controls_display_data = make_input_controls_display_data(gamestatus); DrawAlways(str_os, DataSuppliment(input_controls_display_data, GameInputControlsOverlay)); ... }
计分板与棋盘呈现后,我们期待玩家进行输入,开始游玩。需要告诉可用的按键有哪些,分别对应什么功能。GameInputControlsOverlay()负责此事。
game-graphics.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 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" }; std ::ostringstream str_os; for (const auto txt : input_commands_list_text) { str_os << sp << txt << "\n" ; } 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" ; } 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; const auto InputControlLists = [=] { std ::ostringstream str_os; DrawAlways(str_os, Graphics::InputCommandListPrompt); DrawOnlyWhen(str_os, is_in_endless_mode, Graphics::EndlessModeCommandListPrompt); DrawAlways(str_os, Graphics::InputCommandListFooterPrompt); return str_os.str(); }; DrawOnlyWhen(str_os, !is_in_question_mode, InputControlLists); return str_os.str(); }
由InputCommandListPrompt()知,我们的游戏既允许玩家使用WASD键进行方向选择,也允许玩家使用HJKL键进行方向选择,甚至还允许玩家使用四个方向键进行方向选择,是时候更新一下game-input.*了。
game-input.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 #ifndef GAME_INPUT_H #define GAME_INPUT_H #include <array> namespace Game{ namespace Input { namespace Keypress { namespace Code { enum { CODE_ESC = 27 , CODE_LSQUAREBRACKET = '[' }; enum { CODE_ANSI_TRIGGER_1 = CODE_ESC, CODE_ANSI_TRIGGER_2 = CODE_LSQUAREBRACKET }; enum { CODE_ANSI_UP = 'A' , CODE_ANSI_DOWN = 'B' , CODE_ANSI_LEFT = 'D' , CODE_ANSI_RIGHT = 'C' }; enum { CODE_WASD_UP = 'W' , CODE_WASD_DOWN = 'S' , CODE_WASD_LEFT = 'A' , CODE_WASD_RIGHT = 'D' }; enum { CODE_VIM_UP = 'K' , CODE_VIM_DOWN = 'J' , CODE_VIM_LEFT = 'H' , CODE_VIM_RIGHT = 'L' }; enum { CODE_HOTKEY_PREGAME_BACK_TO_MENU = 0 , CODE_HOTKEY_ACTION_SAVE = 'Z' , CODE_HOTKEY_ALTERNATE_ACTION_SAVE = 'P' , CODE_HOTKEY_QUIT_ENDLESS_MODE = 'X' , CODE_HOTKEY_CHOICE_NO = 'N' , CODE_HOTKEY_CHOICE_YES = 'Y' , }; } } enum IntendedMoveFlag { FLAG_MOVE_LEFT, FLAG_MOVE_RIGHT, FLAG_MOVE_UP, FLAG_MOVE_DOWN, MAX_NO_INTENDED_MOVE_FLAGS }; using intendedmove_t = std ::array <bool , MAX_NO_INTENDED_MOVE_FLAGS>; bool check_input_ansi (char c, intendedmove_t & intendedmove) ; bool check_input_vim (char c, intendedmove_t & intendedmove) ; bool check_input_wasd (char c, intendedmove_t & intendedmove) ; } } #endif
game-input.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include "game-input.hpp" #include "global.hpp" namespace Game { namespace Input { bool check_input_ansi (char c, intendedmove_t & intendedmove) { using namespace Keypress::Code; if (c == CODE_ANSI_TRIGGER_1) { getKeypressDownInput(c); if (c == CODE_ANSI_TRIGGER_2) { getKeypressDownInput(c); switch (c) { case CODE_ANSI_UP: intendedmove[FLAG_MOVE_UP] = true ; return false ; case CODE_ANSI_DOWN: intendedmove[FLAG_MOVE_DOWN] = true ; return false ; case CODE_ANSI_RIGHT: intendedmove[FLAG_MOVE_RIGHT] = true ; return false ; case CODE_ANSI_LEFT: intendedmove[FLAG_MOVE_LEFT] = true ; return false ; } } } return true ; } bool check_input_vim (char c, intendedmove_t & intendedmove) { using namespace Keypress::Code; switch (toupper (c)) { case CODE_VIM_UP: intendedmove[FLAG_MOVE_UP] = true ; return false ; case CODE_VIM_LEFT: intendedmove[FLAG_MOVE_LEFT] = true ; return false ; case CODE_VIM_DOWN: intendedmove[FLAG_MOVE_DOWN] = true ; return false ; case CODE_VIM_RIGHT: intendedmove[FLAG_MOVE_RIGHT] = true ; return false ; } return true ; } bool check_input_wasd (char c, intendedmove_t & intendedmove) { using namespace Keypress::Code; switch (toupper (c)) { case CODE_WASD_UP: intendedmove[FLAG_MOVE_UP] = true ; return false ; case CODE_WASD_LEFT: intendedmove[FLAG_MOVE_LEFT] = true ; return false ; case CODE_WASD_DOWN: intendedmove[FLAG_MOVE_DOWN] = true ; return false ; case CODE_WASD_RIGHT: intendedmove[FLAG_MOVE_RIGHT] = true ; return false ; } return true ; } } }
check_input_ansi()用到了getKeypressDownInput(),后者定义于global.cpp中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #ifdef _WIN32 void getKeypressDownInput (char & c) { std ::cin >> c; } #else # include <termios.h> # include <unistd.h> char getch () { char buf = 0 ; struct termios old = { 0 }; if (tcgetattr(0 , &old) < 0 ) perror("tcsetattr()" ); old.c_lflag &= ~ICANON; old.c_lflag &= ~ECHO; old.c_cc[VMIN] = 1 ; old.c_cc[VTIME] = 0 ; if (tcsetattr(0 , TCSANOW, &old) < 0 ) perror("tcsetattr ICANON" ); if (read(0 , &buf, 1 ) < 0 ) perror("read()" ); old.c_lflag |= ICANON; old.c_lflag |= ECHO; if (tcsetattr(0 , TCSADRAIN, &old) < 0 ) perror("tcsetattr ~ICANON" ); return (buf); } void getKeypressDownInput (char & c) { c = getch(); } #endif
再次回到 drawGraphics()中。
1 2 3 4 5 6 7 8 9 10 11 std ::string drawGraphics (current_game_session_t cgs) { ... DrawOnlyWhen(str_os, gamestatus[FLAG_INPUT_ERROR], InvalidInputGameBoardErrorPrompt); ... }
最后,当用户输入错误时,提醒用户重新输入。InvalidInputGameBoardErrorPrompt()定义于game-graphics.cpp中。
1 2 3 4 5 6 7 8 9 10 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(); }
至此,drawGraphics()分析完毕,而soloGameLoop()也完成了三分之二左右,由于本阶段drawGraphics()占用大量篇幅,soloGameLoop()剩余部分留至下一阶段完成。
代码 game.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 #include "game.hpp" #include "game-graphics.hpp" #include "game-input.hpp" #include "game-pregame.hpp" #include "gameboard.hpp" #include "gameboard-graphics.hpp" #include "global.hpp" #include "statistics.hpp" #include "scoreboard.hpp" #include <array> #include <chrono> #include <iostream> #include <sstream> namespace Game{ namespace { enum { COMPETITION_GAME_BOARD_SIZE = 4 }; using competition_mode_t = bool ; enum GameStatusFlag { FLAG_WIN, FLAG_END_GAME, FLAG_ONE_SHOT, FLAG_SAVED_GAME, FLAG_INPUT_ERROR, FLAG_ENDLESS_MODE, FLAG_GAME_IS_ASKING_QUESTION_MODE, FLAG_QUESTION_STAY_OR_QUIT, MAX_NO_GAME_STATUS_FLAGS }; using gamestatus_t = std ::array <bool , MAX_NO_GAME_STATUS_FLAGS>; using current_game_session_t = std ::tuple<ull, competition_mode_t , gamestatus_t , GameBoard>; enum tuple_cgs_t_idx { IDX_BESTSCORE, IDX_COMP_MODE, IDX_GAMESTATUS, IDX_GAMEBOARD }; Scoreboard::Score makeFinalscoreFromGameSession (double duration, GameBoard gb) { Scoreboard::Score finalscore{}; finalscore.score = gb.score; finalscore.win = hasWonOnGameboard(gb); finalscore.moveCount = MoveCountOnGameBoard(gb); finalscore.largestTile = gb.largestTile; finalscore.duration = duration; return finalscore; } void doPostGameSaveStuff (Scoreboard::Score finalscore, competition_mode_t cm) { if (cm) { Statistics::createFinalScoreAndEndGameDataFile(std ::cout , std ::cin , finalscore); } } using gamestatus_gameboard_t = std ::tuple<gamestatus_t , GameBoard>; gamestatus_gameboard_t processGameLogic (gamestatus_gameboard_t gsgb) { gamestatus_t gamestatus; GameBoard gb; std ::tie(gamestatus, gb) = gsgb; unblockTilesOnGameboard(gb); if (gb.moved) { addTileOnGameboard(gb); registerMoveByOneOnGameboard(gb); } if (!gamestatus[FLAG_ENDLESS_MODE]) { if (hasWonOnGameboard(gb)) { gamestatus[FLAG_WIN] = true ; gamestatus[FLAG_GAME_IS_ASKING_QUESTION_MODE] = true ; gamestatus[FLAG_QUESTION_STAY_OR_QUIT] = true ; } } if (!canMoveOnGameboard(gb)) { gamestatus[FLAG_END_GAME] = true ; } return std ::make_tuple(gamestatus, gb); } Graphics::scoreboard_display_data_t make_scoreboard_display_data(ull bestScore, competition_mode_t cm, GameBoard gb) { const auto gameboardScore = gb.score; const auto tmpBestScore = (bestScore < gb.score ? gb.score : bestScore); const auto comp_mode = cm; const auto movecount = MoveCountOnGameBoard(gb); const auto scdd = std ::make_tuple(comp_mode, std ::to_string(gameboardScore), std ::to_string(tmpBestScore), std ::to_string(movecount)); return scdd; }; std ::string DisplayGameQuestionsToPlayerPrompt (gamestatus_t gamestatus) { using namespace Graphics; std ::ostringstream str_os; DrawOnlyWhen(str_os, gamestatus[FLAG_QUESTION_STAY_OR_QUIT], QuestionEndOfWinningGamePrompt); return str_os.str(); } Graphics::input_controls_display_data_t make_input_controls_display_data(gamestatus_t gamestatus) { const auto icdd = std ::make_tuple(gamestatus[FLAG_ENDLESS_MODE], gamestatus[FLAG_QUESTION_STAY_OR_QUIT]); return icdd; }; std ::string drawGraphics (current_game_session_t cgs) { using namespace Graphics; using namespace Gameboard::Graphics; using tup_idx = tuple_cgs_t_idx; const auto bestScore = std ::get<tup_idx::IDX_BESTSCORE>(cgs); const auto comp_mode = std ::get<tup_idx::IDX_COMP_MODE>(cgs); const auto gamestatus = std ::get<tup_idx::IDX_GAMESTATUS>(cgs); const auto gb = std ::get<tup_idx::IDX_GAMEBOARD>(cgs); std ::ostringstream str_os; clearScreen(); DrawAlways(str_os, AsciiArt2048); const auto scdd = make_scoreboard_display_data(bestScore, comp_mode, gb); DrawAlways(str_os, DataSuppliment(scdd, GameScoreBoardOverlay)); DrawAlways(str_os, DataSuppliment(gb, GameBoardTextOutput)); DrawOnlyWhen(str_os, gamestatus[FLAG_SAVED_GAME], GameStateNowSavedPrompt); DrawOnlyWhen(str_os, gamestatus[FLAG_GAME_IS_ASKING_QUESTION_MODE], DataSuppliment(gamestatus, DisplayGameQuestionsToPlayerPrompt)); const auto input_controls_display_data = make_input_controls_display_data(gamestatus); DrawAlways(str_os, DataSuppliment(input_controls_display_data, GameInputControlsOverlay)); DrawOnlyWhen(str_os, gamestatus[FLAG_INPUT_ERROR], InvalidInputGameBoardErrorPrompt); return str_os.str(); } std::tuple<bool, current_game_session_t> soloGameLoop(current_game_session_t cgs) { using namespace Input; using tup_idx = tuple_cgs_t_idx; const auto pGamestatus = std ::addressof(std ::get<tup_idx::IDX_GAMESTATUS>(cgs)); const auto pGameboard = std ::addressof(std ::get<tup_idx::IDX_GAMEBOARD>(cgs)); std ::tie(*pGamestatus, *pGameboard) = processGameLogic(std ::make_tuple(*pGamestatus, *pGameboard)); DrawAlways(std ::cout , DataSuppliment(cgs, drawGraphics)); return std ::make_tuple(false , cgs); } GameBoard endlessGameLoop (ull currentBestScore, competition_mode_t cm, GameBoard gb) { auto loop_again{ true }; auto currentgamestatus = std ::make_tuple(currentBestScore, cm, gamestatus_t {}, gb); while (loop_again) { std ::tie(loop_again, currentgamestatus) = soloGameLoop(currentgamestatus); } return gb; } } void playGame (PlayGameFlag flag, GameBoard gb, ull boardSize) { const auto isThisNewlyGame = (flag == PlayGameFlag::BrandNewGame); const auto isCompetitionMode = (boardSize == COMPETITION_GAME_BOARD_SIZE); const auto bestScore = Statistics::loadBestScore(); if (isThisNewlyGame) { gb = GameBoard(boardSize); addTileOnGameboard(gb); } const auto startTime = std ::chrono::high_resolution_clock::now(); gb = endlessGameLoop(bestScore, isCompetitionMode, gb); const auto finishTime = std ::chrono::high_resolution_clock::now(); const std ::chrono::duration<double > elapsed = finishTime - startTime; const auto duration = elapsed.count(); if (isThisNewlyGame) { const auto finalscore = makeFinalscoreFromGameSession(duration, gb); doPostGameSaveStuff(finalscore, isCompetitionMode); } } void startGame () { PreGameSetup::SetupNewGame(); } }
game-graphics.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #ifndef GAME_GRAPHICS_H #define GAME_GRAPHICS_H #include <string> #include <tuple> enum GameBoardDimensions { MIN_GAME_BOARD_PLAY_SIZE = 3 , MAX_GAME_BOARD_PLAY_SIZE = 10 }; namespace Game{ namespace Graphics { std ::string AsciiArt2048 () ; std ::string BoardSizeErrorPrompt () ; std ::string BoardInputPrompt () ; std ::string GameBoardNoSaveErrorPrompt () ; std ::string GameStateNowSavedPrompt () ; std ::string QuestionEndOfWinningGamePrompt () ; std ::string InvalidInputGameBoardErrorPrompt () ; using scoreboard_display_data_t = std ::tuple<bool , std ::string , std ::string , std ::string >; std ::string GameScoreBoardBox (scoreboard_display_data_t scdd) ; std ::string GameScoreBoardOverlay (scoreboard_display_data_t scdd) ; std ::string InputCommandListPrompt () ; std ::string EndlessModeCommandListPrompt () ; std ::string InputCommandListFooterPrompt () ; using input_controls_display_data_t = std ::tuple<bool , bool >; std ::string GameInputControlsOverlay (input_controls_display_data_t gamestatus) ; } } #endif
game-graphics.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 #include "game-graphics.hpp" #include "global.hpp" #include "color.hpp" #include <sstream> namespace Game{ namespace Graphics { std ::string AsciiArt2048 () { constexpr auto title_card_2048 = R"( /\\\\\\\\\ /\\\\\\\ /\\\ /\\\\\\\\\ /\\\///////\\\ /\\\/////\\\ /\\\\\ /\\\///////\\\ \/// \//\\\ /\\\ \//\\\ /\\\/\\\ \/\\\ \/\\\ /\\\/ \/\\\ \/\\\ /\\\/\/\\\ \///\\\\\\\\\/ /\\\// \/\\\ \/\\\ /\\\/ \/\\\ /\\\///////\\\ /\\\// \/\\\ \/\\\ /\\\\\\\\\\\\\\\\ /\\\ \//\\\ /\\\/ \//\\\ /\\\ \///////////\\\// \//\\\ /\\\ /\\\\\\\\\\\\\\\ \///\\\\\\\/ \/\\\ \///\\\\\\\\\/ \/////////////// \/////// \/// \///////// )" ; std ::ostringstream title_card_richtext; title_card_richtext << green << bold_on << title_card_2048 << bold_off << def; title_card_richtext << "\n\n\n" ; return title_card_richtext.str(); } std ::string BoardSizeErrorPrompt () { const auto invalid_prompt_text = { "Invalid input. Gameboard size should range from " , " to " , "." }; 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(); } 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 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 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 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 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 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:" ; enum { UI_SCOREBOARD_SIZE = 27 , UI_BORDER_OUTER_PADDING = 2 , UI_BORDER_INNER_PADDING = 1 }; constexpr auto border_padding_char = ' ' ; constexpr auto vertical_border_pattern = "│" ; constexpr auto top_board = "┌───────────────────────────┐" ; constexpr auto bottom_board = "└───────────────────────────┘" ; 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 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" }; std ::ostringstream str_os; for (const auto txt : input_commands_list_text) { str_os << sp << txt << "\n" ; } 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" ; } 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; const auto InputControlLists = [=] { std ::ostringstream str_os; DrawAlways(str_os, Graphics::InputCommandListPrompt); DrawOnlyWhen(str_os, is_in_endless_mode, Graphics::EndlessModeCommandListPrompt); DrawAlways(str_os, Graphics::InputCommandListFooterPrompt); return str_os.str(); }; DrawOnlyWhen(str_os, !is_in_question_mode, InputControlLists); return str_os.str(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 #ifndef GAME_INPUT_H #define GAME_INPUT_H #include <array> namespace Game{ namespace Input { namespace Keypress { namespace Code { enum { CODE_ESC = 27 , CODE_LSQUAREBRACKET = '[' }; enum { CODE_ANSI_TRIGGER_1 = CODE_ESC, CODE_ANSI_TRIGGER_2 = CODE_LSQUAREBRACKET }; enum { CODE_ANSI_UP = 'A' , CODE_ANSI_DOWN = 'B' , CODE_ANSI_LEFT = 'D' , CODE_ANSI_RIGHT = 'C' }; enum { CODE_WASD_UP = 'W' , CODE_WASD_DOWN = 'S' , CODE_WASD_LEFT = 'A' , CODE_WASD_RIGHT = 'D' }; enum { CODE_VIM_UP = 'K' , CODE_VIM_DOWN = 'J' , CODE_VIM_LEFT = 'H' , CODE_VIM_RIGHT = 'L' }; enum { CODE_HOTKEY_PREGAME_BACK_TO_MENU = 0 , CODE_HOTKEY_ACTION_SAVE = 'Z' , CODE_HOTKEY_ALTERNATE_ACTION_SAVE = 'P' , CODE_HOTKEY_QUIT_ENDLESS_MODE = 'X' , CODE_HOTKEY_CHOICE_NO = 'N' , CODE_HOTKEY_CHOICE_YES = 'Y' , }; } } enum IntendedMoveFlag { FLAG_MOVE_LEFT, FLAG_MOVE_RIGHT, FLAG_MOVE_UP, FLAG_MOVE_DOWN, MAX_NO_INTENDED_MOVE_FLAGS }; using intendedmove_t = std ::array <bool , MAX_NO_INTENDED_MOVE_FLAGS>; bool check_input_ansi (char c, intendedmove_t & intendedmove) ; bool check_input_vim (char c, intendedmove_t & intendedmove) ; bool check_input_wasd (char c, intendedmove_t & intendedmove) ; } } #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include "game-input.hpp" #include "global.hpp" namespace Game { namespace Input { bool check_input_ansi (char c, intendedmove_t & intendedmove) { using namespace Keypress::Code; if (c == CODE_ANSI_TRIGGER_1) { getKeypressDownInput(c); if (c == CODE_ANSI_TRIGGER_2) { getKeypressDownInput(c); switch (c) { case CODE_ANSI_UP: intendedmove[FLAG_MOVE_UP] = true ; return false ; case CODE_ANSI_DOWN: intendedmove[FLAG_MOVE_DOWN] = true ; return false ; case CODE_ANSI_RIGHT: intendedmove[FLAG_MOVE_RIGHT] = true ; return false ; case CODE_ANSI_LEFT: intendedmove[FLAG_MOVE_LEFT] = true ; return false ; } } } return true ; } bool check_input_vim (char c, intendedmove_t & intendedmove) { using namespace Keypress::Code; switch (toupper (c)) { case CODE_VIM_UP: intendedmove[FLAG_MOVE_UP] = true ; return false ; case CODE_VIM_LEFT: intendedmove[FLAG_MOVE_LEFT] = true ; return false ; case CODE_VIM_DOWN: intendedmove[FLAG_MOVE_DOWN] = true ; return false ; case CODE_VIM_RIGHT: intendedmove[FLAG_MOVE_RIGHT] = true ; return false ; } return true ; } bool check_input_wasd (char c, intendedmove_t & intendedmove) { using namespace Keypress::Code; switch (toupper (c)) { case CODE_WASD_UP: intendedmove[FLAG_MOVE_UP] = true ; return false ; case CODE_WASD_LEFT: intendedmove[FLAG_MOVE_LEFT] = true ; return false ; case CODE_WASD_DOWN: intendedmove[FLAG_MOVE_DOWN] = true ; return false ; case CODE_WASD_RIGHT: intendedmove[FLAG_MOVE_RIGHT] = true ; return false ; } return true ; } } }
global.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #ifndef GLOBAL_H #define GLOBAL_H #include <iosfwd> #include <string> using ull = unsigned long long ;template <typename T>void DrawAlways (std ::ostream& os, T f) { os << f(); } template <typename T>void DrawOnlyWhen (std ::ostream& os, bool trigger, T f) { if (trigger) { DrawAlways(os, f); } } template <typename T>void DrawAsOneTimeFlag (std ::ostream& os, bool & trigger, T f) { if (trigger) { DrawAlways(os, f); trigger = !trigger; } } template <typename suppliment_t >struct DataSupplimentInternalType { suppliment_t suppliment_data; template <typename function_t > std ::string operator () (function_t f) const { return f(suppliment_data); } }; template <typename suppliment_t , typename function_t >auto DataSuppliment (suppliment_t needed_data, function_t f) { using dsit_t = DataSupplimentInternalType<suppliment_t >; const auto lambda_f_to_return = [=]() { const dsit_t depinject_func = dsit_t { needed_data }; return depinject_func(f); }; return lambda_f_to_return; } void clearScreen () ;void getKeypressDownInput (char & c) ;std ::string secondsFormat (double sec) ;#endif
global.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 #include "global.hpp" #include <iostream> #include <sstream> void clearScreen () {#ifdef _WIN32 system("cls" ); #else system("clear" ); #endif } #ifdef _WIN32 void getKeypressDownInput (char & c) { std ::cin >> c; } #else # include <termios.h> # include <unistd.h> char getch () { char buf = 0 ; struct termios old = { 0 }; if (tcgetattr(0 , &old) < 0 ) perror("tcsetattr()" ); old.c_lflag &= ~ICANON; old.c_lflag &= ~ECHO; old.c_cc[VMIN] = 1 ; old.c_cc[VTIME] = 0 ; if (tcsetattr(0 , TCSANOW, &old) < 0 ) perror("tcsetattr ICANON" ); if (read(0 , &buf, 1 ) < 0 ) perror("read()" ); old.c_lflag |= ICANON; old.c_lflag |= ECHO; if (tcsetattr(0 , TCSADRAIN, &old) < 0 ) perror("tcsetattr ~ICANON" ); return (buf); } void getKeypressDownInput (char & c) { c = getch(); } #endif std ::string secondsFormat (double sec) { double second = sec; int minute = second / 60 ; int hour = minute / 60 ; second -= minute * 60 ; minute %= 60 ; second = static_cast <int >(second); std ::ostringstream oss; if (hour) { oss << hour << "h " ; } if (minute) { oss << minute << "m " ; } oss << second << "s" ; return oss.str(); }
tile-graphics.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #ifndef TILE_GRAPHICS_H #define TILE_GRAPHICS_H #include <string> namespace Game{ struct tile_t ; std ::string drawTileString (tile_t currentTile) ; } #endif
tile-graphics.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include "tile.hpp" #include "tile-graphics.hpp" #include "color.hpp" #include <iomanip> // setw #include <sstream> #include <vector> #include <cmath> namespace Game{ namespace { Color::Modifier tileColor (ull value) { std ::vector <Color::Modifier> colors{ red, yellow, magenta, blue, cyan, yellow, red, yellow, magenta, blue, green }; int log = log2(value); int index = log < 12 ? log - 1 : 10 ; return colors[index]; } } std ::string drawTileString (tile_t currentTile) { std ::ostringstream tile_richtext; if (!currentTile.value) { tile_richtext << " " ; } else { tile_richtext << tileColor(currentTile.value) << bold_on << std ::setw(4 ) << currentTile.value << bold_off << def; } return tile_richtext.str(); } }
阶段六 构建 接着完善soloGameLoop()剩余部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 std::tuple<bool, current_game_session_t> soloGameLoop(current_game_session_t cgs) { ... *pGamestatus = update_one_shot_display_flags(*pGamestatus); intendedmove_t player_intendedmove{}; std ::tie(player_intendedmove, *pGamestatus) = receive_agent_input(player_intendedmove, *pGamestatus); std ::tie(std ::ignore, *pGameboard) = process_agent_input(player_intendedmove, *pGameboard); bool loop_again{ false }; std ::tie(loop_again, *pGamestatus) = process_gameStatus(std ::make_tuple(*pGamestatus, *pGameboard)); return std ::make_tuple(loop_again, cgs); }
查看update_one_shot_display_flags()函数定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 gamestatus_t update_one_shot_display_flags (gamestatus_t gamestatus) { const auto disable_one_shot_flag = [](bool & trigger) { trigger = !trigger; }; if (gamestatus[FLAG_ONE_SHOT]) { disable_one_shot_flag(gamestatus[FLAG_ONE_SHOT]); if (gamestatus[FLAG_SAVED_GAME]) { disable_one_shot_flag(gamestatus[FLAG_SAVED_GAME]); } if (gamestatus[FLAG_INPUT_ERROR]) { disable_one_shot_flag(gamestatus[FLAG_INPUT_ERROR]); } } return gamestatus; }
要理解update_one_shot_display_flags(),需理解:游戏上一轮进入soloGameLoop(),假设玩家在行动时选择保存游戏或是输入非法值,(仍是在上一轮中)soloGameLoop()后续代码执行游戏存储操作或是提醒玩家重新输入,然后,上一轮soloGameLoop()便结束了。只要soloGameLoop()返回的loop_again值为真,又会开启下一轮循环。在新一轮的循环中,仍是先打印画面,接着读取输入。但必须注意的是,上一轮的一些游戏状态flags,如FLAG_SAVED_GAME或FLAG_INPUT_ERROR,在上轮循环之后并没有复位,因此update_one_shot_display_flags()被创建出来,用以复位上一轮部分标志位。
复位工作完成后,该要读取用户输入了,该工作由receive_agent_input()函数完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 using bool_gamestatus_t = std ::tuple<bool , gamestatus_t >;bool_gamestatus_t check_input_other (char c, gamestatus_t gamestatus) { using namespace Input::Keypress::Code; auto is_invalid_keycode{ true }; switch (toupper (c)) { case CODE_HOTKEY_ACTION_SAVE: case CODE_HOTKEY_ALTERNATE_ACTION_SAVE: gamestatus[FLAG_ONE_SHOT] = true ; gamestatus[FLAG_SAVED_GAME] = true ; is_invalid_keycode = false ; break ; case CODE_HOTKEY_QUIT_ENDLESS_MODE: if (gamestatus[FLAG_ENDLESS_MODE]) { gamestatus[FLAG_END_GAME] = true ; is_invalid_keycode = false ; } break ; } return std ::make_tuple(is_invalid_keycode, gamestatus); } using intendedmove_gamestatus_t = std ::tuple<Input::intendedmove_t , gamestatus_t >; intendedmove_gamestatus_t receive_agent_input(Input::intendedmove_t intendedmove, gamestatus_t gamestatus) { using namespace Input; const bool game_still_in_play = !gamestatus[FLAG_END_GAME] && !gamestatus[FLAG_WIN]; if (game_still_in_play) { char c; getKeypressDownInput(c); const auto is_invalid_keypress_code = check_input_ansi(c, intendedmove) && check_input_wasd(c, intendedmove) && check_input_vim(c, intendedmove); bool is_invalid_special_keypress_code{ false }; std ::tie(is_invalid_special_keypress_code, gamestatus) = check_input_other(c, gamestatus); if (is_invalid_keypress_code && is_invalid_special_keypress_code) { gamestatus[FLAG_ONE_SHOT] = true ; gamestatus[FLAG_INPUT_ERROR] = true ; } } return std ::make_tuple(intendedmove, gamestatus); }
先分析receive_agent_input()。
1 2 const bool game_still_in_play = !gamestatus[FLAG_END_GAME] && !gamestatus[FLAG_WIN];
游戏结束,或是玩家胜利时,都有单独的交互逻辑,需特别处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 const auto is_invalid_keypress_code = check_input_ansi(c, intendedmove) && check_input_wasd(c, intendedmove) && check_input_vim(c, intendedmove); bool is_invalid_special_keypress_code{ false };std ::tie(is_invalid_special_keypress_code, gamestatus) = check_input_other(c, gamestatus); if (is_invalid_keypress_code && is_invalid_special_keypress_code) { gamestatus[FLAG_ONE_SHOT] = true ; gamestatus[FLAG_INPUT_ERROR] = true ; }
玩家的输入按键可分为三类:或是方向移动相关按键类,或是保存等特殊按键类,或是非法按键类。上述代码便是用于判断本轮玩家输入究竟属于哪一类。辅助函数check_input_other()便是用于检测玩家按键是否为特殊按键类。
很好,现在读取了玩家输入,该进行输入处理了。process_agent_input()负责输入处理事宜。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 enum Directions { UP, DOWN, RIGHT, LEFT };GameBoard decideMove (Directions dir, GameBoard gb) { switch (dir) { case UP: tumbleTilesUpOnGameboard(gb); break ; case DOWN: tumbleTilesDownOnGameboard(gb); break ; case LEFT: tumbleTilesLeftOnGameboard(gb); break ; case RIGHT: tumbleTilesRightOnGameboard(gb); break ; } return gb; } using bool_gameboard_t = std ::tuple<bool , GameBoard>;bool_gameboard_t process_agent_input (Input::intendedmove_t intendedmove, GameBoard gb) { using namespace Input; if (intendedmove[FLAG_MOVE_LEFT]) { gb = decideMove(LEFT, gb); } if (intendedmove[FLAG_MOVE_RIGHT]) { gb = decideMove(RIGHT, gb); } if (intendedmove[FLAG_MOVE_UP]) { gb = decideMove(UP, gb); } if (intendedmove[FLAG_MOVE_DOWN]) { gb = decideMove(DOWN, gb); } return std ::make_tuple(true , gb); }
分析略。
现在进入soloGameLoop()最后一个阶段:根据本轮loop玩家行动后的场上局势,判断是否应开启下一轮循环。process_gameStatus()负责进行判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 bool check_input_check_to_end_game (char c) { using namespace Input::Keypress::Code; switch (std ::toupper (c)) { case CODE_HOTKEY_CHOICE_NO: return true ; } return false ; } bool continue_playing_game (std ::istream& in_os) { char letter_choice; in_os >> letter_choice; if (check_input_check_to_end_game(letter_choice)) { return false ; } return true ; } bool_gamestatus_t process_gameStatus (gamestatus_gameboard_t gsgb) { gamestatus_t gamestatus; GameBoard gb; std ::tie(gamestatus, gb) = gsgb; auto loop_again{ true }; if (!gamestatus[FLAG_ENDLESS_MODE]) { if (gamestatus[FLAG_WIN]) { if (continue_playing_game(std ::cin )) { gamestatus[FLAG_ENDLESS_MODE] = true ; gamestatus[FLAG_QUESTION_STAY_OR_QUIT] = false ; gamestatus[FLAG_WIN] = false ; } else { loop_again = false ; } } } if (gamestatus[FLAG_END_GAME]) { loop_again = false ; } if (gamestatus[FLAG_SAVED_GAME]) { } gamestatus[FLAG_GAME_IS_ASKING_QUESTION_MODE] = false ; return std ::make_tuple(loop_again, gamestatus); }
关于游戏保存的实现留到以后,现在,让我们回到endlessGameLoop()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 GameBoard endlessGameLoop (ull currentBestScore, competition_mode_t cm, GameBoard gb) { auto loop_again{ true }; auto currentgamestatus = std ::make_tuple(currentBestScore, cm, gamestatus_t {}, gb); while (loop_again) { std ::tie(loop_again, currentgamestatus) = soloGameLoop(currentgamestatus); } DrawAlways(std ::cout , DataSuppliment(currentgamestatus, drawEndGameLoopGraphics)); return gb; }
while循环的条件不再满足时,便是游戏结束时,需由drawEndGameLoopGraphics()打印游戏结束字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 std ::string drawEndGameLoopGraphics (current_game_session_t finalgamestatus) { using namespace Graphics; using namespace Gameboard::Graphics; using tup_idx = tuple_cgs_t_idx; const auto bestScore = std ::get<tup_idx::IDX_BESTSCORE>(finalgamestatus); const auto comp_mode = std ::get<tup_idx::IDX_COMP_MODE>(finalgamestatus); const auto gb = std ::get<tup_idx::IDX_GAMEBOARD>(finalgamestatus); const auto end_gamestatus = std ::get<tup_idx::IDX_GAMESTATUS>(finalgamestatus); std ::ostringstream str_os; clearScreen(); DrawAlways(str_os, AsciiArt2048); const auto scdd = make_scoreboard_display_data(bestScore, comp_mode, gb); DrawAlways(str_os, DataSuppliment(scdd, GameScoreBoardOverlay)); DrawAlways(str_os, DataSuppliment(gb, GameBoardTextOutput)); const auto esdd = make_end_screen_display_data(end_gamestatus); DrawAlways(str_os, DataSuppliment(esdd, GameEndScreenOverlay)); return str_os.str(); }
游戏结束后,清屏,重新打印Title Art、计分板、棋盘以及游戏结束语。涉及的函数列举如下:
game.cpp
1 2 3 4 5 6 7 8 9 Graphics::end_screen_display_data_t make_end_screen_display_data(gamestatus_t world_gamestatus) { const auto esdd = std ::make_tuple(world_gamestatus[FLAG_WIN], world_gamestatus[FLAG_ENDLESS_MODE]); return esdd; };
game-graphics.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 std ::string GameOverWinPrompt () { 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 GameOverLosePrompt () { 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 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, GameOverWinPrompt); DrawOnlyWhen(str_os, !did_win, GameOverLosePrompt); return str_os.str(); }; DrawOnlyWhen(str_os, !is_endless_mode, standardWinLosePrompt); DrawOnlyWhen(str_os, is_endless_mode, EndOfEndlessPrompt); return str_os.str(); }
至此,游戏游玩的核心部分已基本完成,我们的游戏已能够顺利游玩。
(本游戏虽在windows与unix环境下均可运行,但实测windows环境下方向键输入有问题,且按键输入时必须手动回车才能读入,以上问题留到游戏整体完成后再做调整)
现在,考虑完善游戏保存功能。文件saveresource.*负责数据保存工作。
saveresource.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #ifndef SAVERESOURCE_H #define SAVERESOURCE_H #include <string> #include <tuple> namespace Game { struct GameBoard ; namespace Saver { void saveGamePlayState (GameBoard gb) ; } } #endif
saveresource.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include "saveresource.hpp" #include "gameboard.hpp" #include <fstream> namespace Game{ namespace Saver { namespace { bool generateFilefromPreviousGameStateData (std ::ostream& os, const GameBoard& gb) { os << printStateOfGameBoard(gb); return true ; } void saveToFilePreviousGameStateData (std ::string filename, const GameBoard& gb) { std ::ofstream stateFile (filename, std ::ios_base::app) ; generateFilefromPreviousGameStateData(stateFile, gb); } bool generateFilefromPreviousGameStatisticsData (std ::ostream& os, const GameBoard& gb) { os << gb.score << ":" << MoveCountOnGameBoard(gb); return true ; } void saveToFilePreviousGameStatisticsData (std ::string filename, const GameBoard& gb) { std ::ofstream stats (filename, std ::ios_base::app) ; generateFilefromPreviousGameStatisticsData(stats, gb); } } } void saveGamePlayState (GameBoard gb) { constexpr auto filename_game_data_state = "../data/previousGame.txt" ; constexpr auto filename_game_data_statistics = "../data/previousGameStats.txt" ; std ::remove(filename_game_data_state); std ::remove(filename_game_data_statistics); saveToFilePreviousGameStateData(filename_game_data_state, gb); saveToFilePreviousGameStatisticsData(filename_game_data_statistics, gb); } }
加上这两个文件后,编译会报错,原因是generateFilefromPreviousGameStatisticsData()的参数类型为const GameBoard&,而
MoveCountOnGameBoard()函数参数为GameBoard&,只需将MoveCountOnGameBoard()声明与定义均加上const即可。
数据存储实现了,数据加载自然也不能落下。loadresource.*负责数据加载模块。
loadresource.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #ifndef LOADRESOURCE_H #define LOADRESOURCE_H #include <string> #include <tuple> namespace Game { using load_gameboard_status_t = std ::tuple<bool , struct GameBoard>; namespace Loader { load_gameboard_status_t load_GameBoard_data_from_file (std ::string filename) ; std ::tuple<bool , std ::tuple<unsigned long long , long long >> load_game_stats_from_file(std ::string filename); } } #endif
loadresource与saveresource镜像。
loadresource.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 #include "loadresource.hpp" #include "global.hpp" #include "tile.hpp" #include <fstream> #include <array> #include <string> #include <sstream> #include <vector> #include <iostream> #include <algorithm> namespace Game{ namespace Loader { namespace { int GetLines (std ::string filename) { std ::ifstream stateFile (filename) ; using iter = std ::istreambuf_iterator<char >; const auto noOfLines = std ::count(iter{ stateFile }, iter{}, '\n' ); return noOfLines; } std ::vector <std ::string > get_file_tile_data (std ::istream& buf) { std ::vector <std ::string > tempbuffer; enum { MAX_WIDTH = 10 , MAX_HEIGHT = 10 }; auto i{ 0 }; for (std ::string tempLine; std ::getline(buf, tempLine) && i < MAX_WIDTH; i++) { std ::istringstream temp_filestream (tempLine) ; auto j{ 0 }; for (std ::string a_word; std ::getline(temp_filestream, a_word, ',' ) && j < MAX_HEIGHT; j++) { tempbuffer.push_back(a_word); } } return tempbuffer; } std ::vector <tile_t > process_file_tile_string_data(std ::vector <std ::string > buf) { std ::vector <tile_t > result_buf; auto tile_processed_counter{ 0 }; const auto prime_tile_data = [&tile_processed_counter](const std ::string tile_data) { enum FieldIndex { IDX_TILE_VALUE, IDX_TILE_BLOCKED, MAX_NO_TILE_IDXS }; std ::array <int , MAX_NO_TILE_IDXS> tile_internal{}; std ::istringstream blocks (tile_data) ; auto idx_id{ 0 }; for (std ::string temptiledata; std ::getline( blocks, temptiledata, ':' ) ; idx_id++) { switch (idx_id) { case IDX_TILE_VALUE: std ::get<IDX_TILE_VALUE>(tile_internal) = std ::stoi(temptiledata); break ; case IDX_TILE_BLOCKED: std ::get<IDX_TILE_BLOCKED>(tile_internal) = std ::stoi(temptiledata); break ; default : std ::cout << "ERROR: [tile_processed_counter: " << tile_processed_counter << "]: Read past MAX_NO_TILE_IDXS! (idx no:" << MAX_NO_TILE_IDXS << ")\n" ; } } tile_processed_counter++; const unsigned long long tile_value = std ::get<IDX_TILE_VALUE>(tile_internal); const bool tile_blocked = std ::get<IDX_TILE_BLOCKED>(tile_internal); return tile_t { tile_value, tile_blocked }; }; std ::transform(std ::begin(buf), std ::end(buf), std ::back_inserter(result_buf), prime_tile_data); return result_buf; } std ::tuple<bool , std ::tuple<ull, long long >> get_and_process_game_stats_string_data(std ::istream& stats_file) { if (stats_file) { ull score{}; long long moveCount{}; for (std ::string tempLine; std ::getline(stats_file, tempLine);) { enum GameStatsFieldIndex { IDX_GAME_SCORE_VALUE, IDX_GAME_MOVECOUNT, MAX_NO_GAME_STATS_IDXS }; std ::istringstream line (tempLine) ; auto idx_id{ 0 }; for (std ::string temp; std ::getline(line, temp, ':' ); idx_id++) { switch (idx_id) { case IDX_GAME_SCORE_VALUE: score = std ::stoi(temp); break ; case IDX_GAME_MOVECOUNT: moveCount = std ::stoi(temp) - 1 ; break ; default : break ; } } } return std ::make_tuple(true , std ::make_tuple(score, moveCount)); } return std ::make_tuple(false , std ::make_tuple(0 , 0 )); } } load_gameboard_status_t load_GameBoard_data_from_file (std ::string filename) { std ::ifstream stateFile (filename) ; if (stateFile) { const ull savedBoardPlaySize = GetLines(filename); const auto file_tile_data = get_file_tile_data(stateFile); const auto processed_tile_data = process_file_tile_string_data(file_tile_data); return std ::make_tuple(true , GameBoard(savedBoardPlaySize, processed_tile_data)); } return std ::make_tuple(false , GameBoard{}); } std ::tuple<bool , std ::tuple<ull, long long >> load_game_stats_from_file(std ::string filename) { std ::ifstream stats (filename) ; return get_and_process_game_stats_string_data(stats); } } }
介绍略。
现在,游戏数据的存储与加载均实现了,可以开始完善continue game功能了。
menu.cpp。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void continueGame () { Game::continueGame(); } void processPlayerInput () { if (menustatus[FLAG_START_GAME]) { startGame(); } if (menustatus[FLAG_CONTINUE_GAME]) { continueGame(); } if (menustatus[FLAG_DISPLAY_HIGHSCORES]) { } if (menustatus[FLAG_EXIT_GAME]) { exit (EXIT_SUCCESS); } }
game.cpp
1 2 3 4 void continueGame () { PreGameSetup::ContinueOldGame(); }
game-pregame.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 load_gameboard_status_t initialiseContinueBoardArray () { using namespace Loader; constexpr auto gameboard_data_filename = "../data/previousGame.txt" ; constexpr auto game_stats_data_filename = "../data/previousGameStats.txt" ; auto loaded_gameboard{ false }; auto loaded_game_stats{ false }; auto tempGBoard = GameBoard{ 1 }; auto score_and_movecount = std ::tuple<decltype (tempGBoard.score), decltype (tempGBoard.moveCount)>{}; std ::tie(loaded_gameboard, tempGBoard) = load_GameBoard_data_from_file(gameboard_data_filename); std ::tie(loaded_game_stats, score_and_movecount) = load_game_stats_from_file(game_stats_data_filename); std ::tie(tempGBoard.score, tempGBoard.moveCount) = score_and_movecount; const auto all_files_loaded_ok = (loaded_gameboard && loaded_game_stats); return std ::make_tuple(all_files_loaded_ok, tempGBoard); } void DoContinueOldGame () { bool load_old_game_ok{ false }; GameBoard oldGameBoard; std ::tie(load_old_game_ok, oldGameBoard) = initialiseContinueBoardArray(); if (load_old_game_ok) { playGame(PlayGameFlag::ContinuePreviousGame, oldGameBoard); } else { SetupNewGame(NewGameFlag::NoPreviousSaveAvailable); } } void ContinueOldGame () { DoContinueOldGame(); }
至此,old game的加载也已实现,阶段六告一段落。
代码 game.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 #include "game.hpp" #include "game-graphics.hpp" #include "game-input.hpp" #include "game-pregame.hpp" #include "gameboard.hpp" #include "gameboard-graphics.hpp" #include "global.hpp" #include "saveresource.hpp" #include "loadresource.hpp" #include "statistics.hpp" #include "scoreboard.hpp" #include <array> #include <chrono> #include <iostream> #include <sstream> namespace Game{ namespace { enum { COMPETITION_GAME_BOARD_SIZE = 4 }; using competition_mode_t = bool ; enum GameStatusFlag { FLAG_WIN, FLAG_END_GAME, FLAG_ONE_SHOT, FLAG_SAVED_GAME, FLAG_INPUT_ERROR, FLAG_ENDLESS_MODE, FLAG_GAME_IS_ASKING_QUESTION_MODE, FLAG_QUESTION_STAY_OR_QUIT, MAX_NO_GAME_STATUS_FLAGS }; using gamestatus_t = std ::array <bool , MAX_NO_GAME_STATUS_FLAGS>; using current_game_session_t = std ::tuple<ull, competition_mode_t , gamestatus_t , GameBoard>; enum tuple_cgs_t_idx { IDX_BESTSCORE, IDX_COMP_MODE, IDX_GAMESTATUS, IDX_GAMEBOARD }; Scoreboard::Score makeFinalscoreFromGameSession (double duration, GameBoard gb) { Scoreboard::Score finalscore{}; finalscore.score = gb.score; finalscore.win = hasWonOnGameboard(gb); finalscore.moveCount = MoveCountOnGameBoard(gb); finalscore.largestTile = gb.largestTile; finalscore.duration = duration; return finalscore; } void doPostGameSaveStuff (Scoreboard::Score finalscore, competition_mode_t cm) { if (cm) { Statistics::createFinalScoreAndEndGameDataFile(std ::cout , std ::cin , finalscore); } } using gamestatus_gameboard_t = std ::tuple<gamestatus_t , GameBoard>; gamestatus_gameboard_t processGameLogic (gamestatus_gameboard_t gsgb) { gamestatus_t gamestatus; GameBoard gb; std ::tie(gamestatus, gb) = gsgb; unblockTilesOnGameboard(gb); if (gb.moved) { addTileOnGameboard(gb); registerMoveByOneOnGameboard(gb); } if (!gamestatus[FLAG_ENDLESS_MODE]) { if (hasWonOnGameboard(gb)) { gamestatus[FLAG_WIN] = true ; gamestatus[FLAG_GAME_IS_ASKING_QUESTION_MODE] = true ; gamestatus[FLAG_QUESTION_STAY_OR_QUIT] = true ; } } if (!canMoveOnGameboard(gb)) { gamestatus[FLAG_END_GAME] = true ; } return std ::make_tuple(gamestatus, gb); } Graphics::scoreboard_display_data_t make_scoreboard_display_data(ull bestScore, competition_mode_t cm, GameBoard gb) { const auto gameboardScore = gb.score; const auto tmpBestScore = (bestScore < gb.score ? gb.score : bestScore); const auto comp_mode = cm; const auto movecount = MoveCountOnGameBoard(gb); const auto scdd = std ::make_tuple(comp_mode, std ::to_string(gameboardScore), std ::to_string(tmpBestScore), std ::to_string(movecount)); return scdd; }; std ::string DisplayGameQuestionsToPlayerPrompt (gamestatus_t gamestatus) { using namespace Graphics; std ::ostringstream str_os; DrawOnlyWhen(str_os, gamestatus[FLAG_QUESTION_STAY_OR_QUIT], QuestionEndOfWinningGamePrompt); return str_os.str(); } Graphics::input_controls_display_data_t make_input_controls_display_data(gamestatus_t gamestatus) { const auto icdd = std ::make_tuple(gamestatus[FLAG_ENDLESS_MODE], gamestatus[FLAG_QUESTION_STAY_OR_QUIT]); return icdd; }; std ::string drawGraphics (current_game_session_t cgs) { using namespace Graphics; using namespace Gameboard::Graphics; using tup_idx = tuple_cgs_t_idx; const auto bestScore = std ::get<tup_idx::IDX_BESTSCORE>(cgs); const auto comp_mode = std ::get<tup_idx::IDX_COMP_MODE>(cgs); const auto gamestatus = std ::get<tup_idx::IDX_GAMESTATUS>(cgs); const auto gb = std ::get<tup_idx::IDX_GAMEBOARD>(cgs); std ::ostringstream str_os; clearScreen(); DrawAlways(str_os, AsciiArt2048); const auto scdd = make_scoreboard_display_data(bestScore, comp_mode, gb); DrawAlways(str_os, DataSuppliment(scdd, GameScoreBoardOverlay)); DrawAlways(str_os, DataSuppliment(gb, GameBoardTextOutput)); DrawOnlyWhen(str_os, gamestatus[FLAG_SAVED_GAME], GameStateNowSavedPrompt); DrawOnlyWhen(str_os, gamestatus[FLAG_GAME_IS_ASKING_QUESTION_MODE], DataSuppliment(gamestatus, DisplayGameQuestionsToPlayerPrompt)); const auto input_controls_display_data = make_input_controls_display_data(gamestatus); DrawAlways(str_os, DataSuppliment(input_controls_display_data, GameInputControlsOverlay)); DrawOnlyWhen(str_os, gamestatus[FLAG_INPUT_ERROR], InvalidInputGameBoardErrorPrompt); return str_os.str(); } gamestatus_t update_one_shot_display_flags (gamestatus_t gamestatus) { const auto disable_one_shot_flag = [](bool & trigger) { trigger = !trigger; }; if (gamestatus[FLAG_ONE_SHOT]) { disable_one_shot_flag(gamestatus[FLAG_ONE_SHOT]); if (gamestatus[FLAG_SAVED_GAME]) { disable_one_shot_flag(gamestatus[FLAG_SAVED_GAME]); } if (gamestatus[FLAG_INPUT_ERROR]) { disable_one_shot_flag(gamestatus[FLAG_INPUT_ERROR]); } } return gamestatus; } using bool_gamestatus_t = std ::tuple<bool , gamestatus_t >; bool_gamestatus_t check_input_other (char c, gamestatus_t gamestatus) { using namespace Input::Keypress::Code; auto is_invalid_keycode{ true }; switch (toupper (c)) { case CODE_HOTKEY_ACTION_SAVE: case CODE_HOTKEY_ALTERNATE_ACTION_SAVE: gamestatus[FLAG_ONE_SHOT] = true ; gamestatus[FLAG_SAVED_GAME] = true ; is_invalid_keycode = false ; break ; case CODE_HOTKEY_QUIT_ENDLESS_MODE: if (gamestatus[FLAG_ENDLESS_MODE]) { gamestatus[FLAG_END_GAME] = true ; is_invalid_keycode = false ; } break ; } return std ::make_tuple(is_invalid_keycode, gamestatus); } using intendedmove_gamestatus_t = std ::tuple<Input::intendedmove_t , gamestatus_t >; intendedmove_gamestatus_t receive_agent_input(Input::intendedmove_t intendedmove, gamestatus_t gamestatus) { using namespace Input; const bool game_still_in_play = !gamestatus[FLAG_END_GAME] && !gamestatus[FLAG_WIN]; if (game_still_in_play) { char c; getKeypressDownInput(c); const auto is_invalid_keypress_code = check_input_ansi(c, intendedmove) && check_input_wasd(c, intendedmove) && check_input_vim(c, intendedmove); bool is_invalid_special_keypress_code{ false }; std ::tie(is_invalid_special_keypress_code, gamestatus) = check_input_other(c, gamestatus); if (is_invalid_keypress_code && is_invalid_special_keypress_code) { gamestatus[FLAG_ONE_SHOT] = true ; gamestatus[FLAG_INPUT_ERROR] = true ; } } return std ::make_tuple(intendedmove, gamestatus); } enum Directions { UP, DOWN, RIGHT, LEFT }; GameBoard decideMove (Directions dir, GameBoard gb) { switch (dir) { case UP: tumbleTilesUpOnGameboard(gb); break ; case DOWN: tumbleTilesDownOnGameboard(gb); break ; case LEFT: tumbleTilesLeftOnGameboard(gb); break ; case RIGHT: tumbleTilesRightOnGameboard(gb); break ; } return gb; } using bool_gameboard_t = std ::tuple<bool , GameBoard>; bool_gameboard_t process_agent_input (Input::intendedmove_t intendedmove, GameBoard gb) { using namespace Input; if (intendedmove[FLAG_MOVE_LEFT]) { gb = decideMove(LEFT, gb); } if (intendedmove[FLAG_MOVE_RIGHT]) { gb = decideMove(RIGHT, gb); } if (intendedmove[FLAG_MOVE_UP]) { gb = decideMove(UP, gb); } if (intendedmove[FLAG_MOVE_DOWN]) { gb = decideMove(DOWN, gb); } return std ::make_tuple(true , gb); } bool check_input_check_to_end_game (char c) { using namespace Input::Keypress::Code; switch (std ::toupper (c)) { case CODE_HOTKEY_CHOICE_NO: return true ; } return false ; } bool continue_playing_game (std ::istream& in_os) { char letter_choice; in_os >> letter_choice; if (check_input_check_to_end_game(letter_choice)) { return false ; } return true ; } bool_gamestatus_t process_gameStatus (gamestatus_gameboard_t gsgb) { gamestatus_t gamestatus; GameBoard gb; std ::tie(gamestatus, gb) = gsgb; auto loop_again{ true }; if (!gamestatus[FLAG_ENDLESS_MODE]) { if (gamestatus[FLAG_WIN]) { if (continue_playing_game(std ::cin )) { gamestatus[FLAG_ENDLESS_MODE] = true ; gamestatus[FLAG_QUESTION_STAY_OR_QUIT] = false ; gamestatus[FLAG_WIN] = false ; } else { loop_again = false ; } } } if (gamestatus[FLAG_END_GAME]) { loop_again = false ; } if (gamestatus[FLAG_SAVED_GAME]) { Saver::saveGamePlayState(gb); } gamestatus[FLAG_GAME_IS_ASKING_QUESTION_MODE] = false ; return std ::make_tuple(loop_again, gamestatus); } std::tuple<bool, current_game_session_t> soloGameLoop(current_game_session_t cgs) { using namespace Input; using tup_idx = tuple_cgs_t_idx; const auto pGamestatus = std ::addressof(std ::get<tup_idx::IDX_GAMESTATUS>(cgs)); const auto pGameboard = std ::addressof(std ::get<tup_idx::IDX_GAMEBOARD>(cgs)); std ::tie(*pGamestatus, *pGameboard) = processGameLogic(std ::make_tuple(*pGamestatus, *pGameboard)); DrawAlways(std ::cout , DataSuppliment(cgs, drawGraphics)); *pGamestatus = update_one_shot_display_flags(*pGamestatus); intendedmove_t player_intendedmove{}; std ::tie(player_intendedmove, *pGamestatus) = receive_agent_input(player_intendedmove, *pGamestatus); std ::tie(std ::ignore, *pGameboard) = process_agent_input(player_intendedmove, *pGameboard); bool loop_again{ false }; std ::tie(loop_again, *pGamestatus) = process_gameStatus(std ::make_tuple(*pGamestatus, *pGameboard)); return std ::make_tuple(loop_again, cgs); } Graphics::end_screen_display_data_t make_end_screen_display_data(gamestatus_t world_gamestatus) { const auto esdd = std ::make_tuple(world_gamestatus[FLAG_WIN], world_gamestatus[FLAG_ENDLESS_MODE]); return esdd; }; std ::string drawEndGameLoopGraphics (current_game_session_t finalgamestatus) { using namespace Graphics; using namespace Gameboard::Graphics; using tup_idx = tuple_cgs_t_idx; const auto bestScore = std ::get<tup_idx::IDX_BESTSCORE>(finalgamestatus); const auto comp_mode = std ::get<tup_idx::IDX_COMP_MODE>(finalgamestatus); const auto gb = std ::get<tup_idx::IDX_GAMEBOARD>(finalgamestatus); const auto end_gamestatus = std ::get<tup_idx::IDX_GAMESTATUS>(finalgamestatus); std ::ostringstream str_os; clearScreen(); DrawAlways(str_os, AsciiArt2048); const auto scdd = make_scoreboard_display_data(bestScore, comp_mode, gb); DrawAlways(str_os, DataSuppliment(scdd, GameScoreBoardOverlay)); DrawAlways(str_os, DataSuppliment(gb, GameBoardTextOutput)); const auto esdd = make_end_screen_display_data(end_gamestatus); DrawAlways(str_os, DataSuppliment(esdd, GameEndScreenOverlay)); return str_os.str(); } GameBoard endlessGameLoop (ull currentBestScore, competition_mode_t cm, GameBoard gb) { auto loop_again{ true }; auto currentgamestatus = std ::make_tuple(currentBestScore, cm, gamestatus_t {}, gb); while (loop_again) { std ::tie(loop_again, currentgamestatus) = soloGameLoop(currentgamestatus); } DrawAlways(std ::cout , DataSuppliment(currentgamestatus, drawEndGameLoopGraphics)); return gb; } } void playGame (PlayGameFlag flag, GameBoard gb, ull boardSize) { const auto isThisNewlyGame = (flag == PlayGameFlag::BrandNewGame); const auto isCompetitionMode = (boardSize == COMPETITION_GAME_BOARD_SIZE); const auto bestScore = Statistics::loadBestScore(); if (isThisNewlyGame) { gb = GameBoard(boardSize); addTileOnGameboard(gb); } const auto startTime = std ::chrono::high_resolution_clock::now(); gb = endlessGameLoop(bestScore, isCompetitionMode, gb); const auto finishTime = std ::chrono::high_resolution_clock::now(); const std ::chrono::duration<double > elapsed = finishTime - startTime; const auto duration = elapsed.count(); if (isThisNewlyGame) { const auto finalscore = makeFinalscoreFromGameSession(duration, gb); doPostGameSaveStuff(finalscore, isCompetitionMode); } } void startGame () { PreGameSetup::SetupNewGame(); } void continueGame () { PreGameSetup::ContinueOldGame(); } }
game-graphics.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #ifndef GAME_GRAPHICS_H #define GAME_GRAPHICS_H #include <string> #include <tuple> enum GameBoardDimensions { MIN_GAME_BOARD_PLAY_SIZE = 3 , MAX_GAME_BOARD_PLAY_SIZE = 10 }; namespace Game{ namespace Graphics { std ::string AsciiArt2048 () ; std ::string BoardSizeErrorPrompt () ; std ::string BoardInputPrompt () ; std ::string GameBoardNoSaveErrorPrompt () ; std ::string GameStateNowSavedPrompt () ; std ::string QuestionEndOfWinningGamePrompt () ; std ::string InvalidInputGameBoardErrorPrompt () ; using scoreboard_display_data_t = std ::tuple<bool , std ::string , std ::string , std ::string >; std ::string GameScoreBoardBox (scoreboard_display_data_t scdd) ; std ::string GameScoreBoardOverlay (scoreboard_display_data_t scdd) ; std ::string InputCommandListPrompt () ; std ::string EndlessModeCommandListPrompt () ; std ::string InputCommandListFooterPrompt () ; using input_controls_display_data_t = std ::tuple<bool , bool >; std ::string GameInputControlsOverlay (input_controls_display_data_t gamestatus) ; std ::string GameOverWinPrompt () ; std ::string GameOverLosePrompt () ; std ::string EndOfEndlessPrompt () ; using end_screen_display_data_t = std ::tuple<bool , bool >; std ::string GameEndScreenOverlay (end_screen_display_data_t esdd) ; } } #endif
game-graphics.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 #include "game-graphics.hpp" #include "global.hpp" #include "color.hpp" #include <sstream> namespace Game{ namespace Graphics { std ::string AsciiArt2048 () { constexpr auto title_card_2048 = R"( /\\\\\\\\\ /\\\\\\\ /\\\ /\\\\\\\\\ /\\\///////\\\ /\\\/////\\\ /\\\\\ /\\\///////\\\ \/// \//\\\ /\\\ \//\\\ /\\\/\\\ \/\\\ \/\\\ /\\\/ \/\\\ \/\\\ /\\\/\/\\\ \///\\\\\\\\\/ /\\\// \/\\\ \/\\\ /\\\/ \/\\\ /\\\///////\\\ /\\\// \/\\\ \/\\\ /\\\\\\\\\\\\\\\\ /\\\ \//\\\ /\\\/ \//\\\ /\\\ \///////////\\\// \//\\\ /\\\ /\\\\\\\\\\\\\\\ \///\\\\\\\/ \/\\\ \///\\\\\\\\\/ \/////////////// \/////// \/// \///////// )" ; std ::ostringstream title_card_richtext; title_card_richtext << green << bold_on << title_card_2048 << bold_off << def; title_card_richtext << "\n\n\n" ; return title_card_richtext.str(); } std ::string BoardSizeErrorPrompt () { const auto invalid_prompt_text = { "Invalid input. Gameboard size should range from " , " to " , "." }; 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(); } 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 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 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 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 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 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:" ; enum { UI_SCOREBOARD_SIZE = 27 , UI_BORDER_OUTER_PADDING = 2 , UI_BORDER_INNER_PADDING = 1 }; constexpr auto border_padding_char = ' ' ; constexpr auto vertical_border_pattern = "│" ; constexpr auto top_board = "┌───────────────────────────┐" ; constexpr auto bottom_board = "└───────────────────────────┘" ; 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 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" }; std ::ostringstream str_os; for (const auto txt : input_commands_list_text) { str_os << sp << txt << "\n" ; } 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" ; } 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; const auto InputControlLists = [=] { std ::ostringstream str_os; DrawAlways(str_os, Graphics::InputCommandListPrompt); DrawOnlyWhen(str_os, is_in_endless_mode, Graphics::EndlessModeCommandListPrompt); DrawAlways(str_os, Graphics::InputCommandListFooterPrompt); return str_os.str(); }; DrawOnlyWhen(str_os, !is_in_question_mode, InputControlLists); return str_os.str(); } std ::string GameOverWinPrompt () { 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 GameOverLosePrompt () { 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 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, GameOverWinPrompt); DrawOnlyWhen(str_os, !did_win, GameOverLosePrompt); return str_os.str(); }; DrawOnlyWhen(str_os, !is_endless_mode, standardWinLosePrompt); DrawOnlyWhen(str_os, is_endless_mode, EndOfEndlessPrompt); return str_os.str(); } } }
saveresource.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #ifndef SAVERESOURCE_H #define SAVERESOURCE_H #include <string> #include <tuple> namespace Game { struct GameBoard ; namespace Saver { void saveGamePlayState (GameBoard gb) ; } } #endif
saveresource.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include "saveresource.hpp" #include "gameboard.hpp" #include <fstream> namespace Game{ namespace Saver { namespace { bool generateFilefromPreviousGameStateData (std ::ostream& os, const GameBoard& gb) { os << printStateOfGameBoard(gb); return true ; } void saveToFilePreviousGameStateData (std ::string filename, const GameBoard& gb) { std ::ofstream stateFile (filename, std ::ios_base::app) ; generateFilefromPreviousGameStateData(stateFile, gb); } bool generateFilefromPreviousGameStatisticsData (std ::ostream& os, const GameBoard& gb) { os << gb.score << ":" << MoveCountOnGameBoard(gb); return true ; } void saveToFilePreviousGameStatisticsData (std ::string filename, const GameBoard& gb) { std ::ofstream stats (filename, std ::ios_base::app) ; generateFilefromPreviousGameStatisticsData(stats, gb); } } void saveGamePlayState (GameBoard gb) { constexpr auto filename_game_data_state = "../data/previousGame.txt" ; constexpr auto filename_game_data_statistics = "../data/previousGameStats.txt" ; std ::remove(filename_game_data_state); std ::remove(filename_game_data_statistics); saveToFilePreviousGameStateData(filename_game_data_state, gb); saveToFilePreviousGameStatisticsData(filename_game_data_statistics, gb); } } }
loadresource.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #ifndef LOADRESOURCE_H #define LOADRESOURCE_H #include <string> #include <tuple> namespace Game { using load_gameboard_status_t = std ::tuple<bool , struct GameBoard>; namespace Loader { load_gameboard_status_t load_GameBoard_data_from_file (std ::string filename) ; std ::tuple<bool , std ::tuple<unsigned long long , long long >> load_game_stats_from_file(std ::string filename); } } #endif
loadresource.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 #include "loadresource.hpp" #include "global.hpp" #include "gameboard.hpp" #include "tile.hpp" #include <fstream> #include <array> #include <string> #include <sstream> #include <vector> #include <iostream> #include <algorithm> namespace Game{ namespace Loader { namespace { int GetLines (std ::string filename) { std ::ifstream stateFile (filename) ; using iter = std ::istreambuf_iterator<char >; const auto noOfLines = std ::count(iter{ stateFile }, iter{}, '\n' ); return noOfLines; } std ::vector <std ::string > get_file_tile_data (std ::istream& buf) { std ::vector <std ::string > tempbuffer; enum { MAX_WIDTH = 10 , MAX_HEIGHT = 10 }; auto i{ 0 }; for (std ::string tempLine; std ::getline(buf, tempLine) && i < MAX_WIDTH; i++) { std ::istringstream temp_filestream (tempLine) ; auto j{ 0 }; for (std ::string a_word; std ::getline(temp_filestream, a_word, ',' ) && j < MAX_HEIGHT; j++) { tempbuffer.push_back(a_word); } } return tempbuffer; } std ::vector <tile_t > process_file_tile_string_data(std ::vector <std ::string > buf) { std ::vector <tile_t > result_buf; auto tile_processed_counter{ 0 }; const auto prime_tile_data = [&tile_processed_counter](const std ::string tile_data) { enum FieldIndex { IDX_TILE_VALUE, IDX_TILE_BLOCKED, MAX_NO_TILE_IDXS }; std ::array <int , MAX_NO_TILE_IDXS> tile_internal{}; std ::istringstream blocks (tile_data) ; auto idx_id{ 0 }; for (std ::string temptiledata; std ::getline( blocks, temptiledata, ':' ) ; idx_id++) { switch (idx_id) { case IDX_TILE_VALUE: std ::get<IDX_TILE_VALUE>(tile_internal) = std ::stoi(temptiledata); break ; case IDX_TILE_BLOCKED: std ::get<IDX_TILE_BLOCKED>(tile_internal) = std ::stoi(temptiledata); break ; default : std ::cout << "ERROR: [tile_processed_counter: " << tile_processed_counter << "]: Read past MAX_NO_TILE_IDXS! (idx no:" << MAX_NO_TILE_IDXS << ")\n" ; } } tile_processed_counter++; const unsigned long long tile_value = std ::get<IDX_TILE_VALUE>(tile_internal); const bool tile_blocked = std ::get<IDX_TILE_BLOCKED>(tile_internal); return tile_t { tile_value, tile_blocked }; }; std ::transform(std ::begin(buf), std ::end(buf), std ::back_inserter(result_buf), prime_tile_data); return result_buf; } std ::tuple<bool , std ::tuple<ull, long long >> get_and_process_game_stats_string_data(std ::istream& stats_file) { if (stats_file) { ull score{}; long long moveCount{}; for (std ::string tempLine; std ::getline(stats_file, tempLine);) { enum GameStatsFieldIndex { IDX_GAME_SCORE_VALUE, IDX_GAME_MOVECOUNT, MAX_NO_GAME_STATS_IDXS }; std ::istringstream line (tempLine) ; auto idx_id{ 0 }; for (std ::string temp; std ::getline(line, temp, ':' ); idx_id++) { switch (idx_id) { case IDX_GAME_SCORE_VALUE: score = std ::stoi(temp); break ; case IDX_GAME_MOVECOUNT: moveCount = std ::stoi(temp) - 1 ; break ; default : break ; } } } return std ::make_tuple(true , std ::make_tuple(score, moveCount)); } return std ::make_tuple(false , std ::make_tuple(0 , 0 )); } } load_gameboard_status_t load_GameBoard_data_from_file (std ::string filename) { std ::ifstream stateFile (filename) ; if (stateFile) { const ull savedBoardPlaySize = GetLines(filename); const auto file_tile_data = get_file_tile_data(stateFile); const auto processed_tile_data = process_file_tile_string_data(file_tile_data); return std ::make_tuple(true , GameBoard(savedBoardPlaySize, processed_tile_data)); } return std ::make_tuple(false , GameBoard{}); } std ::tuple<bool , std ::tuple<ull, long long >> load_game_stats_from_file(std ::string filename) { std ::ifstream stats (filename) ; return get_and_process_game_stats_string_data(stats); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 #include "menu.hpp" #include "menu-graphics.hpp" #include "game.hpp" #include "global.hpp" #include "game-graphics.hpp" #include <array> #include <iostream> namespace { enum MenuStatusFlag { FLAG_NULL, FLAG_START_GAME, FLAG_CONTINUE_GAME, FLAG_DISPLAY_HIGHSCORES, FLAG_EXIT_GAME, MAX_NO_MAIN_MENU_STATUS_FLAGS }; using menuStatus_t = std ::array <bool , MAX_NO_MAIN_MENU_STATUS_FLAGS>; menuStatus_t menustatus{}; bool flagInputErroneousChoice{ false }; void startGame () { Game::startGame(); } void continueGame () { Game::continueGame(); } void receiveInputFromPlayer (std ::istream& in_os) { flagInputErroneousChoice = bool {}; char c; in_os >> c; switch (c) { case '1' : menustatus[FLAG_START_GAME] = true ; break ; case '2' : menustatus[FLAG_CONTINUE_GAME] = true ; break ; case '3' : menustatus[FLAG_DISPLAY_HIGHSCORES] = true ; break ; case '4' : menustatus[FLAG_EXIT_GAME] = true ; break ; default : flagInputErroneousChoice = true ; break ; } } void processPlayerInput () { if (menustatus[FLAG_START_GAME]) { startGame(); } if (menustatus[FLAG_CONTINUE_GAME]) { continueGame(); } if (menustatus[FLAG_DISPLAY_HIGHSCORES]) { } if (menustatus[FLAG_EXIT_GAME]) { exit (EXIT_SUCCESS); } } bool soloLoop () { menustatus = menuStatus_t{}; clearScreen(); DrawAlways(std ::cout , Game::Graphics::AsciiArt2048); DrawAlways(std ::cout , DataSuppliment(flagInputErroneousChoice, Game::Graphics::Menu::MenuGraphicsOverlay)); receiveInputFromPlayer(std ::cin ); processPlayerInput(); return flagInputErroneousChoice; } void endlessLoop () { while (soloLoop()) ; } } namespace Menu{ void startMenu () { endlessLoop(); } }
game.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #ifndef GAME_H #define GAME_H namespace Game { struct GameBoard ; enum class PlayGameFlag { BrandNewGame, ContinuePreviousGame }; void playGame (PlayGameFlag flag, GameBoard gb, unsigned long long boardSize = 1 ) ; void startGame () ; void continueGame () ; }; #endif
game.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 #include "game.hpp" #include "game-graphics.hpp" #include "game-input.hpp" #include "game-pregame.hpp" #include "gameboard.hpp" #include "gameboard-graphics.hpp" #include "global.hpp" #include "saveresource.hpp" #include "loadresource.hpp" #include "statistics.hpp" #include "scoreboard.hpp" #include <array> #include <chrono> #include <iostream> #include <sstream> namespace Game{ namespace { enum { COMPETITION_GAME_BOARD_SIZE = 4 }; using competition_mode_t = bool ; enum GameStatusFlag { FLAG_WIN, FLAG_END_GAME, FLAG_ONE_SHOT, FLAG_SAVED_GAME, FLAG_INPUT_ERROR, FLAG_ENDLESS_MODE, FLAG_GAME_IS_ASKING_QUESTION_MODE, FLAG_QUESTION_STAY_OR_QUIT, MAX_NO_GAME_STATUS_FLAGS }; using gamestatus_t = std ::array <bool , MAX_NO_GAME_STATUS_FLAGS>; using current_game_session_t = std ::tuple<ull, competition_mode_t , gamestatus_t , GameBoard>; enum tuple_cgs_t_idx { IDX_BESTSCORE, IDX_COMP_MODE, IDX_GAMESTATUS, IDX_GAMEBOARD }; Scoreboard::Score makeFinalscoreFromGameSession (double duration, GameBoard gb) { Scoreboard::Score finalscore{}; finalscore.score = gb.score; finalscore.win = hasWonOnGameboard(gb); finalscore.moveCount = MoveCountOnGameBoard(gb); finalscore.largestTile = gb.largestTile; finalscore.duration = duration; return finalscore; } void doPostGameSaveStuff (Scoreboard::Score finalscore, competition_mode_t cm) { if (cm) { Statistics::createFinalScoreAndEndGameDataFile(std ::cout , std ::cin , finalscore); } } using gamestatus_gameboard_t = std ::tuple<gamestatus_t , GameBoard>; gamestatus_gameboard_t processGameLogic (gamestatus_gameboard_t gsgb) { gamestatus_t gamestatus; GameBoard gb; std ::tie(gamestatus, gb) = gsgb; unblockTilesOnGameboard(gb); if (gb.moved) { addTileOnGameboard(gb); registerMoveByOneOnGameboard(gb); } if (!gamestatus[FLAG_ENDLESS_MODE]) { if (hasWonOnGameboard(gb)) { gamestatus[FLAG_WIN] = true ; gamestatus[FLAG_GAME_IS_ASKING_QUESTION_MODE] = true ; gamestatus[FLAG_QUESTION_STAY_OR_QUIT] = true ; } } if (!canMoveOnGameboard(gb)) { gamestatus[FLAG_END_GAME] = true ; } return std ::make_tuple(gamestatus, gb); } Graphics::scoreboard_display_data_t make_scoreboard_display_data(ull bestScore, competition_mode_t cm, GameBoard gb) { const auto gameboardScore = gb.score; const auto tmpBestScore = (bestScore < gb.score ? gb.score : bestScore); const auto comp_mode = cm; const auto movecount = MoveCountOnGameBoard(gb); const auto scdd = std ::make_tuple(comp_mode, std ::to_string(gameboardScore), std ::to_string(tmpBestScore), std ::to_string(movecount)); return scdd; }; std ::string DisplayGameQuestionsToPlayerPrompt (gamestatus_t gamestatus) { using namespace Graphics; std ::ostringstream str_os; DrawOnlyWhen(str_os, gamestatus[FLAG_QUESTION_STAY_OR_QUIT], QuestionEndOfWinningGamePrompt); return str_os.str(); } Graphics::input_controls_display_data_t make_input_controls_display_data(gamestatus_t gamestatus) { const auto icdd = std ::make_tuple(gamestatus[FLAG_ENDLESS_MODE], gamestatus[FLAG_QUESTION_STAY_OR_QUIT]); return icdd; }; std ::string drawGraphics (current_game_session_t cgs) { using namespace Graphics; using namespace Gameboard::Graphics; using tup_idx = tuple_cgs_t_idx; const auto bestScore = std ::get<tup_idx::IDX_BESTSCORE>(cgs); const auto comp_mode = std ::get<tup_idx::IDX_COMP_MODE>(cgs); const auto gamestatus = std ::get<tup_idx::IDX_GAMESTATUS>(cgs); const auto gb = std ::get<tup_idx::IDX_GAMEBOARD>(cgs); std ::ostringstream str_os; clearScreen(); DrawAlways(str_os, AsciiArt2048); const auto scdd = make_scoreboard_display_data(bestScore, comp_mode, gb); DrawAlways(str_os, DataSuppliment(scdd, GameScoreBoardOverlay)); DrawAlways(str_os, DataSuppliment(gb, GameBoardTextOutput)); DrawOnlyWhen(str_os, gamestatus[FLAG_SAVED_GAME], GameStateNowSavedPrompt); DrawOnlyWhen(str_os, gamestatus[FLAG_GAME_IS_ASKING_QUESTION_MODE], DataSuppliment(gamestatus, DisplayGameQuestionsToPlayerPrompt)); const auto input_controls_display_data = make_input_controls_display_data(gamestatus); DrawAlways(str_os, DataSuppliment(input_controls_display_data, GameInputControlsOverlay)); DrawOnlyWhen(str_os, gamestatus[FLAG_INPUT_ERROR], InvalidInputGameBoardErrorPrompt); return str_os.str(); } gamestatus_t update_one_shot_display_flags (gamestatus_t gamestatus) { const auto disable_one_shot_flag = [](bool & trigger) { trigger = !trigger; }; if (gamestatus[FLAG_ONE_SHOT]) { disable_one_shot_flag(gamestatus[FLAG_ONE_SHOT]); if (gamestatus[FLAG_SAVED_GAME]) { disable_one_shot_flag(gamestatus[FLAG_SAVED_GAME]); } if (gamestatus[FLAG_INPUT_ERROR]) { disable_one_shot_flag(gamestatus[FLAG_INPUT_ERROR]); } } return gamestatus; } using bool_gamestatus_t = std ::tuple<bool , gamestatus_t >; bool_gamestatus_t check_input_other (char c, gamestatus_t gamestatus) { using namespace Input::Keypress::Code; auto is_invalid_keycode{ true }; switch (toupper (c)) { case CODE_HOTKEY_ACTION_SAVE: case CODE_HOTKEY_ALTERNATE_ACTION_SAVE: gamestatus[FLAG_ONE_SHOT] = true ; gamestatus[FLAG_SAVED_GAME] = true ; is_invalid_keycode = false ; break ; case CODE_HOTKEY_QUIT_ENDLESS_MODE: if (gamestatus[FLAG_ENDLESS_MODE]) { gamestatus[FLAG_END_GAME] = true ; is_invalid_keycode = false ; } break ; } return std ::make_tuple(is_invalid_keycode, gamestatus); } using intendedmove_gamestatus_t = std ::tuple<Input::intendedmove_t , gamestatus_t >; intendedmove_gamestatus_t receive_agent_input(Input::intendedmove_t intendedmove, gamestatus_t gamestatus) { using namespace Input; const bool game_still_in_play = !gamestatus[FLAG_END_GAME] && !gamestatus[FLAG_WIN]; if (game_still_in_play) { char c; getKeypressDownInput(c); const auto is_invalid_keypress_code = check_input_ansi(c, intendedmove) && check_input_wasd(c, intendedmove) && check_input_vim(c, intendedmove); bool is_invalid_special_keypress_code{ false }; std ::tie(is_invalid_special_keypress_code, gamestatus) = check_input_other(c, gamestatus); if (is_invalid_keypress_code && is_invalid_special_keypress_code) { gamestatus[FLAG_ONE_SHOT] = true ; gamestatus[FLAG_INPUT_ERROR] = true ; } } return std ::make_tuple(intendedmove, gamestatus); } enum Directions { UP, DOWN, RIGHT, LEFT }; GameBoard decideMove (Directions dir, GameBoard gb) { switch (dir) { case UP: tumbleTilesUpOnGameboard(gb); break ; case DOWN: tumbleTilesDownOnGameboard(gb); break ; case LEFT: tumbleTilesLeftOnGameboard(gb); break ; case RIGHT: tumbleTilesRightOnGameboard(gb); break ; } return gb; } using bool_gameboard_t = std ::tuple<bool , GameBoard>; bool_gameboard_t process_agent_input (Input::intendedmove_t intendedmove, GameBoard gb) { using namespace Input; if (intendedmove[FLAG_MOVE_LEFT]) { gb = decideMove(LEFT, gb); } if (intendedmove[FLAG_MOVE_RIGHT]) { gb = decideMove(RIGHT, gb); } if (intendedmove[FLAG_MOVE_UP]) { gb = decideMove(UP, gb); } if (intendedmove[FLAG_MOVE_DOWN]) { gb = decideMove(DOWN, gb); } return std ::make_tuple(true , gb); } bool check_input_check_to_end_game (char c) { using namespace Input::Keypress::Code; switch (std ::toupper (c)) { case CODE_HOTKEY_CHOICE_NO: return true ; } return false ; } bool continue_playing_game (std ::istream& in_os) { char letter_choice; in_os >> letter_choice; if (check_input_check_to_end_game(letter_choice)) { return false ; } return true ; } bool_gamestatus_t process_gameStatus (gamestatus_gameboard_t gsgb) { gamestatus_t gamestatus; GameBoard gb; std ::tie(gamestatus, gb) = gsgb; auto loop_again{ true }; if (!gamestatus[FLAG_ENDLESS_MODE]) { if (gamestatus[FLAG_WIN]) { if (continue_playing_game(std ::cin )) { gamestatus[FLAG_ENDLESS_MODE] = true ; gamestatus[FLAG_QUESTION_STAY_OR_QUIT] = false ; gamestatus[FLAG_WIN] = false ; } else { loop_again = false ; } } } if (gamestatus[FLAG_END_GAME]) { loop_again = false ; } if (gamestatus[FLAG_SAVED_GAME]) { Saver::saveGamePlayState(gb); } gamestatus[FLAG_GAME_IS_ASKING_QUESTION_MODE] = false ; return std ::make_tuple(loop_again, gamestatus); } std::tuple<bool, current_game_session_t> soloGameLoop(current_game_session_t cgs) { using namespace Input; using tup_idx = tuple_cgs_t_idx; const auto pGamestatus = std ::addressof(std ::get<tup_idx::IDX_GAMESTATUS>(cgs)); const auto pGameboard = std ::addressof(std ::get<tup_idx::IDX_GAMEBOARD>(cgs)); std ::tie(*pGamestatus, *pGameboard) = processGameLogic(std ::make_tuple(*pGamestatus, *pGameboard)); DrawAlways(std ::cout , DataSuppliment(cgs, drawGraphics)); *pGamestatus = update_one_shot_display_flags(*pGamestatus); intendedmove_t player_intendedmove{}; std ::tie(player_intendedmove, *pGamestatus) = receive_agent_input(player_intendedmove, *pGamestatus); std ::tie(std ::ignore, *pGameboard) = process_agent_input(player_intendedmove, *pGameboard); bool loop_again{ false }; std ::tie(loop_again, *pGamestatus) = process_gameStatus(std ::make_tuple(*pGamestatus, *pGameboard)); return std ::make_tuple(loop_again, cgs); } Graphics::end_screen_display_data_t make_end_screen_display_data(gamestatus_t world_gamestatus) { const auto esdd = std ::make_tuple(world_gamestatus[FLAG_WIN], world_gamestatus[FLAG_ENDLESS_MODE]); return esdd; }; std ::string drawEndGameLoopGraphics (current_game_session_t finalgamestatus) { using namespace Graphics; using namespace Gameboard::Graphics; using tup_idx = tuple_cgs_t_idx; const auto bestScore = std ::get<tup_idx::IDX_BESTSCORE>(finalgamestatus); const auto comp_mode = std ::get<tup_idx::IDX_COMP_MODE>(finalgamestatus); const auto gb = std ::get<tup_idx::IDX_GAMEBOARD>(finalgamestatus); const auto end_gamestatus = std ::get<tup_idx::IDX_GAMESTATUS>(finalgamestatus); std ::ostringstream str_os; clearScreen(); DrawAlways(str_os, AsciiArt2048); const auto scdd = make_scoreboard_display_data(bestScore, comp_mode, gb); DrawAlways(str_os, DataSuppliment(scdd, GameScoreBoardOverlay)); DrawAlways(str_os, DataSuppliment(gb, GameBoardTextOutput)); const auto esdd = make_end_screen_display_data(end_gamestatus); DrawAlways(str_os, DataSuppliment(esdd, GameEndScreenOverlay)); return str_os.str(); } GameBoard endlessGameLoop (ull currentBestScore, competition_mode_t cm, GameBoard gb) { auto loop_again{ true }; auto currentgamestatus = std ::make_tuple(currentBestScore, cm, gamestatus_t {}, gb); while (loop_again) { std ::tie(loop_again, currentgamestatus) = soloGameLoop(currentgamestatus); } DrawAlways(std ::cout , DataSuppliment(currentgamestatus, drawEndGameLoopGraphics)); return gb; } } void playGame (PlayGameFlag flag, GameBoard gb, ull boardSize) { const auto isThisNewlyGame = (flag == PlayGameFlag::BrandNewGame); const auto isCompetitionMode = (boardSize == COMPETITION_GAME_BOARD_SIZE); const auto bestScore = Statistics::loadBestScore(); if (isThisNewlyGame) { gb = GameBoard(boardSize); addTileOnGameboard(gb); } const auto startTime = std ::chrono::high_resolution_clock::now(); gb = endlessGameLoop(bestScore, isCompetitionMode, gb); const auto finishTime = std ::chrono::high_resolution_clock::now(); const std ::chrono::duration<double > elapsed = finishTime - startTime; const auto duration = elapsed.count(); if (isThisNewlyGame) { const auto finalscore = makeFinalscoreFromGameSession(duration, gb); doPostGameSaveStuff(finalscore, isCompetitionMode); } } void startGame () { PreGameSetup::SetupNewGame(); } void continueGame () { PreGameSetup::ContinueOldGame(); } }
game-pregame.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #ifndef GAME_PREGAME_H #define GAME_PREGAME_H namespace Game{ namespace PreGameSetup { void SetupNewGame () ; void ContinueOldGame () ; } } #endif
game-pregame.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 #include "game-pregame.hpp" #include "game-graphics.hpp" #include "game-input.hpp" #include "gameboard.hpp" #include "game.hpp" #include "menu.hpp" #include "global.hpp" #include "loadresource.hpp" #include <array> #include <sstream> #include <iostream> #include <limits> namespace Game{ namespace { enum PreGameSetupStatusFlag { FLAG_NULL, FLAG_START_GAME, FLAG_RETURN_TO_MAIN_MENU, MAX_NO_PREGAME_SETUP_STATUS_FLAGS }; using pregameesetup_status_t = std ::array <bool , MAX_NO_PREGAME_SETUP_STATUS_FLAGS>; pregameesetup_status_t pregamesetup_status{}; enum class NewGameFlag { NewGameFlagNull, NoPreviousSaveAvailable }; bool noSave{ false }; bool flagInputErroneousChoice{ false }; ull storedGameBoardSize{ 1 }; int receiveGameBoardSize (std ::istream& is) { int playerInputBoardSize{ 0 }; if (!(is >> playerInputBoardSize)) { constexpr auto INVALID_INPUT_VALUE_FLAG = -1 ; playerInputBoardSize = INVALID_INPUT_VALUE_FLAG; is.clear(); is.ignore(std ::numeric_limits<std ::streamsize>::max(), '\n' ); } return playerInputBoardSize; } void receiveInputFromPlayer (std ::istream& is) { using namespace Input::Keypress::Code; flagInputErroneousChoice = bool { false }; const auto gbsize = receiveGameBoardSize(is); const auto isValidBoardSize = (gbsize >= MIN_GAME_BOARD_PLAY_SIZE) && (gbsize <= MAX_GAME_BOARD_PLAY_SIZE); if (isValidBoardSize) { storedGameBoardSize = gbsize; pregamesetup_status[FLAG_START_GAME] = true ; } bool goBackToMainMenu{ true }; switch (gbsize) { case CODE_HOTKEY_PREGAME_BACK_TO_MENU: pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU] = true ; break ; default : goBackToMainMenu = false ; break ; } if (!isValidBoardSize && !goBackToMainMenu) { flagInputErroneousChoice = true ; } } void processPreGame () { if (pregamesetup_status[FLAG_START_GAME]) { playGame(PlayGameFlag::BrandNewGame, GameBoard{ storedGameBoardSize }, storedGameBoardSize); } if (pregamesetup_status[FLAG_RETURN_TO_MAIN_MENU]) { Menu::startMenu(); } } bool soloLoop () { bool invalidInputValue = flagInputErroneousChoice; const auto questionAboutGameBoardSizePrompt = [&invalidInputValue]() { std ::ostringstream str_os; DrawOnlyWhen(str_os, invalidInputValue, Graphics::BoardSizeErrorPrompt); DrawAlways(str_os, Graphics::BoardInputPrompt); return str_os.str(); }; pregamesetup_status = pregameesetup_status_t {}; clearScreen(); DrawAlways(std ::cout , Game::Graphics::AsciiArt2048); DrawAsOneTimeFlag(std ::cout , noSave, Graphics::GameBoardNoSaveErrorPrompt); DrawAlways(std ::cout , questionAboutGameBoardSizePrompt); receiveInputFromPlayer(std ::cin ); processPreGame(); return flagInputErroneousChoice; } void endlessLoop () { while (soloLoop()) ; } void SetupNewGame (NewGameFlag ns) { noSave = (ns == NewGameFlag::NoPreviousSaveAvailable) ? true : false ; endlessLoop(); } load_gameboard_status_t initialiseContinueBoardArray () { using namespace Loader; constexpr auto gameboard_data_filename = "../data/previousGame.txt" ; constexpr auto game_stats_data_filename = "../data/previousGameStats.txt" ; auto loaded_gameboard{ false }; auto loaded_game_stats{ false }; auto tempGBoard = GameBoard{ 1 }; auto score_and_movecount = std ::tuple<decltype (tempGBoard.score), decltype (tempGBoard.moveCount)>{}; std ::tie(loaded_gameboard, tempGBoard) = load_GameBoard_data_from_file(gameboard_data_filename); std ::tie(loaded_game_stats, score_and_movecount) = load_game_stats_from_file(game_stats_data_filename); std ::tie(tempGBoard.score, tempGBoard.moveCount) = score_and_movecount; const auto all_files_loaded_ok = (loaded_gameboard && loaded_game_stats); return std ::make_tuple(all_files_loaded_ok, tempGBoard); } void DoContinueOldGame () { bool load_old_game_ok{ false }; GameBoard oldGameBoard; std ::tie(load_old_game_ok, oldGameBoard) = initialiseContinueBoardArray(); if (load_old_game_ok) { playGame(PlayGameFlag::ContinuePreviousGame, oldGameBoard); } else { SetupNewGame(NewGameFlag::NoPreviousSaveAvailable); } } } namespace PreGameSetup { void SetupNewGame () { SetupNewGame(NewGameFlag::NewGameFlagNull); } void ContinueOldGame () { DoContinueOldGame(); } } }
阶段七 构建 这一阶段我们实现排行榜系统。
menu.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void processPlayerInput () { if (menustatus[FLAG_START_GAME]) { startGame(); } if (menustatus[FLAG_CONTINUE_GAME]) { continueGame(); } if (menustatus[FLAG_DISPLAY_HIGHSCORES]) { showScores(); } if (menustatus[FLAG_EXIT_GAME]) { exit (EXIT_SUCCESS); } }
(此处强烈建议对照“项目成果展示——排行榜”图片进行代码阅读与理解)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void showScores () { using namespace Game::Graphics; using namespace Scoreboard::Graphics; using namespace Statistics::Graphics; const auto sbddl = make_scoreboard_display_data_list(); const auto tsdd = make_total_stats_display_data(); clearScreen(); DrawAlways(std ::cout , AsciiArt2048); DrawAlways(std ::cout , DataSuppliment(sbddl, ScoreboardOverlay)); DrawAlways(std ::cout , DataSuppliment(tsdd, TotalStatisticsOverlay)); std ::cout << std ::flush; pause_for_keypress(); ::Menu::startMenu(); }
若用户选择查看游戏排行榜,则打印Title Art,历史得分与各项数据统计信息,然后等待用户输入任意键后返回主菜单。
pause_for_keypress()定义于global.cpp中。
1 2 3 4 5 void pause_for_keypress () { char c{}; getKeypressDownInput(c); }
辅助函数make_scoreboard_display_data_list()与make_total_stats_display_data()同样定义于menu.cpp。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 Scoreboard::Graphics::scoreboard_display_data_list_t make_scoreboard_display_data_list() { using namespace Scoreboard::Graphics; auto scoreList = Scoreboard::Scoreboard_t{}; std ::tie(std ::ignore, scoreList) = Scoreboard::loadFromFileScore("../data/scores.txt" ); auto counter{ 1 }; const auto convert_to_display_list_t = [&counter](const Scoreboard::Score s) { const auto data_stats = std ::make_tuple( std ::to_string(counter), s.name, std ::to_string(s.score), s.win ? "Yes" : "No" , std ::to_string(s.moveCount), std ::to_string(s.largestTile), secondsFormat(s.duration)); counter++; return data_stats; }; auto scoreboard_display_list = scoreboard_display_data_list_t {}; std ::transform(std ::begin(scoreList), std ::end(scoreList), std ::back_inserter(scoreboard_display_list), convert_to_display_list_t ); return scoreboard_display_list; }; Statistics::Graphics::total_stats_display_data_t make_total_stats_display_data() { Statistics::total_game_stats_t stats; bool stats_file_loaded{}; std ::tie(stats_file_loaded, stats) = Statistics::loadFromFileStatistics("../data/statistics.txt" ); const auto tsdd = std ::make_tuple( stats_file_loaded, std ::to_string(stats.bestScore), std ::to_string(stats.gameCount), std ::to_string(stats.winCount), std ::to_string(stats.totalMoveCount), secondsFormat(stats.totalDuration)); return tsdd; };
简单,不多说。
ScoreboardOverlay()与TotalStatisticsOverlay()分别定义于scoreboard-graphics.cpp和statistics-graphics.cpp。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 std ::string ScoreboardOverlay (scoreboard_display_data_list_t sbddl) { constexpr auto no_save_text = "No saved scores." ; const auto score_attributes_text = { "No." , "Name" , "Score" , "Won?" , "Moves" , "Largest Tile" , "Duration" }; constexpr auto header_border_text = "┌─────┬────────────────────┬──────────┬──────┬───────┬──────────────┬──────────────┐" ; constexpr auto mid_border_text = "├─────┼────────────────────┼──────────┼──────┼───────┼──────────────┼──────────────┤" ; constexpr auto bottom_border_text = "└─────┴────────────────────┴──────────┴──────┴───────┴──────────────┴──────────────┘" ; constexpr auto score_title_text = "SCOREBOARD" ; constexpr auto divider_text = "──────────" ; constexpr auto sp = " " ; std ::ostringstream str_os; str_os << green << bold_on << sp << score_title_text << bold_off << def << "\n" ; str_os << green << bold_on << sp << divider_text << bold_off << def << "\n" ; const auto number_of_scores = sbddl.size(); if (number_of_scores) { str_os << sp << header_border_text << "\n" ; str_os << std ::left; str_os << sp << "│ " << bold_on << std ::begin(score_attributes_text)[0 ] << bold_off << " │ " << bold_on << std ::setw(18 ) << std ::begin(score_attributes_text)[1 ] << bold_off << " │ " << bold_on << std ::setw(8 ) << std ::begin(score_attributes_text)[2 ] << bold_off << " │ " << bold_on << std ::begin(score_attributes_text)[3 ] << bold_off << " │ " << bold_on << std ::begin(score_attributes_text)[4 ] << bold_off << " │ " << bold_on << std ::begin(score_attributes_text)[5 ] << bold_off << " │ " << bold_on << std ::setw(12 ) << std ::begin(score_attributes_text)[6 ] << bold_off << " │" << "\n" ; str_os << std ::right; str_os << sp << mid_border_text << "\n" ; const auto print_score_stat = [&](const scoreboard_display_data_t data) { str_os << sp << "│ " << std ::setw(2 ) << std ::get<0 >(data) << ". │ " << std ::left << std ::setw(18 ) << std ::get<1 >(data) << std ::right << " │ " << std ::setw(8 ) << std ::get<2 >(data) << " │ " << std ::setw(4 ) << std ::get<3 >(data) << " │ " << std ::setw(5 ) << std ::get<4 >(data) << " │ " << std ::setw(12 ) << std ::get<5 >(data) << " │ " << std ::setw(12 ) << std ::get<6 >(data) << " │" << "\n" ; }; for (const auto s : sbddl) { print_score_stat(s); } str_os << sp << bottom_border_text << "\n" ; } else { str_os << sp << no_save_text << "\n" ; } str_os << "\n\n" ; return str_os.str(); } std ::string TotalStatisticsOverlay (total_stats_display_data_t tsdd) { constexpr auto stats_title_text = "STATISTICS" ; constexpr auto divider_text = "──────────" ; constexpr auto header_border_text = "┌────────────────────┬─────────────┐" ; constexpr auto footer_border_text = "└────────────────────┴─────────────┘" ; const auto stats_attributes_text = { "Best Score" , "Game Count" , "Number of Wins" , "Total Moves Played" , "Total Duration" }; constexpr auto no_save_text = "No saved statistics." ; constexpr auto any_key_exit_text = "Press any key to return to the main menu... " ; constexpr auto sp = " " ; enum TotalStatsDisplayDataFields { IDX_DATA_AVAILABLE, IDX_BEST_SCORE, IDX_GAME_COUNT, IDX_GAME_WIN_COUNT, IDX_TOTAL_MOVE_COUNT, IDX_TOTAL_DURATION, MAX_TOTALSTATSDISPLAYDATA_INDEXES }; std ::ostringstream stats_richtext; const auto stats_file_loaded = std ::get<IDX_DATA_AVAILABLE>(tsdd); if (stats_file_loaded) { constexpr auto num_of_stats_attributes_text = 5 ; auto data_stats = std ::array <std ::string , num_of_stats_attributes_text>{}; data_stats = { std ::get<IDX_BEST_SCORE>(tsdd), std ::get<IDX_GAME_COUNT>(tsdd), std ::get<IDX_GAME_WIN_COUNT>(tsdd), std ::get<IDX_TOTAL_MOVE_COUNT>(tsdd), std ::get<IDX_TOTAL_DURATION>(tsdd) }; auto counter{ 0 }; const auto populate_stats_info = [=, &counter, &stats_richtext](const std ::string ) { stats_richtext << sp << "│ " << bold_on << std ::left << std ::setw(18 ) << std ::begin(stats_attributes_text)[counter] << bold_off << " │ " << std ::right << std ::setw(11 ) << data_stats[counter] << " │" << "\n" ; counter++; }; stats_richtext << green << bold_on << sp << stats_title_text << bold_off << def << "\n" ; stats_richtext << green << bold_on << sp << divider_text << bold_off << def << "\n" ; stats_richtext << sp << header_border_text << "\n" ; for (const auto s : stats_attributes_text) { populate_stats_info(s); } stats_richtext << sp << footer_border_text << "\n" ; } else { stats_richtext << sp << no_save_text << "\n" ; } stats_richtext << "\n\n\n" ; stats_richtext << sp << any_key_exit_text; return stats_richtext.str(); }
两个函数内容参照项目成果预览很容易理解,不赘述。
现在我们解决一下windows环境下必须手动键入回车才能读取输入的问题。
至此,全项目施工完成。
代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 #include "menu.hpp" #include "menu-graphics.hpp" #include "game.hpp" #include "game-graphics.hpp" #include "scoreboard.hpp" #include "scoreboard-graphics.hpp" #include "statistics.hpp" #include "statistics-graphics.hpp" #include "global.hpp" #include "game-graphics.hpp" #include <array> #include <iostream> #include <sstream> #include <algorithm> namespace { enum MenuStatusFlag { FLAG_NULL, FLAG_START_GAME, FLAG_CONTINUE_GAME, FLAG_DISPLAY_HIGHSCORES, FLAG_EXIT_GAME, MAX_NO_MAIN_MENU_STATUS_FLAGS }; using menuStatus_t = std ::array <bool , MAX_NO_MAIN_MENU_STATUS_FLAGS>; menuStatus_t menustatus{}; bool flagInputErroneousChoice{ false }; void startGame () { Game::startGame(); } void continueGame () { Game::continueGame(); } Scoreboard::Graphics::scoreboard_display_data_list_t make_scoreboard_display_data_list() { using namespace Scoreboard::Graphics; auto scoreList = Scoreboard::Scoreboard_t{}; std ::tie(std ::ignore, scoreList) = Scoreboard::loadFromFileScore("../data/scores.txt" ); auto counter{ 1 }; const auto convert_to_display_list_t = [&counter](const Scoreboard::Score s) { const auto data_stats = std ::make_tuple( std ::to_string(counter), s.name, std ::to_string(s.score), s.win ? "Yes" : "No" , std ::to_string(s.moveCount), std ::to_string(s.largestTile), secondsFormat(s.duration)); counter++; return data_stats; }; auto scoreboard_display_list = scoreboard_display_data_list_t {}; std ::transform(std ::begin(scoreList), std ::end(scoreList), std ::back_inserter(scoreboard_display_list), convert_to_display_list_t ); return scoreboard_display_list; }; Statistics::Graphics::total_stats_display_data_t make_total_stats_display_data() { Statistics::total_game_stats_t stats; bool stats_file_loaded{}; std ::tie(stats_file_loaded, stats) = Statistics::loadFromFileStatistics("../data/statistics.txt" ); const auto tsdd = std ::make_tuple( stats_file_loaded, std ::to_string(stats.bestScore), std ::to_string(stats.gameCount), std ::to_string(stats.winCount), std ::to_string(stats.totalMoveCount), secondsFormat(stats.totalDuration)); return tsdd; }; void showScores () { using namespace Game::Graphics; using namespace Scoreboard::Graphics; using namespace Statistics::Graphics; const auto sbddl = make_scoreboard_display_data_list(); const auto tsdd = make_total_stats_display_data(); clearScreen(); DrawAlways(std ::cout , AsciiArt2048); DrawAlways(std ::cout , DataSuppliment(sbddl, ScoreboardOverlay)); DrawAlways(std ::cout , DataSuppliment(tsdd, TotalStatisticsOverlay)); std ::cout << std ::flush; pause_for_keypress(); ::Menu::startMenu(); } void receiveInputFromPlayer (std ::istream& in_os) { flagInputErroneousChoice = bool {}; char c; in_os >> c; switch (c) { case '1' : menustatus[FLAG_START_GAME] = true ; break ; case '2' : menustatus[FLAG_CONTINUE_GAME] = true ; break ; case '3' : menustatus[FLAG_DISPLAY_HIGHSCORES] = true ; break ; case '4' : menustatus[FLAG_EXIT_GAME] = true ; break ; default : flagInputErroneousChoice = true ; break ; } } void processPlayerInput () { if (menustatus[FLAG_START_GAME]) { startGame(); } if (menustatus[FLAG_CONTINUE_GAME]) { continueGame(); } if (menustatus[FLAG_DISPLAY_HIGHSCORES]) { showScores(); } if (menustatus[FLAG_EXIT_GAME]) { exit (EXIT_SUCCESS); } } bool soloLoop () { menustatus = menuStatus_t{}; clearScreen(); DrawAlways(std ::cout , Game::Graphics::AsciiArt2048); DrawAlways(std ::cout , DataSuppliment(flagInputErroneousChoice, Game::Graphics::Menu::MenuGraphicsOverlay)); receiveInputFromPlayer(std ::cin ); processPlayerInput(); return flagInputErroneousChoice; } void endlessLoop () { while (soloLoop()) ; } } namespace Menu{ void startMenu () { endlessLoop(); } }
global.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #ifndef GLOBAL_H #define GLOBAL_H #include <iosfwd> #include <string> using ull = unsigned long long ;template <typename T>void DrawAlways (std ::ostream& os, T f) { os << f(); } template <typename T>void DrawOnlyWhen (std ::ostream& os, bool trigger, T f) { if (trigger) { DrawAlways(os, f); } } template <typename T>void DrawAsOneTimeFlag (std ::ostream& os, bool & trigger, T f) { if (trigger) { DrawAlways(os, f); trigger = !trigger; } } template <typename suppliment_t >struct DataSupplimentInternalType { suppliment_t suppliment_data; template <typename function_t > std ::string operator () (function_t f) const { return f(suppliment_data); } }; template <typename suppliment_t , typename function_t >auto DataSuppliment (suppliment_t needed_data, function_t f) { using dsit_t = DataSupplimentInternalType<suppliment_t >; const auto lambda_f_to_return = [=]() { const dsit_t depinject_func = dsit_t { needed_data }; return depinject_func(f); }; return lambda_f_to_return; } void clearScreen () ;void getKeypressDownInput (char & c) ;void pause_for_keypress () ;std ::string secondsFormat (double sec) ;#endif
global.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 #include "global.hpp" #include <iostream> #include <sstream> void clearScreen () {#ifdef _WIN32 system("cls" ); #else system("clear" ); #endif } #ifdef _WIN32 void getKeypressDownInput (char & c) { std ::cin >> c; } #else # include <termios.h> # include <unistd.h> char getch () { char buf = 0 ; struct termios old = { 0 }; if (tcgetattr(0 , &old) < 0 ) perror("tcsetattr()" ); old.c_lflag &= ~ICANON; old.c_lflag &= ~ECHO; old.c_cc[VMIN] = 1 ; old.c_cc[VTIME] = 0 ; if (tcsetattr(0 , TCSANOW, &old) < 0 ) perror("tcsetattr ICANON" ); if (read(0 , &buf, 1 ) < 0 ) perror("read()" ); old.c_lflag |= ICANON; old.c_lflag |= ECHO; if (tcsetattr(0 , TCSADRAIN, &old) < 0 ) perror("tcsetattr ~ICANON" ); return (buf); } void getKeypressDownInput (char & c) { c = getch(); } #endif void pause_for_keypress () { char c{}; getKeypressDownInput(c); } std ::string secondsFormat (double sec) { double second = sec; int minute = second / 60 ; int hour = minute / 60 ; second -= minute * 60 ; minute %= 60 ; second = static_cast <int >(second); std ::ostringstream oss; if (hour) { oss << hour << "h " ; } if (minute) { oss << minute << "m " ; } oss << second << "s" ; return oss.str(); }
scoreboard-graphics.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #ifndef SCOREBOARD_GRAPHICS_H #define SCOREBOARD_GRAPHICS_H #include <string> #include <tuple> #include <vector> namespace Scoreboard{ namespace Graphics { using scoreboard_display_data_t = std ::tuple<std ::string , std ::string , std ::string , std ::string , std ::string , std ::string , std ::string >; using scoreboard_display_data_list_t = std ::vector <scoreboard_display_data_t >; std ::string ScoreboardOverlay (scoreboard_display_data_list_t sbddl) ; using finalscore_display_data_t = std ::tuple<std ::string , std ::string , std ::string , std ::string >; std ::string EndGameStatisticsPrompt (finalscore_display_data_t finalscore) ; } } #endif
scoreboard-graphics.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 #include "scoreboard-graphics.hpp" #include "global.hpp" #include "color.hpp" #include <sstream> #include <iomanip> // std::setw #include <array> // std::begin namespace Scoreboard{ namespace Graphics { std ::string ScoreboardOverlay (scoreboard_display_data_list_t sbddl) { constexpr auto no_save_text = "No saved scores." ; const auto score_attributes_text = { "No." , "Name" , "Score" , "Won?" , "Moves" , "Largest Tile" , "Duration" }; constexpr auto header_border_text = "┌─────┬────────────────────┬──────────┬──────┬───────┬──────────────┬──────────────┐" ; constexpr auto mid_border_text = "├─────┼────────────────────┼──────────┼──────┼───────┼──────────────┼──────────────┤" ; constexpr auto bottom_border_text = "└─────┴────────────────────┴──────────┴──────┴───────┴──────────────┴──────────────┘" ; constexpr auto score_title_text = "SCOREBOARD" ; constexpr auto divider_text = "──────────" ; constexpr auto sp = " " ; std ::ostringstream str_os; str_os << green << bold_on << sp << score_title_text << bold_off << def << "\n" ; str_os << green << bold_on << sp << divider_text << bold_off << def << "\n" ; const auto number_of_scores = sbddl.size(); if (number_of_scores) { str_os << sp << header_border_text << "\n" ; str_os << std ::left; str_os << sp << "│ " << bold_on << std ::begin(score_attributes_text)[0 ] << bold_off << " │ " << bold_on << std ::setw(18 ) << std ::begin(score_attributes_text)[1 ] << bold_off << " │ " << bold_on << std ::setw(8 ) << std ::begin(score_attributes_text)[2 ] << bold_off << " │ " << bold_on << std ::begin(score_attributes_text)[3 ] << bold_off << " │ " << bold_on << std ::begin(score_attributes_text)[4 ] << bold_off << " │ " << bold_on << std ::begin(score_attributes_text)[5 ] << bold_off << " │ " << bold_on << std ::setw(12 ) << std ::begin(score_attributes_text)[6 ] << bold_off << " │" << "\n" ; str_os << std ::right; str_os << sp << mid_border_text << "\n" ; const auto print_score_stat = [&](const scoreboard_display_data_t data) { str_os << sp << "│ " << std ::setw(2 ) << std ::get<0 >(data) << ". │ " << std ::left << std ::setw(18 ) << std ::get<1 >(data) << std ::right << " │ " << std ::setw(8 ) << std ::get<2 >(data) << " │ " << std ::setw(4 ) << std ::get<3 >(data) << " │ " << std ::setw(5 ) << std ::get<4 >(data) << " │ " << std ::setw(12 ) << std ::get<5 >(data) << " │ " << std ::setw(12 ) << std ::get<6 >(data) << " │" << "\n" ; }; for (const auto s : sbddl) { print_score_stat(s); } str_os << sp << bottom_border_text << "\n" ; } else { str_os << sp << no_save_text << "\n" ; } str_os << "\n\n" ; return str_os.str(); } std ::string EndGameStatisticsPrompt (finalscore_display_data_t finalscore) { std ::ostringstream str_os; constexpr auto stats_title_text = "STATISTICS" ; constexpr auto divider_text = "──────────" ; constexpr auto sp = " " ; const auto stats_attributes_text = { "Final score:" , "Largest Tile:" , "Number of moves:" , "Time taken:" }; enum FinalScoreDisplayDataFields { IDX_FINAL_SCORE_VALUE, IDX_LARGEST_TILE, IDX_MOVE_COUNT, IDX_DURATION, MAX_NUM_OF_FINALSCOREDISPLAYDATA_INDEXES }; const auto data_stats = std ::array <std ::string , MAX_NUM_OF_FINALSCOREDISPLAYDATA_INDEXES>{ std ::get<IDX_FINAL_SCORE_VALUE>(finalscore), std ::get<IDX_LARGEST_TILE>(finalscore), std ::get<IDX_MOVE_COUNT>(finalscore), std ::get<IDX_DURATION>(finalscore) }; std ::ostringstream stats_richtext; stats_richtext << yellow << sp << stats_title_text << def << "\n" ; stats_richtext << yellow << sp << divider_text << def << "\n" ; auto counter{ 0 }; const auto populate_stats_info = [=, &counter, &stats_richtext](const std ::string ) { stats_richtext << sp << std ::left << std ::setw(19 ) << std ::begin(stats_attributes_text)[counter] << bold_on << std ::begin(data_stats)[counter] << bold_off << "\n" ; counter++; }; for (const auto s : stats_attributes_text) { populate_stats_info(s); } str_os << stats_richtext.str(); str_os << "\n\n" ; return str_os.str(); } } }
statistics-graphics.hpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #ifndef STATISTICSGRAPHICS_H #define STATISTICSGRAPHICS_H #include <string> #include <tuple> namespace Statistics { namespace Graphics { std ::string AskForPlayerNamePrompt () ; std ::string MessageScoreSavedPrompt () ; using total_stats_display_data_t = std ::tuple<bool , std ::string , std ::string , std ::string , std ::string , std ::string >; std ::string TotalStatisticsOverlay (total_stats_display_data_t tsdd) ; } } #endif
statistics-graphics.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 #include "statistics.hpp" #include "statistics-graphics.hpp" #include "color.hpp" #include <sstream> #include <iomanip> #include <array> namespace Statistics{ namespace Graphics { std ::string AskForPlayerNamePrompt () { constexpr auto score_prompt_text = "Please enter your name to save this score: " ; constexpr auto sp = " " ; std ::ostringstream score_prompt_richtext; score_prompt_richtext << bold_on << sp << score_prompt_text << bold_off; return score_prompt_richtext.str(); } std ::string MessageScoreSavedPrompt () { constexpr auto score_saved_text = "Score saved!" ; constexpr auto sp = " " ; std ::ostringstream score_saved_richtext; score_saved_richtext << "\n" << green << bold_on << sp << score_saved_text << bold_off << def << "\n" ; return score_saved_richtext.str(); } std ::string TotalStatisticsOverlay (total_stats_display_data_t tsdd) { constexpr auto stats_title_text = "STATISTICS" ; constexpr auto divider_text = "──────────" ; constexpr auto header_border_text = "┌────────────────────┬─────────────┐" ; constexpr auto footer_border_text = "└────────────────────┴─────────────┘" ; const auto stats_attributes_text = { "Best Score" , "Game Count" , "Number of Wins" , "Total Moves Played" , "Total Duration" }; constexpr auto no_save_text = "No saved statistics." ; constexpr auto any_key_exit_text = "Press any key to return to the main menu... " ; constexpr auto sp = " " ; enum TotalStatsDisplayDataFields { IDX_DATA_AVAILABLE, IDX_BEST_SCORE, IDX_GAME_COUNT, IDX_GAME_WIN_COUNT, IDX_TOTAL_MOVE_COUNT, IDX_TOTAL_DURATION, MAX_TOTALSTATSDISPLAYDATA_INDEXES }; std ::ostringstream stats_richtext; const auto stats_file_loaded = std ::get<IDX_DATA_AVAILABLE>(tsdd); if (stats_file_loaded) { constexpr auto num_of_stats_attributes_text = 5 ; auto data_stats = std ::array <std ::string , num_of_stats_attributes_text>{}; data_stats = { std ::get<IDX_BEST_SCORE>(tsdd), std ::get<IDX_GAME_COUNT>(tsdd), std ::get<IDX_GAME_WIN_COUNT>(tsdd), std ::get<IDX_TOTAL_MOVE_COUNT>(tsdd), std ::get<IDX_TOTAL_DURATION>(tsdd) }; auto counter{ 0 }; const auto populate_stats_info = [=, &counter, &stats_richtext](const std ::string ) { stats_richtext << sp << "│ " << bold_on << std ::left << std ::setw(18 ) << std ::begin(stats_attributes_text)[counter] << bold_off << " │ " << std ::right << std ::setw(11 ) << data_stats[counter] << " │" << "\n" ; counter++; }; stats_richtext << green << bold_on << sp << stats_title_text << bold_off << def << "\n" ; stats_richtext << green << bold_on << sp << divider_text << bold_off << def << "\n" ; stats_richtext << sp << header_border_text << "\n" ; for (const auto s : stats_attributes_text) { populate_stats_info(s); } stats_richtext << sp << footer_border_text << "\n" ; } else { stats_richtext << sp << no_save_text << "\n" ; } stats_richtext << "\n\n\n" ; stats_richtext << sp << any_key_exit_text; return stats_richtext.str(); } } }
后记 windows环境下必须回车才能读入输入的问题尝试解决了一下,但没解决掉。。。懒得管了。
这东西在windows的黑终端玩起来又丑又闪,直接在linux环境下玩就好啦~
完结撒花,累死了。
2022/2/14