diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index ca3a2668..e3be9296 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -71,7 +71,7 @@ jobs: submodules: recursive - name: Configure CMake - run: cmake -G "MinGW Makefiles" -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DWITH_GCOV=OFF -DWITH_ASAN=OFF -DWITH_GLOG=OFF -DWITH_SQLITE=ON -DWITH_TEST=ON -DWITH_SIMULATOR=ON -DWITH_GAMES=ON + run: cmake -G "MinGW Makefiles" -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DWITH_GCOV=OFF -DWITH_ASAN=OFF -DWITH_GLOG=OFF -DWITH_SQLITE=ON -DWITH_TEST=ON -DWITH_SIMULATOR=ON -DWITH_GAMES=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5 - name: Build working-directory: build diff --git a/bot_core/bot_ctx.cc b/bot_core/bot_ctx.cc index 93033993..37766475 100644 --- a/bot_core/bot_ctx.cc +++ b/bot_core/bot_ctx.cc @@ -124,16 +124,24 @@ static std::variant LoadGameModules(const char* cons return game_handles; } #ifdef _WIN32 - WIN32_FIND_DATA file_data; - HANDLE file_handle = FindFirstFile((std::string(games_path) + "\\*.dll").c_str(), &file_data); - if (file_handle == INVALID_HANDLE_VALUE) { - return "LoadGameModules: find first file failed"; + WIN32_FIND_DATA dir_data; + HANDLE dir_handle = FindFirstFile((std::string(games_path) + "\\*").c_str(), &dir_data); + if (dir_handle == INVALID_HANDLE_VALUE) { + return "LoadGameModules: open directory failed"; } do { - const auto dll_path = std::string(games_path) + "\\" + file_data.cFileName; - LoadGame(LoadLibrary(dll_path.c_str()), game_handles); - } while (FindNextFile(file_handle, &file_data)); - FindClose(file_handle); + if ((dir_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 || strcmp(dir_data.cFileName, ".") == 0 || strcmp(dir_data.cFileName, "..") == 0) { + WarnLog() << "Not the game directory, skip: " << dir_data.cFileName; + continue; + } + std::string dll_path = std::string(games_path) + "\\" + dir_data.cFileName + "\\libgame.dll"; + if (GetFileAttributes(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES) { + WarnLog() << "Cannot find libgame.dll, skip: " << dir_data.cFileName; + } else { + LoadGame(LoadLibrary(dll_path.c_str()), game_handles); + } + } while (FindNextFile(dir_handle, &dir_data)); + FindClose(dir_handle); InfoLog() << "Load module count: " << game_handles.size(); #elif __linux__ DIR* d = opendir(games_path); diff --git a/bot_core/db_manager.cc b/bot_core/db_manager.cc index a6352729..33c95c5a 100644 --- a/bot_core/db_manager.cc +++ b/bot_core/db_manager.cc @@ -52,7 +52,7 @@ uint64_t InsertMatch(sqlite::database& db, const std::string& game_name, const s { db << "INSERT INTO match (game_name, finish_time, group_id, host_user_id, user_count, multiple) VALUES (?,datetime(CURRENT_TIMESTAMP, \'localtime\'),?,?,?,?);" << game_name - << gid + << (gid ? std::optional{gid->GetStr()} : std::nullopt) << host_uid.GetStr() << user_count << multiple; diff --git a/bot_core/image.h b/bot_core/image.h index 1f3ca1e8..54e97556 100644 --- a/bot_core/image.h +++ b/bot_core/image.h @@ -14,9 +14,9 @@ #include "utility/log.h" #ifdef TEST_BOT -static bool enable_markdown_to_image = false; +inline bool enable_markdown_to_image = false; #else -static bool enable_markdown_to_image = true; +inline bool enable_markdown_to_image = true; #endif inline const std::string k_markdown2image_path = (std::filesystem::current_path() / "markdown2image").string(); // TODO: config diff --git a/bot_core/message_handlers.cc b/bot_core/message_handlers.cc index b11330ef..3dbcc67a 100644 --- a/bot_core/message_handlers.cc +++ b/bot_core/message_handlers.cc @@ -188,7 +188,7 @@ static ErrCode show_gamelist(BotCtx& bot, const UserID uid, const std::optional< table.Get(table.Row() - 2, 4).SetContent(""); - table.Get(table.Row() - 1, 1).SetContent(std::string(" ") + game_handle.Info().description_ + ""); + table.Get(table.Row() - 1, 1).SetStyle("style=\"width:470px;\"").SetContent(std::string(" ") + game_handle.Info().description_ + ""); } send_image(); } @@ -666,7 +666,7 @@ static ErrCode show_profile(BotCtx& bot, const UserID uid, const std::optionalsecond.Info().achievements_) { if (name == info.achievement_name_) { - recent_honors_table.GetLastRow(3).SetContent(description); + recent_honors_table.GetLastRow(3).SetStyle("style=\"width:440px;\"").SetContent(description); break; } } diff --git a/bot_core/test_bot.cc b/bot_core/test_bot.cc index 6b47c775..dfab0800 100644 --- a/bot_core/test_bot.cc +++ b/bot_core/test_bot.cc @@ -171,7 +171,7 @@ namespace game { namespace GAME_MODULE_NAME { -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { return true; @@ -180,9 +180,9 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener template constexpr T ValueFunc() { return V; } -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 4; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 4; } -uint32_t Multiple(const MyGameOptions& options) { return 1; } +uint32_t Multiple(const CustomOptions& options) { return 1; } class MainStage; template using SubGameStage = StageFsm; diff --git a/game_framework/game_options.h b/game_framework/game_options.h index ebb1f5bd..8da960c4 100644 --- a/game_framework/game_options.h +++ b/game_framework/game_options.h @@ -32,13 +32,13 @@ namespace game { namespace GAME_MODULE_NAME { -#define OPTION_CLASSNAME MyGameOptions +#define OPTION_CLASSNAME CustomOptions #define OPTION_FILENAME GAME_OPTION_FILENAME #include "utility/extend_option.h" #undef OPTION_CLASSNAME #undef OPTION_FILENAME -class GameOptions : public GameOptionsBase, public MyGameOptions +class GameOptions : public GameOptionsBase, public CustomOptions { public: GameOptions() = default; @@ -48,13 +48,13 @@ class GameOptions : public GameOptionsBase, public MyGameOptions virtual bool SetOption(const char* const msg) override { MsgReader msg_reader(msg); - return MyGameOptions::SetOption(msg_reader); + return CustomOptions::SetOption(msg_reader); } virtual const char* Info(const bool with_example, const bool with_html_syntax, const char* const prefix) const override { thread_local static std::string info; - return (info = MyGameOptions::Info(with_example, with_html_syntax, prefix)).c_str(); + return (info = CustomOptions::Info(with_example, with_html_syntax, prefix)).c_str(); } virtual const char* const* ShortInfo() const override diff --git a/game_framework/stage.h b/game_framework/stage.h index c34c50d4..17f073b1 100644 --- a/game_framework/stage.h +++ b/game_framework/stage.h @@ -297,7 +297,7 @@ class VariantSubStage::Impl : public VariantSubStage::Base class MainStageFactory { public: - MainStageFactory(const MyGameOptions& game_options, const GenericOptions& generic_options, MatchBase& match) + MainStageFactory(const CustomOptions& game_options, const GenericOptions& generic_options, MatchBase& match) : game_options_(std::move(game_options)), generic_options_(generic_options), match_(match) {} // This function can only be invoked once. @@ -307,11 +307,11 @@ class MainStageFactory return new internal::MainStage(std::make_unique(StageUtility{game_options_, generic_options_, match_})); } - const MyGameOptions& GetGameOptions() const { return game_options_; } + const CustomOptions& GetGameOptions() const { return game_options_; } const GenericOptions& GetGenericOptions() const { return generic_options_; } private: - const MyGameOptions& game_options_; + const CustomOptions& game_options_; const GenericOptions& generic_options_; MatchBase& match_; }; diff --git a/game_framework/stage_utility.cc b/game_framework/stage_utility.cc index ba0d7bcd..1387aa90 100644 --- a/game_framework/stage_utility.cc +++ b/game_framework/stage_utility.cc @@ -18,7 +18,7 @@ namespace GAME_MODULE_NAME { namespace internal { -PublicStageUtility::PublicStageUtility(const MyGameOptions& game_options, const lgtbot::game::GenericOptions& generic_options, MatchBase& match) +PublicStageUtility::PublicStageUtility(const CustomOptions& game_options, const lgtbot::game::GenericOptions& generic_options, MatchBase& match) : game_options_{game_options} , generic_options_(generic_options) , match_(match) @@ -103,7 +103,7 @@ void PublicStageUtility::Leave(const PlayerID pid) { masker_.SetPermanentInactive(pid); if (IsInDeduction()) { - Boardcast() << "所有玩家都失去了行动能力,于是游戏将直接推演至终局"; + match_.GroupMsgSender()() << "所有玩家都失去了行动能力,于是游戏将直接推演至终局"; } } diff --git a/game_framework/stage_utility.h b/game_framework/stage_utility.h index e2ac185a..af2ccd00 100644 --- a/game_framework/stage_utility.h +++ b/game_framework/stage_utility.h @@ -34,7 +34,7 @@ class PublicStageUtility using AchievementCounts = std::array; public: - PublicStageUtility(const MyGameOptions& game_options, const lgtbot::game::GenericOptions& generic_options, MatchBase& match); + PublicStageUtility(const CustomOptions& game_options, const lgtbot::game::GenericOptions& generic_options, MatchBase& match); PublicStageUtility(PublicStageUtility&&) = default; // Message functions @@ -81,7 +81,7 @@ class PublicStageUtility // Options - const MyGameOptions& Options() const { return game_options_; } + const CustomOptions& Options() const { return game_options_; } auto PlayerNum() const { return generic_options_.PlayerNum(); } const char* ResourceDir() const { return generic_options_.resource_dir_; } @@ -102,7 +102,7 @@ class PublicStageUtility static void TimerCallbackPublic_(void* const p, const uint64_t alert_sec); static void TimerCallbackPrivate_(void* const p, const uint64_t alert_sec); - const MyGameOptions& game_options_; + const CustomOptions& game_options_; const GenericOptions& generic_options_; MatchBase& match_; PlayerReadyMasker masker_; diff --git a/game_framework/util.h b/game_framework/util.h index de7b56a5..4810d7b3 100644 --- a/game_framework/util.h +++ b/game_framework/util.h @@ -19,7 +19,7 @@ class GenericOptions; namespace GAME_MODULE_NAME { -class MyGameOptions; +class CustomOptions; // ===================================== // Developer-defined constance @@ -40,7 +40,7 @@ enum class NewGameMode { SINGLE_USER, // start game immediately MULTIPLE_USERS, // wait other users to join }; -using InitOptionsCommand = Command; // command to initialize options +using InitOptionsCommand = Command; // command to initialize options extern const std::vector k_init_options_commands; // ===================================== @@ -50,15 +50,15 @@ extern const std::vector k_init_options_commands; // Validate the options and adapt them if not valid. // The return value of false indicates the options are not valid and fail to adapt. In this scenario, the game will fail // to start. -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options); +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options); // Get the maximum player numbers under the current options. // The return value of 0 indicates there are no player number limits. -uint64_t MaxPlayerNum(const MyGameOptions& options); +uint64_t MaxPlayerNum(const CustomOptions& options); // Get the score multiple under the current options. // The value of 0 indicates it is a match for practice. -uint32_t Multiple(const MyGameOptions& options); +uint32_t Multiple(const CustomOptions& options); } // namespace GAME_MODULE_NAME diff --git a/game_template/mygame.cc b/game_template/mygame.cc index b8dd7dcd..9de3f844 100644 --- a/game_template/mygame.cc +++ b/game_template/mygame.cc @@ -16,32 +16,48 @@ class MainStage; template using SubGameStage = StageFsm; template using MainGameStage = StageFsm; -// 0 indicates no max-player limits -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } +const GameProperties k_properties { + // The game name which should be unique among all the games. + .name_ = "测试游戏", -// The default score multiple for the game. The value of 0 denotes a testing game. -// We recommend to increase the multiple by one for every 7~8 minutes the game lasts. -uint32_t Multiple(const MyGameOptions& options) { return 0; } + // The game developer which can be shown in the game list image. + .developer_ = "佚名", -// The game name which should be unique among all the games. -const std::string k_game_name = "测试游戏"; + // The game description which can be shown in the game list image. + .description_ = "暂无游戏描述", -// The game developer which can be shown in the game list image. -const std::string k_developer = "佚名"; + // The true value indicates each user may be assigned with different player IDs in different matches. + .shuffled_player_id_ = false, +}; -// The game description which can be shown in the game list image. -const std::string k_description = "暂无游戏描述"; +// 0 indicates no max-player limits +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } + +// The default score multiple for the game. The value of 0 denotes a testing game. +// We recommend to increase the multiple by one for every 7~8 minutes the game lasts. +uint32_t Multiple(const CustomOptions& options) { return 0; } // The default generic options. const MutableGenericOptions k_default_generic_options; +// The function is invoked before a game starts. You can make final adaption for the options. +// The return value of false denotes failure to start a game. +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +{ + if (generic_options_readonly.PlayerNum() < 3) { + reply() << "该游戏至少 3 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); + return false; + } + return true; +} + // The commands for showing more rules information. Users can get the information by "#规则 ...". const std::vector k_rule_commands = {}; // The commands for initialize the options when starting a game by "#新游戏 ..." const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { // Set the target player numbers when an user start the game with the "单机" argument. // It is ok to make `k_init_options_commands` empty. @@ -51,17 +67,6 @@ const std::vector k_init_options_commands = { VoidChecker("单机")), }; -// The function is invoked before a game starts. You can make final adaption for the options. -// The return value of false denotes failure to start a game. -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) -{ - if (generic_options_readonly.PlayerNum() < 3) { - reply() << "该游戏至少 3 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); - return false; - } - return true; -} - // ========== GAME STAGES ========== class RoundStage; diff --git a/game_util/mahjong_util.h b/game_util/mahjong_util.h index eeca9528..2c0f6fb8 100644 --- a/game_util/mahjong_util.h +++ b/game_util/mahjong_util.h @@ -242,7 +242,7 @@ static std::string DoraHtml(const std::string& image_path, const std::span"; } diff --git a/games/CMakeLists.txt b/games/CMakeLists.txt index 3b9d0b44..ac03f26d 100644 --- a/games/CMakeLists.txt +++ b/games/CMakeLists.txt @@ -87,7 +87,9 @@ foreach (GAME_DIR ${GAME_DIRS}) target_include_directories(${GAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/${GAME}) target_link_libraries(${GAME} ${GAME_THIRD_PARTIES}) set_target_properties(${GAME} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${GAME_OUTPUT_PATH} LIBRARY_OUTPUT_DIRECTORY ${GAME_OUTPUT_PATH} + ARCHIVE_OUTPUT_DIRECTORY ${GAME_OUTPUT_PATH} OUTPUT_NAME "libgame" PREFIX "") diff --git a/games/alchemist/mygame.cc b/games/alchemist/mygame.cc index 7da0fad3..eca5636d 100644 --- a/games/alchemist/mygame.cc +++ b/games/alchemist/mygame.cc @@ -31,8 +31,8 @@ const GameProperties k_properties { .description_ = "通过放置卡牌,让卡牌连成直线获得积分,比拼分数高低的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return GET_OPTION_VALUE(options, 种子).empty() ? std::min(2U, GET_OPTION_VALUE(options, 回合数) / 10) : 0; } @@ -41,7 +41,7 @@ const std::vector k_rule_commands = {}; static int WinScoreThreshold(const bool mode) { return mode ? 200 : 10; } -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { const auto card_num = GET_OPTION_VALUE(game_options, 颜色) * GET_OPTION_VALUE(game_options, 点数) * GET_OPTION_VALUE(game_options, 副数); if (GET_OPTION_VALUE(game_options, 回合数) > card_num && GET_OPTION_VALUE(game_options, 副数) > 0) { @@ -53,7 +53,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 1; return NewGameMode::SINGLE_USER; diff --git a/games/alter_rps/mygame.cc b/games/alter_rps/mygame.cc index 433be700..bf38adb3 100644 --- a/games/alter_rps/mygame.cc +++ b/games/alter_rps/mygame.cc @@ -29,12 +29,12 @@ const GameProperties k_properties { .developer_ = "森高", .description_ = "伸出两拳,收回一拳的猜拳游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) { return 1; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return 1; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为" << generic_options_readonly.PlayerNum(); @@ -45,7 +45,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; @@ -95,7 +95,7 @@ static std::string StarsString(const uint32_t n) { using CardMap = std::map; -static CardMap GetCardMap(const MyGameOptions& option) +static CardMap GetCardMap(const CustomOptions& option) { CardMap cards; const auto emplace_card = [&](const auto& card) { diff --git a/games/avalon/mygame.cc b/games/avalon/mygame.cc index af8c6389..7d41f372 100644 --- a/games/avalon/mygame.cc +++ b/games/avalon/mygame.cc @@ -43,9 +43,9 @@ template using SubGameStage = StageFsm using MainGameStage = StageFsm; // 0 indicates no max-player limits -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 10; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 10; } -uint32_t Multiple(const MyGameOptions& options) { return 0; } +uint32_t Multiple(const CustomOptions& options) { return 0; } const GameProperties k_properties { .name_ = "阿瓦隆", @@ -63,7 +63,7 @@ const std::vector k_rule_commands = {}; // The commands for initialize the options when starting a game by "#新游戏 ..." const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { // Set the target player numbers when an user start the game with the "单机" argument. // It is ok to make `k_init_options_commands` empty. @@ -72,7 +72,7 @@ const std::vector k_init_options_commands = { }, VoidChecker("单机")), InitOptionsCommand("不启用任何扩展包", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { GET_OPTION_VALUE(game_options, 兰斯洛特模式) = LancelotMode::disable; GET_OPTION_VALUE(game_options, 王者之剑) = false; @@ -83,7 +83,7 @@ const std::vector k_init_options_commands = { // The function is invoked before a game starts. You can make final adaption for the options. // The return value of false denotes failure to start a game. -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 5) { reply() << "该游戏至少 5 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); diff --git a/games/beast_trap_chess/achievements.h b/games/beast_trap_chess/achievements.h new file mode 100644 index 00000000..e69de29b diff --git a/games/beast_trap_chess/board.h b/games/beast_trap_chess/board.h new file mode 100644 index 00000000..17086954 --- /dev/null +++ b/games/beast_trap_chess/board.h @@ -0,0 +1,425 @@ + +enum class Direct { UP = 0, DOWN = 1, LEFT = 2, RIGHT = 3 }; + +const map direction_map = { + {"上", Direct::UP}, {"U", Direct::UP}, {"s", Direct::UP}, + {"下", Direct::DOWN}, {"D", Direct::DOWN}, {"x", Direct::DOWN}, + {"左", Direct::LEFT}, {"L", Direct::LEFT}, {"z", Direct::LEFT}, + {"右", Direct::RIGHT}, {"R", Direct::RIGHT}, {"y", Direct::RIGHT}, +}; + +class Player +{ + public: + Player(const string &name, const string &avatar) : name(name), avatar(avatar) {} + + void SetPos(const pair pos) { + this->x = pos.first; + this->y = pos.second; + } + + const string name; + const string avatar; + int x, y; +}; + + +class Grid +{ + public: + template + void SetWall(bool has_wall) { wall[static_cast(direct)] = has_wall; } + + template + bool Wall() const { return wall[static_cast(direct)]; } + + private: + // 四周墙面(上/下/左/右) + bool wall[4] = {false, false, false, false}; +}; + + +class Board +{ + public: + // 地图大小 + int size = 5; + // 玩家 + vector players; + // 地图 + vector> grid_map; + + // 初始化地图 + void Initialize() + { + players[0].SetPos({0, size - 1}); + players[1].SetPos({size - 1, 0}); + grid_map.resize(size); + for (int i = 0; i < size; i++) { + grid_map[i].resize(size); + } + InitializeBoundary(); // 初始化边界 + } + + // 获取玩家和地图的markdown字符串 + string GetMarkdown(const int round, const PlayerID currentPlayer) const + { + return GetPlayerTable(round, currentPlayer) + GetBoard(); + } + + // 获取地图html + string GetBoard() const + { + html::Table map(size * 2 + 3, size * 2 + 3); + map.SetTableStyle("align=\"center\" cellpadding=\"0\" cellspacing=\"0\" style=\"border-collapse: collapse;\""); + // 坐标 + for (int i = 2; i < size * 2 + 2; i = i + 2) { + int n = i / 2 - 1; + map.Get(i, 0).SetStyle("class=\"pos\"").SetContent(char('A' + n)); + map.Get(i, size * 2 + 2).SetStyle("class=\"pos\"").SetContent(char('A' + n)); + map.Get(0, i).SetStyle("class=\"pos\"").SetContent(to_string(n + 1)); + map.Get(size * 2 + 2, i).SetStyle("class=\"pos\"").SetContent(to_string(n + 1)); + } + // 方格和玩家 + for (int x = 2; x < size * 2 + 1; x = x + 2) { + for (int y = 2; y < size * 2 + 1; y = y + 2) { + int gridX = x/2-1; + int gridY = y/2-1; + map.Get(x, y).SetStyle("class=\"grid\""); + for (int pid = 0; pid < 2; pid++) { + if (players[pid].x == gridX && players[pid].y == gridY) { + if (players[pid].name == "机器人0号") { + map.Get(x, y).SetContent("电脑"); + } else { + map.Get(x, y).SetContent(players[pid].avatar); + } + } + } + } + } + // 纵向围墙 + for (int x = 2; x < size * 2 + 1; x = x + 2) { + for (int y = 1; y < size * 2; y = y + 2) { + if (grid_map[x/2-1][y/2].Wall()) { + map.Get(x, y).SetStyle("class=\"wall-col\"").SetColor("black"); + } + } + if (grid_map[x/2-1][size-1].Wall()) { + map.Get(x, size*2+1).SetStyle("class=\"wall-col\"").SetColor("black"); + } + } + // 横向围墙 + for (int y = 2; y < size * 2 + 1; y = y + 2) { + for (int x = 1; x < size * 2; x = x + 2) { + if (grid_map[x/2][y/2-1].Wall()) { + map.Get(x, y).SetStyle("class=\"wall-row\"").SetColor("black"); + } + } + if (grid_map[size-1][y/2-1].Wall()) { + map.Get(size*2+1, y).SetStyle("class=\"wall-row\"").SetColor("black"); + } + } + // 角落方块 + for (int x = 1; x < size * 2 + 2; x = x + 2) { + for (int y = 1; y < size * 2 + 2; y = y + 2) { + map.Get(x, y).SetStyle("class=\"corner\"").SetColor("black"); + } + } + return style + map.ToString(); + } + + // 获取玩家信息 + string GetPlayerTable(const int round, const PlayerID currentPlayer) const + { + html::Table playerTable(2, 3); + playerTable.SetTableStyle("align=\"center\" cellpadding=\"2\""); + if (currentPlayer == 0) playerTable.Get(0, 1).SetColor("gainsboro"); + if (currentPlayer == 1) playerTable.Get(1, 1).SetColor("gainsboro"); + for (int pid = 0; pid < 2; pid++) { + playerTable.Get(pid, 0).SetStyle("style=\"width:40px;\"").SetContent(players[pid].avatar); + playerTable.Get(pid, 1).SetStyle("style=\"width:300px; text-align:left;\"").SetContent(players[pid].name); + playerTable.Get(pid, 2).SetStyle("style=\"width:40px;\""); + if (currentPlayer == 2) { + playerTable.Get(pid, 2).SetColor("gainsboro").SetContent(to_string(pid == 0 ? GetAreaSize().first : GetAreaSize().second)); + } + } + return "### 第 " + to_string(round) + " 回合" + playerTable.ToString(); + } + + // 检查坐标是否合法 + static string CheckCoordinate(string &s) + { + // 长度必须为2 + if (s.length() != 2 && s.length() != 3) { + return "[错误] 输入的坐标长度只能为 2 或 3,如:A1"; + } + // 大小写不敏感 + if (s[0] <= 'z' && s[0] >= 'a') { + s[0] = s[0] - 'a' + 'A'; + } + // 检查是否为合法输入 + if (s[0] > 'Z' || s[0] < 'A' || s[1] > '9' || s[1] < '0') { + return "[错误] 请输入合法的坐标(字母+数字),如:A1"; + } + if (s.length() == 3 && (s[2] > '9' || s[2] < '0')) { + return "[错误] 请输入合法的坐标(字母+数字),如:A1"; + } + return "OK"; + } + + // 将字符串转为一个位置pair。必须确保字符串是合法的再执行这个操作。 + static pair TranString(string s) + { + int nowX = s[0] - 'A', nowY = s[1] - '0' - 1; + if (s.length() == 3) + { + nowY = (s[1] - '0') * 10 + s[2] - '0' - 1; + } + pair ret; + ret.first = nowX; + ret.second = nowY; + return ret; + } + + // 检查移动(返回所需要的步数,无法抵达返回-1) + int CheckMoveStep(const PlayerID pid, const pair target) const + { + int sx = players[pid].x, sy = players[pid].y; + int ex = target.first, ey = target.second; + const int dx[4] = {-1, 1, 0, 0}; + const int dy[4] = {0, 0, -1, 1}; + + // 某格是否有玩家 + auto hasPlayer = [&](int x, int y) { + for (int i = 0; i < 2; i++) { + if (i != pid && players[i].x == x && players[i].y == y) return true; + } + return false; + }; + + vector> vis(size, vector(size, false)); + queue> q; + q.emplace(sx, sy, 0); + vis[sx][sy] = true; + + auto mod = [&](int v) { + return (v + size) % size; + }; + + while (!q.empty()) { + auto [x, y, d] = q.front(); q.pop(); + if (x == ex && y == ey) return d; + + for (int dir = 0; dir < 4; dir++) { + int nx = mod(x + dx[dir]); + int ny = mod(y + dy[dir]); + if (!IsConnected(x, y, dx[dir], dy[dir])) continue; + + if (!hasPlayer(nx, ny)) { + if (!vis[nx][ny]) { + vis[nx][ny] = true; + q.emplace(nx, ny, d + 1); + } + } else { + int jx = mod(nx + dx[dir]); + int jy = mod(ny + dy[dir]); + if (IsConnected(nx, ny, dx[dir], dy[dir]) && !hasPlayer(jx, jy)) { + if (!vis[jx][jy]) { + vis[jx][jy] = true; + q.emplace(jx, jy, d + 1); + } + } else { + vector perp; + if (dir < 2) perp = {2, 3}; + else perp = {0, 1}; + + for (int pd : perp) { + int px = mod(nx + dx[pd]); + int py = mod(ny + dy[pd]); + if (IsConnected(nx, ny, dx[pd], dy[pd]) && !hasPlayer(px, py)) { + if (!vis[px][py]) { + vis[px][py] = true; + q.emplace(px, py, d + 1); + } + } + } + } + } + } + } + return -1; + } + + // 检查是否存在墙壁 + bool CheckWall(const pair pos, const Direct direction) const + { + switch (direction) { + case Direct::UP: return grid_map[pos.first][pos.second].Wall(); + case Direct::DOWN: return grid_map[pos.first][pos.second].Wall(); + case Direct::LEFT: return grid_map[pos.first][pos.second].Wall(); + case Direct::RIGHT: return grid_map[pos.first][pos.second].Wall(); + default: return true; + } + } + + // 执行移动和放置墙壁 + void MoveAndPlace(const PlayerID pid, const pair pos, const Direct direction) + { + players[pid].SetPos(pos); + switch (direction) { + case Direct::UP: + grid_map[pos.first][pos.second].SetWall(true); + grid_map[pos.first - 1][pos.second].SetWall(true); + break; + case Direct::DOWN: + grid_map[pos.first][pos.second].SetWall(true); + grid_map[pos.first + 1][pos.second].SetWall(true); + break; + case Direct::LEFT: + grid_map[pos.first][pos.second].SetWall(true); + grid_map[pos.first][pos.second - 1].SetWall(true); + break; + case Direct::RIGHT: + grid_map[pos.first][pos.second].SetWall(true); + grid_map[pos.first][pos.second + 1].SetWall(true); + break; + default:; + } + } + + // 判断游戏是否结束 + bool IsGameOver() const + { + const int dx[4] = {-1, 1, 0, 0}; + const int dy[4] = {0, 0, -1, 1}; + + int sx = players[0].x, sy = players[0].y; + int tx = players[1].x, ty = players[1].y; + + vector> vis(size, vector(size, false)); + queue> q; + q.emplace(sx, sy); + vis[sx][sy] = true; + + auto mod = [&](int v) { return (v + size) % size; }; + + while (!q.empty()) { + auto [x, y] = q.front(); q.pop(); + if (x == tx && y == ty) { + return false; + } + for (int dir = 0; dir < 4; dir++) { + int nx = mod(x + dx[dir]); + int ny = mod(y + dy[dir]); + if (!vis[nx][ny] && IsConnected(x, y, dx[dir], dy[dir])) { + vis[nx][ny] = true; + q.emplace(nx, ny); + } + } + } + return true; + } + + // 计算双方玩家所在区域大小 + pair GetAreaSize() const + { + const int dx[4] = {-1, 1, 0, 0}; + const int dy[4] = {0, 0, -1, 1}; + auto mod = [&](int v) { return (v + size) % size; }; + + auto bfs_count = [&](int pid) { + int sx = players[pid].x, sy = players[pid].y; + vector> vis(size, vector(size, false)); + queue> q; + q.emplace(sx, sy); + vis[sx][sy] = true; + int cnt = 1; + + while (!q.empty()) { + auto [x, y] = q.front(); q.pop(); + for (int dir = 0; dir < 4; dir++) { + int nx = mod(x + dx[dir]); + int ny = mod(y + dy[dir]); + if (!vis[nx][ny] && IsConnected(x, y, dx[dir], dy[dir])) { + vis[nx][ny] = true; + cnt++; + q.emplace(nx, ny); + } + } + } + return cnt; + }; + + int size0 = bfs_count(0); + int size1 = bfs_count(1); + return { size0, size1 }; + } + + private: + // 初始化边界 + void InitializeBoundary() + { + for (int x = 0; x < size; x++) { + for (int y = 0; y < size; y++) { + if (y == size - 1) { + grid_map[x][y].SetWall(true); + } + if (y == 0) { + grid_map[x][y].SetWall(true); + } + if (x == size - 1) { + grid_map[x][y].SetWall(true); + } + if (x == 0) { + grid_map[x][y].SetWall(true); + } + } + } + } + + // 判断(x, y)格子沿(dx, dy)方向是否连通 + bool IsConnected(int x, int y, int dx, int dy) const + { + int nx = (x + dx + size) % size; + int ny = (y + dy + size) % size; + if (dx == -1 && dy == 0) + return !grid_map[x][y].Wall() && !grid_map[nx][ny].Wall(); + else if (dx == 1 && dy == 0) + return !grid_map[x][y].Wall() && !grid_map[nx][ny].Wall(); + else if (dx == 0 && dy == -1) + return !grid_map[x][y].Wall() && !grid_map[nx][ny].Wall(); + else if (dx == 0 && dy == 1) + return !grid_map[x][y].Wall() && !grid_map[nx][ny].Wall(); + return false; + } + + inline static constexpr const char* style = R"( + +)"; +}; diff --git a/games/beast_trap_chess/icon.png b/games/beast_trap_chess/icon.png new file mode 100644 index 00000000..626943d6 Binary files /dev/null and b/games/beast_trap_chess/icon.png differ diff --git a/games/beast_trap_chess/mygame.cc b/games/beast_trap_chess/mygame.cc new file mode 100644 index 00000000..2ab1b25d --- /dev/null +++ b/games/beast_trap_chess/mygame.cc @@ -0,0 +1,245 @@ +// Copyright (c) 2018-present, JiaQi Yu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include +#include +#include + +#include "game_framework/stage.h" +#include "game_framework/util.h" +#include "utility/html.h" + +using namespace std; + +#include "board.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +class MainStage; +template using SubGameStage = StageFsm; +template using MainGameStage = StageFsm; +const GameProperties k_properties { + .name_ = "困兽棋", // the game name which should be unique among all the games + .developer_ = "铁蛋", + .description_ = "使自己地盘更大的圈地游戏", + .shuffled_player_id_ = true, +}; +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } +uint32_t Multiple(const CustomOptions& options) { + return min(static_cast((GET_OPTION_VALUE(options, 边长) - 3) / 2), 4); +} +const MutableGenericOptions k_default_generic_options{}; +const std::vector k_rule_commands = {}; + +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +{ + if (generic_options_readonly.PlayerNum() != 2) { + reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); + return false; + } + return true; +} + +const std::vector k_init_options_commands = { + InitOptionsCommand("设置游戏配置", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const uint32_t& size, const uint32_t& step) + { + GET_OPTION_VALUE(game_options, 边长) = size; + GET_OPTION_VALUE(game_options, 步数) = step; + return NewGameMode::MULTIPLE_USERS; + }, + ArithChecker(5, 19, "边长"), OptionalDefaultChecker>(3, 1, 10, "步数")), + InitOptionsCommand("独自一人开始游戏", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) + { + generic_options.bench_computers_to_player_num_ = 2; + return NewGameMode::SINGLE_USER; + }, + VoidChecker("单机")), +}; + +// ========== GAME STAGES ========== + +class RoundStage; + +class MainStage : public MainGameStage +{ + public: + MainStage(StageUtility&& utility) + : StageFsm(std::move(utility), + MakeStageCommand(*this, "查看当前游戏进展情况", &MainStage::Status_, VoidChecker("赛况"))) + , round_(0) + , player_scores_(Global().PlayerNum(), 0) + {} + + virtual int64_t PlayerScore(const PlayerID pid) const override { return player_scores_[pid]; } + + std::vector player_scores_; + + // 回合数 + int round_; + // 地图 + Board board; + // 当前行动玩家 + PlayerID currentPlayer; + + private: + CompReqErrCode Status_(const PlayerID pid, const bool is_public, MsgSenderBase& reply){ + reply() << Markdown(board.GetMarkdown(round_, currentPlayer), (GAME_OPTION(边长) + 2) * 60 + 100); + return StageErrCode::OK; + } + + void FirstStageFsm(SubStageFsmSetter setter) + { + srand((unsigned int)time(NULL)); + currentPlayer = 0; + board.size = GAME_OPTION(边长); + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + board.players.emplace_back(Global().PlayerName(pid), Global().PlayerAvatar(pid, 40)); + } + board.Initialize(); + + setter.Emplace(*this, ++round_); + } + + void NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) + { + currentPlayer = 1 - currentPlayer; + if (player_scores_[0] == 0 && player_scores_[1] == 0) { + setter.Emplace(*this, ++round_); + return; + } + Global().Boardcast() << Markdown(board.GetMarkdown(round_, 2), (GAME_OPTION(边长) + 2) * 60 + 150); + } +}; + +class RoundStage : public SubGameStage<> +{ + public: + RoundStage(MainStage& main_stage, const uint64_t round) + : StageFsm(main_stage, "第 " + std::to_string(round) + " 回合" , + MakeStageCommand(*this, "移动并放置墙壁", &RoundStage::MoveAndPlace_, AnyArg("坐标", "A1"), AlterChecker(direction_map)), + MakeStageCommand(*this, "认输", &RoundStage::Concede_, AlterChecker({{"认输", 0}, {"投降", 0}}))) + {} + + virtual void OnStageBegin() override + { + Global().SetReady(1 - Main().currentPlayer); + Global().Boardcast() << Markdown(Main().board.GetMarkdown(Main().round_, Main().currentPlayer), (GAME_OPTION(边长) + 2) * 60 + 150); + Global().Boardcast() << "请 " << At(Main().currentPlayer) << " 选择坐标移动和放置墙壁,格式例如:B3 上。" + << "时限 " << GAME_OPTION(时限) << " 秒,超时未行动判负"; + Global().StartTimer(GAME_OPTION(时限)); + } + + private: + AtomReqErrCode MoveAndPlace_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const string str, const Direct direction) + { + if (pid != Main().currentPlayer) { + reply() << "[错误] 本回合并非您的回合"; + return StageErrCode::FAILED; + } + string s = str; + string result = Board::CheckCoordinate(s); + if (result != "OK") { + reply() << result; + return StageErrCode::FAILED; + } + auto pos = Board::TranString(s); + int step = Main().board.CheckMoveStep(pid, pos); + if (step == -1) { + reply() << "[错误] 移动失败:目标位置无法到达"; + return StageErrCode::FAILED; + } + if (step > GAME_OPTION(步数)) { + reply() << "[错误] 移动失败:所需步数 " << step << "步 超出最大步数限制 " << GAME_OPTION(步数) << "步"; + return StageErrCode::FAILED; + } + const string direct_str[4] = {"上", "下", "左", "右"}; + if (Main().board.CheckWall(pos, direction)) { + reply() << "[错误] 放置墙壁失败:仅能放置于空位,目标位置" << direct_str[static_cast(direction)] << "方已有墙壁"; + return StageErrCode::FAILED; + } + + Main().board.MoveAndPlace(pid, pos, direction); + + return StageErrCode::READY; + } + + AtomReqErrCode Concede_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const uint32_t null) { + Main().player_scores_[pid] = -1; + Global().Boardcast() << "玩家 " << At(PlayerID(pid)) << " 认输,游戏结束。"; + return StageErrCode::CHECKOUT; + } + + virtual CheckoutErrCode OnStageTimeout() override + { + if (!Global().IsReady(0)) { + Main().player_scores_[0] = -1; + Global().Boardcast() << "玩家 " << At(PlayerID(0)) << " 超时判负"; + } else if (!Global().IsReady(1)) { + Main().player_scores_[1] = -1; + Global().Boardcast() << "玩家 " << At(PlayerID(1)) << " 超时判负"; + } + return StageErrCode::CHECKOUT; + } + + virtual CheckoutErrCode OnPlayerLeave(const PlayerID pid) override + { + Global().Boardcast() << "玩家 " << At(PlayerID(pid)) << " 强退认输。"; + Main().player_scores_[pid] = -1; + return StageErrCode::CHECKOUT; + } + + virtual CheckoutErrCode OnStageOver() override + { + if (Main().board.IsGameOver()) { + auto [area0, area1] = Main().board.GetAreaSize(); + if (area0 > area1) { + Main().player_scores_[0] = 1; + Global().Boardcast() << "游戏结束,玩家 " << At(PlayerID(0)) << " 获胜!"; + } else if (area0 < area1) { + Main().player_scores_[1] = 1; + Global().Boardcast() << "游戏结束,玩家 " << At(PlayerID(1)) << " 获胜!"; + } else { + Main().player_scores_[Main().currentPlayer] = 1; + Global().Boardcast() << "游戏结束,双方棋子所在区域大小相同,判定最后行动玩家 " << At(Main().currentPlayer) << " 获胜!"; + } + } + return StageErrCode::CHECKOUT; + } + + virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) override + { + if (Global().IsReady(pid)) { + return StageErrCode::OK; + } + pair pos; + int direction, step; + int attempt = 0; + do { + pos.first = rand() % GAME_OPTION(边长); + pos.second = rand() % GAME_OPTION(边长); + direction = rand() % 4; + step = Main().board.CheckMoveStep(pid, pos); + attempt++; + } while ((step > GAME_OPTION(步数) || step == -1 || Main().board.CheckWall(pos, static_cast(direction))) && attempt <= 1000); + if (attempt <= 1000) { + Main().board.MoveAndPlace(pid, pos, static_cast(direction)); + } + return StageErrCode::READY; + } +}; + +auto* MakeMainStage(MainStageFactory factory) { return factory.Create(); } + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + diff --git a/games/beast_trap_chess/option.cmake b/games/beast_trap_chess/option.cmake new file mode 100644 index 00000000..e69de29b diff --git a/games/beast_trap_chess/options.h b/games/beast_trap_chess/options.h new file mode 100644 index 00000000..1a226aaf --- /dev/null +++ b/games/beast_trap_chess/options.h @@ -0,0 +1,3 @@ +EXTEND_OPTION("每回合的时间限制", 时限, (ArithChecker(30, 3600, "超时时间(秒)")), 180) +EXTEND_OPTION("设置游戏地图边长", 边长, (ArithChecker(5, 19, "边长")), 5) +EXTEND_OPTION("玩家移动步数上限", 步数, (ArithChecker(1, 10, "步数")), 3) diff --git a/games/beast_trap_chess/rule.md b/games/beast_trap_chess/rule.md new file mode 100644 index 00000000..5ab44f05 --- /dev/null +++ b/games/beast_trap_chess/rule.md @@ -0,0 +1,14 @@ +## 困兽棋 + +- **游戏人数:** 2 +- **原作:** saiwei + +### 游戏简介 +- 棋盘为边长为 5 的正方形,玩家初始在棋盘的两角 +- 游戏目标为将双方隔开时,自己的地盘更大 + +### 游戏流程 +- 玩家每回合依次进行以下两个操作: + 1. 将你的棋子移动 0 至 3 步(可以跳过对方) + 2. 在最终停留位置的周围放一面长度为 1 的墙 +- 当双方棋子被完全隔开时游戏结束,棋子所在区域更大的玩家获胜。如果双方区域大小相同,则最后一回合行动的玩家胜利 diff --git a/games/beast_trap_chess/unittest.cc b/games/beast_trap_chess/unittest.cc new file mode 100644 index 00000000..d55c2ba4 --- /dev/null +++ b/games/beast_trap_chess/unittest.cc @@ -0,0 +1,42 @@ +// Copyright (c) 2018-present, Chang Liu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include "game_framework/unittest_base.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +GAME_TEST(2, leave_test1) +{ + START_GAME(); + + ASSERT_LEAVE(CHECKOUT, 0); + + ASSERT_SCORE(-1, 0); +} + +GAME_TEST(2, leave_test2) +{ + START_GAME(); + + ASSERT_LEAVE(CHECKOUT, 1); + + ASSERT_SCORE(0, -1); +} + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + gflags::ParseCommandLineFlags(&argc, &argv, true); + return RUN_ALL_TESTS(); +} diff --git a/games/beauty_vote/mygame.cc b/games/beauty_vote/mygame.cc index 4955ff8a..5d6695e3 100644 --- a/games/beauty_vote/mygame.cc +++ b/games/beauty_vote/mygame.cc @@ -24,12 +24,12 @@ const GameProperties k_properties { .developer_ = "铁蛋", .description_ = "选择恰当的数字,尽可能接近平均数的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 1; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 1; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 3) { reply() << "该游戏至少 3 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -40,7 +40,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 5; return NewGameMode::SINGLE_USER; diff --git a/games/bidding_poker/mygame.cc b/games/bidding_poker/mygame.cc index 0bddfe93..443f0426 100644 --- a/games/bidding_poker/mygame.cc +++ b/games/bidding_poker/mygame.cc @@ -26,15 +26,15 @@ const GameProperties k_properties { .developer_ = "森高", .description_ = "通过投标和拍卖提升波卡牌型的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return GET_OPTION_VALUE(options, 种子).empty() ? GET_OPTION_VALUE(options, 回合数) : 0; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 3) { reply() << "该游戏至少 3 人参加,当前玩家数为" << generic_options_readonly.PlayerNum(); @@ -45,7 +45,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 10; return NewGameMode::SINGLE_USER; diff --git a/games/blocked_road/mygame.cc b/games/blocked_road/mygame.cc index 1f95e9ba..c6e3d1bc 100644 --- a/games/blocked_road/mygame.cc +++ b/games/blocked_road/mygame.cc @@ -24,12 +24,12 @@ const GameProperties k_properties { .developer_ = "铁蛋", .description_ = "猜测并阻止对手移动棋子,使棋子达成3连的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 1; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 1; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -40,7 +40,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; @@ -223,6 +223,10 @@ class RoundStage : public SubGameStage<> private: AtomReqErrCode MoveChess_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const string str1, const string str2) { + if (Global().IsReady(pid)) { + reply() << "[错误] 本回合您已经完成行动"; + return StageErrCode::FAILED; + } if (pid != Main().currentPlayer) { reply() << "[错误] 本回合您为猜测者,无法移动棋子"; return StageErrCode::FAILED; @@ -242,6 +246,10 @@ class RoundStage : public SubGameStage<> AtomReqErrCode GuessMove_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const int guess) { + if (Global().IsReady(pid)) { + reply() << "[错误] 本回合您已经完成行动"; + return StageErrCode::FAILED; + } if (pid == Main().currentPlayer) { reply() << "[错误] 本回合您为移动者,无法进行猜测"; return StageErrCode::FAILED; @@ -258,6 +266,7 @@ class RoundStage : public SubGameStage<> virtual CheckoutErrCode OnStageTimeout() override { if (!Global().IsReady(0) && !Global().IsReady(1)) { + Main().player_scores_[0] = Main().player_scores_[1] = -1; Global().Boardcast() << "双方均超时,游戏平局"; } else if (!Global().IsReady(0)) { Main().player_scores_[0] = -1; diff --git a/games/bread_crisis/mygame.cc b/games/bread_crisis/mygame.cc index 0337bcac..5bb28e33 100644 --- a/games/bread_crisis/mygame.cc +++ b/games/bread_crisis/mygame.cc @@ -20,16 +20,17 @@ const GameProperties k_properties { .name_ = "面包危机", // the game name which should be unique among all the games .developer_ = "dva", .description_ = "保存体力,尽可能活到最后的游戏", + .shuffled_player_id_ = true, }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 2; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 2; } // 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { - if (generic_options_readonly.PlayerNum() < 3) { - reply() << "该游戏至少 3 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { + if (generic_options_readonly.PlayerNum() < 2) { + reply() << "该游戏至少 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); return false; } return true; @@ -37,7 +38,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 6; return NewGameMode::SINGLE_USER; diff --git a/games/chinese_chess/mygame.cc b/games/chinese_chess/mygame.cc index 6317a642..062ec98c 100644 --- a/games/chinese_chess/mygame.cc +++ b/games/chinese_chess/mygame.cc @@ -34,12 +34,12 @@ const GameProperties k_properties { .description_ = "多个帝国共同参与,定时重组棋盘的象棋游戏", }; constexpr uint64_t k_max_player = 6; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return k_max_player; } -uint32_t Multiple(const MyGameOptions& options) { return GET_OPTION_VALUE(options, 最小回合限制) / 6; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return k_max_player; } +uint32_t Multiple(const CustomOptions& options) { return GET_OPTION_VALUE(options, 最小回合限制) / 6; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() == 5) { reply() << "不好意思,该游戏允许 2、3、4、6 人参加,但唯独不允许 5 人参加"; @@ -60,7 +60,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 3; return NewGameMode::SINGLE_USER; diff --git a/games/crowd/mygame.cc b/games/crowd/mygame.cc index 9b40bea0..b9dfe376 100644 --- a/games/crowd/mygame.cc +++ b/games/crowd/mygame.cc @@ -17,6 +17,7 @@ #include "utility/html.h" #include "problems.h" +#include "problems_t.h" #include "rules.h" using namespace std; @@ -35,17 +36,19 @@ const GameProperties k_properties { .developer_ = "睦月", .description_ = "选择恰当选项,尽可能获取分数的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return GET_OPTION_VALUE(options, 测试模式) || GET_OPTION_VALUE(options, 测试) ? 0 : 1; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options; // formal questions -constexpr static uint32_t k_question_num = 72; -// with test questions -constexpr static uint32_t all_question_num = 113; +constexpr static uint32_t k_question_num = 78; +// with test1 questions +constexpr static uint32_t all_question_num = 132; +// test2 questions +constexpr static uint32_t t_question_num = 24; static const std::array create_question{ @@ -121,13 +124,13 @@ static const std::array create_question{ []() -> Question* { return new Q70(); }, []() -> Question* { return new Q71(); }, []() -> Question* { return new Q72(); }, - // test questions []() -> Question* { return new Q73(); }, []() -> Question* { return new Q74(); }, []() -> Question* { return new Q75(); }, []() -> Question* { return new Q76(); }, []() -> Question* { return new Q77(); }, []() -> Question* { return new Q78(); }, + // test questions []() -> Question* { return new Q79(); }, []() -> Question* { return new Q80(); }, []() -> Question* { return new Q81(); }, @@ -163,26 +166,79 @@ static const std::array create_question{ []() -> Question* { return new Q111(); }, []() -> Question* { return new Q112(); }, []() -> Question* { return new Q113(); }, + []() -> Question* { return new Q114(); }, + []() -> Question* { return new Q115(); }, + []() -> Question* { return new Q116(); }, + []() -> Question* { return new Q117(); }, + []() -> Question* { return new Q118(); }, + []() -> Question* { return new Q119(); }, + []() -> Question* { return new Q120(); }, + []() -> Question* { return new Q121(); }, + []() -> Question* { return new Q122(); }, + []() -> Question* { return new Q123(); }, + []() -> Question* { return new Q124(); }, + []() -> Question* { return new Q125(); }, + []() -> Question* { return new Q126(); }, + []() -> Question* { return new Q127(); }, + []() -> Question* { return new Q128(); }, + []() -> Question* { return new Q129(); }, + []() -> Question* { return new Q130(); }, + []() -> Question* { return new Q131(); }, + []() -> Question* { return new Q132(); }, }; +// test mode 2 +static const std::array test_question{ + []() -> Question* { return new Qt1(); }, + []() -> Question* { return new Qt2(); }, + []() -> Question* { return new Qt3(); }, + []() -> Question* { return new Qt4(); }, + []() -> Question* { return new Qt5(); }, + []() -> Question* { return new Qt6(); }, + []() -> Question* { return new Qt7(); }, + []() -> Question* { return new Qt8(); }, + []() -> Question* { return new Qt9(); }, + []() -> Question* { return new Qt10(); }, + []() -> Question* { return new Qt11(); }, + []() -> Question* { return new Qt12(); }, + []() -> Question* { return new Qt13(); }, + []() -> Question* { return new Qt14(); }, + []() -> Question* { return new Qt15(); }, + []() -> Question* { return new Qt16(); }, + []() -> Question* { return new Qt17(); }, + []() -> Question* { return new Qt18(); }, + []() -> Question* { return new Qt19(); }, + []() -> Question* { return new Qt20(); }, + []() -> Question* { return new Qt21(); }, + []() -> Question* { return new Qt22(); }, + []() -> Question* { return new Qt23(); }, + []() -> Question* { return new Qt24(); }, +}; -string init_question(int id) +string init_question(const int id, const int mode) { vector players; Player tempP; for (int i = 0; i < 10; i++) { players.push_back(tempP); } - Question* question = create_question[id - 1](); + Question* question; + if (mode != 2) { + question = create_question[id - 1](); + } else { + question = test_question[id - 1](); + } question -> init(players); question -> initTexts(players); question -> initOptions(); thread_local static string str; str = ""; - str += "题号:#" + to_string(question -> id) + "\n"; + str += string("题号:#") + (mode == 2 ? "t" : "") + to_string(question -> id) + "\n"; str += "出题者:" + (question -> author) + "\n"; - if (id > k_question_num) { + if (mode == 1) { str += "[测试] "; + } else if (mode == 2) { + str += "[其他/废弃] "; } str += "题目:" + (question -> title) + "\n\n"; str += question -> String(); @@ -197,7 +253,18 @@ char* find_question(const string& keyword) notfind_str = "[错误] 未找到包含“" + keyword + "”的题目"; bool flag = false; for (int i = 1; i <= all_question_num; i++) { - find_str = init_question(i); + find_str = init_question(i, i > k_question_num); + string::size_type position = find_str.find(keyword); + if (position != std::string::npos) { + flag = true; + break; + } + } + if (flag) { + return (char*)find_str.c_str(); + } + for (int i = 1; i <= t_question_num; i++) { + find_str = init_question(i, 2); string::size_type position = find_str.find(keyword); if (position != std::string::npos) { flag = true; @@ -217,7 +284,7 @@ const std::vector k_rule_commands = { AnyArg("关键字 / #题号", "#30")), }; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 4) { reply() << "该游戏至少 4 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -228,7 +295,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 10; return NewGameMode::SINGLE_USER; @@ -311,8 +378,10 @@ class RoundStage : public SubGameStage<> while(r == -1 || Main().used.find(r) != Main().used.end()) { r = rand() % k_question_num; - if (GAME_OPTION(测试模式)) { + if (GAME_OPTION(测试模式) == 1) { r = rand() % (all_question_num - k_question_num) + k_question_num; + } else if (GAME_OPTION(测试模式) == 2) { + r = rand() % t_question_num; } if(count++ > 1000) { Main().used.clear(); @@ -328,7 +397,11 @@ class RoundStage : public SubGameStage<> r = 0; } - q = create_question[r](); + if (GAME_OPTION(测试模式) != 2) { + q = create_question[r](); + } else { + q = test_question[r](); + } if(q == NULL) { @@ -566,7 +639,7 @@ void MainStage::FirstStageFsm(SubStageFsmSetter setter) } if(GAME_OPTION(测试模式)) { - Global().Boardcast() << "[警告] 正在进行新题测试!如出现异常,请联系管理员中断游戏。"; + Global().Boardcast() << "[警告] 正在进行新题测试!如出现异常,请联系管理员中断游戏。当前测试题库:" << GAME_OPTION(测试模式); } setter.Emplace(*this, ++round_); @@ -594,6 +667,10 @@ void MainStage::NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, for(int i = 1; i < round_; i++) fb += "     "; fb += "    "; + fb += "玩家"; + for(int i = 1; i < round_; i++) + fb += "第 " + to_string(i) + " 题"; + fb += "终局分数"; for(int i = 0; i < finalBoard.size(); i++) { fb += finalBoard[i]; diff --git a/games/crowd/options.h b/games/crowd/options.h index 9f633e90..7063ded9 100644 --- a/games/crowd/options.h +++ b/games/crowd/options.h @@ -2,4 +2,4 @@ EXTEND_OPTION("回合数", 回合数, (ArithChecker(1, 25, "回合数" EXTEND_OPTION("每回合时间限制", 时限, (ArithChecker(10, 3600, "超时时间(秒)")), 120) EXTEND_OPTION("增加特殊规则", 特殊规则, (ArithChecker(0, 9, "特殊规则")), 0) EXTEND_OPTION("测试题目", 测试, (ArithChecker(0, 999, "测试")), 0) -EXTEND_OPTION("【注意】此功能仅供测试新题,请勿在正常游戏中开启!", 测试模式, (ArithChecker(0, 1, "开启")), 0) +EXTEND_OPTION("仅用于新题测试,0视为不开启", 测试模式, (ArithChecker(0, 2, "模式")), 0) diff --git a/games/crowd/problems.h b/games/crowd/problems.h index bc09280b..25697be8 100644 --- a/games/crowd/problems.h +++ b/games/crowd/problems.h @@ -13,6 +13,15 @@ using namespace std; +#define REGISTER_QUESTION(ID, CLASSNAME) \ + namespace { \ + struct CLASSNAME##Register { \ + CLASSNAME##Register() { \ + QuestionFactory::get().registerQuestion(ID, []() -> Question* { return new CLASSNAME(); }); \ + } \ + } CLASSNAME##RegisterInstance; \ + } + class Player { public: @@ -122,6 +131,7 @@ class Question double playerNum; double maxScore; double minScore; + double medianScore; vector optionCount; double maxSelect; double minSelect; @@ -133,6 +143,18 @@ class Question void init(vector& players) { playerNum = players.size(); + // medianScore + vector scores; + for (int i = 0; i < playerNum; i++) { + scores.push_back(players[i].score); + } + sort(scores.begin(),scores.end()); + size_t size = scores.size(); + if (size % 2 == 0) { + medianScore = (scores[size / 2 - 1] + scores[size / 2]) / 2.0; + } else { + medianScore = scores[size / 2]; + } } // init texts and options. This function must be overloaded @@ -237,8 +259,38 @@ class Question } static double MaxPlayerScore(const std::vector& players) { - return std::ranges::max_element(players, [](const Player& p1, const Player& p2) { return p1.score < p2.score; })->score; + // C++11 兼容 + return std::max_element(players.begin(), players.end(), [](const Player& p1, const Player& p2) { return p1.score < p2.score; })->score; + //return std::ranges::max_element(players, [](const Player& p1, const Player& p2) { return p1.score < p2.score; })->score; + } +}; + +class QuestionFactory +{ +public: + static QuestionFactory& get() + { + static QuestionFactory instance; + return instance; + } + + void registerQuestion(int id, std::function creator) + { + creators[id] = creator; + } + + Question* create(int id) + { + auto it = creators.find(id); + if (it != creators.end()) + { + return it->second(); + } + return nullptr; } + +private: + std::map> creators; }; class QE : public Question @@ -1346,8 +1398,7 @@ class Q29 : public Question options.push_back("村民 (+0.5分,若场上外来者数比村民多,则村民变为+1分)"); options.push_back("外来者 (+1.5分,若人数最多,改为-0.5分)"); options.push_back("爪牙 (+2分,若人数最多,改为-1分)"); - options.push_back("恶魔(如果爪牙比村民和外来者都多,则+4分,否则+0.5分。\ -若人数最多,则额外失去 [恶魔数量 / 2] 分。)"); + options.push_back("恶魔(如果爪牙比村民和外来者都多,则+4分,否则+0.5分。若人数最多,则额外失去 [恶魔数量 / 2] 分。)"); } virtual void initExpects() override { @@ -1718,7 +1769,7 @@ class Q37 : public Question } virtual void initOptions() override { - options.push_back("恰有两人选此项,你们的分数互换"); + options.push_back("恰有两人选此项,你们的分数互换,然后+1"); options.push_back("恰有一人选此项,将你的分数取绝对值"); options.push_back("恰有一人选此项,在结算完上述两个选项后,分数最高者-2"); options.push_back("+0.5"); @@ -1739,6 +1790,8 @@ class Q37 : public Question p = &players[i]; } else { std::swap(p->score, players[i].score); + p->score += 1; + players[i].score += 1; } } } @@ -1910,7 +1963,9 @@ class Q41 : public Question if (optionCount[0] == 0) { tempScore[1] += 1; } - const bool wolf_is_max_count = optionCount[1] == *std::ranges::max_element(optionCount); + // C++11 兼容 + const bool wolf_is_max_count = optionCount[1] == *std::max_element(optionCount.begin(), optionCount.end()); + //const bool wolf_is_max_count = optionCount[1] == *std::ranges::max_element(optionCount); if (wolf_is_max_count) { tempScore[1] += 1; tempScore[2] = 2; @@ -2374,13 +2429,8 @@ class Q53 : public Question virtual void initTexts(vector& players) override { - vector tmp_score; int large_med_count = 0; - for (int i = 0; i < playerNum; i++) { - tmp_score.push_back(players[i].score); - } - sort(tmp_score.begin(),tmp_score.end()); - vars["med"] = int(tmp_score[int(playerNum / 2 + 1)]); + vars["med"] = ceil(medianScore); for (int i = 0; i < playerNum; i++) { if (players[i].score >= vars["med"]) { large_med_count++; @@ -2432,7 +2482,7 @@ class Q54 : public Question virtual void initTexts(vector& players) override { - texts.push_back("如果你盖的楼能盖的起来,不是空中楼阁,则你加所盖楼层的分数,否则你扣所盖楼层的分数。"); + texts.push_back("如果你盖的楼能盖的起来,不是空中楼阁(下方的楼层均有玩家选择),则你加所盖楼层的分数,否则你扣所盖楼层的分数。"); } virtual void initOptions() override { @@ -2638,8 +2688,8 @@ class Q59 : public Question virtual void initOptions() override { options.push_back("役满:如果没有人选择B,+3.2"); - options.push_back("速攻:+0.5,如果有人选择A,改为+1.5"); - options.push_back("平进:+1,如果B的人数最多,则B改为-1"); + options.push_back("速攻:+0.5,如果有人选择A,改为+1.5;但如果有人选择C且B的人数最多,则B改为-1"); + options.push_back("平进:+1"); options.push_back("九种九牌:-4,如果任何人选择此项,则使其他选项无效;但如果有人选择C,此选项分数改为+2"); options.push_back("天选之人:1.6%的概率获得64分"); } @@ -3157,7 +3207,7 @@ class Q71 : public Question { options.push_back("亚瑟的忠臣[好]:好人胜利则+1分"); options.push_back("梅林[好]:好人胜利则+4分;如果有人选刺客,改为-1"); - options.push_back("派西维尔[好]:如果好人胜利,且本选项人数在有人选择的选项里最少,+3分"); + options.push_back("派西维尔[好]:如果本选项人数在有人选择的选项里最少,+3分"); options.push_back("刺客[坏]:如果有人选梅林,坏人阵营必定胜利并且本选项+2分;否则-1分"); options.push_back("莫甘娜[坏]:坏人胜利则+1分"); } @@ -3176,8 +3226,8 @@ class Q71 : public Question } else { tempScore[0] = 1; tempScore[1] = 4; - tempScore[2] = optionCount[2] == nonZero_minSelect ? 3 : 0; } + tempScore[2] = optionCount[2] == nonZero_minSelect ? 3 : 0; tempScore[3] = optionCount[1] > 0 ? 2 : -1; } }; @@ -3219,16 +3269,14 @@ class Q72 : public Question } }; -// ——————————测试题目—————————— - -class Q73 : public Question // [待修改]人多时分数增减幅度太大 +class Q73 : public Question { public: Q73() { id = 73; - author = "圣墓上的倒吊人"; - title = "云顶之巢"; + author = "剩菜剩饭"; + title = "枪打出头鸟"; } virtual void initTexts(vector& players) override @@ -3237,706 +3285,320 @@ class Q73 : public Question // [待修改]人多时分数增减幅度太大 } virtual void initOptions() override { - options.push_back("速攻:如果没有中速,+3"); - options.push_back("中速:+1.5"); - options.push_back("贪贪贪:如果没有速攻,+5;反之-3"); + options.push_back("+1分"); + options.push_back("+2分"); + options.push_back("如果选择B的人数大于A,+4分。反之-3分"); + options.push_back("如果A选项人数最少且有人选择C选项,+C分,并使B选项改为-2分"); } virtual void initExpects() override { - expects.push_back("abbbcc"); + expects.push_back("aaabbbcd"); } virtual void calc(vector& players) override { - if (optionCount[1] == 0) { - tempScore[0] = 3; - } - tempScore[1] = 1.5; - if (optionCount[0] == 0) { - tempScore[2] = 5; - } else { - tempScore[2] = -3; + tempScore[0] = 1; + tempScore[1] = 2; + tempScore[2] = optionCount[1] > optionCount[0] ? 4 : -3; + if (optionCount[0] == minSelect && optionCount[2] > 0) { + tempScore[3] = optionCount[2]; + tempScore[1] = -2; } } }; -class Q74 : public Question // [待定]暂时无法判断选项平衡性 平衡 1 +class Q74 : public Question { public: Q74() { id = 74; - author = "Chance"; - title = "未命名"; + author = "orange juice"; + title = "大战讨口橘"; } virtual void initTexts(vector& players) override { - texts.push_back("选择下面一项,人数最多和最少的选项都成立"); + texts.push_back("街上偶遇个讨口子orange juice,拼尽全力无法战胜,你选择"); } virtual void initOptions() override { - options.push_back("A+1,B+2"); - options.push_back("A-2,C-3"); - options.push_back("C+2,D+2"); - options.push_back("D-1,B-2"); + options.push_back("乖乖投降:-1"); + options.push_back("尝试绕路:+0,该选项选择人最多则改为-2"); + options.push_back("同流合污:平分选择AB玩家失去的总分数,选择人数大于3人则改为-3"); + options.push_back("抢劫橘子:恰好有1人选择该选项,+3,否则-3"); } virtual void initExpects() override { - expects.push_back("aabcccd"); + expects.push_back("aaabbcccd"); } virtual void calc(vector& players) override { - if (optionCount[0] == maxSelect || optionCount[0] == minSelect) { - tempScore[0] += 1; - tempScore[1] += 2; - } - if (optionCount[1] == maxSelect || optionCount[1] == minSelect) { - tempScore[0] -= 2; - tempScore[2] -= 3; - } - if (optionCount[2] == maxSelect || optionCount[2] == minSelect) { - tempScore[2] += 2; - tempScore[3] += 2; - } - if (optionCount[3] == maxSelect || optionCount[3] == minSelect) { - tempScore[3] -= 1; - tempScore[1] -= 2; - } + tempScore[0] = -1; + tempScore[1] = optionCount[1] < maxSelect ? 0 : -2; + tempScore[2] = optionCount[2] <= 3 ? (optionCount[0] - tempScore[1] * optionCount[1]) / optionCount[2] : -3; + tempScore[3] = optionCount[3] == 1 ? 3 : -3; } }; -class Q75 : public Question // 备选题目 平衡 1 +class Q75 : public Question { public: Q75() { id = 75; - author = "Chance"; - title = "未命名"; + author = "圣墓上的倒吊人"; + title = "云顶之巢"; } virtual void initTexts(vector& players) override { - texts.push_back("选择下面一项,人数最少的一项成立"); + texts.push_back("你正在参加一局云顶之巢游戏,请选择你的策略"); } virtual void initOptions() override { - options.push_back("B+2"); - options.push_back("C-0.5"); - options.push_back("D-1"); - options.push_back("A+1"); + options.push_back("速攻:如果没有中速,+2"); + options.push_back("中速:+1"); + options.push_back("贪贪贪:如果没有速攻,+3;反之-1"); } virtual void initExpects() override { - expects.push_back("aabbbccd"); + expects.push_back("abbbcc"); } virtual void calc(vector& players) override { - if (optionCount[0] == minSelect) { - tempScore[1] += 2; - } - if (optionCount[1] == minSelect) { - tempScore[2] -= 0.5; - } - if (optionCount[2] == minSelect) { - tempScore[3] -= 1; - } - if (optionCount[3] == minSelect) { - tempScore[0] += 1; - } + tempScore[0] = optionCount[1] == 0 ? 2 : 0; + tempScore[1] = 1; + tempScore[2] = optionCount[0] == 0 ? 3 : -1; } }; -class Q76 : public Question // [待修改]题目过于复杂 +class Q76 : public Question { public: Q76() { id = 76; - author = "纤光"; - title = "平行时空"; + author = "大梦我先觉"; + title = "买票"; } virtual void initTexts(vector& players) override { - texts.push_back("有一项选择人数唯一最多时,此项生效"); - texts.push_back("有两项选择人数同时最多时,第三项生效"); - texts.push_back("有三项选择人数同时最多时,均不生效,所有人分数清零。"); + texts.push_back("一张票+5分,出价高的先购买,但只有前50%可以买到票(若同一批出价的人数高于剩余票的数量,则此批次和靠后的票都不会出售)请选择以下选项。"); } virtual void initOptions() override { - options.push_back("若有人选B,选C的人+1"); - options.push_back("若有人选C,选A的人+2"); - options.push_back("若有人选A,选B的人+3"); + options.push_back("不买"); + options.push_back("-1分"); + options.push_back("-2分"); + options.push_back("-3分"); } virtual void initExpects() override { - expects.push_back("abc"); + expects.push_back("aabccddd"); } virtual void calc(vector& players) override { - int maxCount = 0; - int win[2]; - for (int i = 0; i < 3; i++) { - if (optionCount[i] == maxSelect) { - maxCount++; - win[0] = i; - } else { - win[1] = i; - } - } - if (maxCount == 3) { - for (int i = 0; i < playerNum; i++) { - players[i].score = 0; - } - } else { - if (win[maxCount - 1] == 0 && optionCount[1] > 0) { - tempScore[2] = 1; - } - if (win[maxCount - 1] == 1 && optionCount[2] > 0) { - tempScore[0] = 2; - } - if (win[maxCount - 1] == 2 && optionCount[0] > 0) { - tempScore[1] = 3; + tempScore[1] = -1; + tempScore[2] = -2; + tempScore[3] = -3; + if (optionCount[3] <= playerNum / 2) { + tempScore[3] += 5; + if (optionCount[3] + optionCount[2] <= playerNum / 2) { + tempScore[2] += 5; + if (optionCount[3] + optionCount[2] + optionCount[1] <= playerNum / 2) { + tempScore[1] += 5; + } } } } }; -class Q77 : public Question // [待修改]题目过于复杂 +class Q77 : public Question { public: Q77() { id = 77; - author = "纤光"; - title = "差值投标"; + author = "克里斯丁"; + title = "杀人案"; } virtual void initTexts(vector& players) override { - texts.push_back("选择人数最多的两项对战(多项相同时取靠近A的两项),对战者中靠近A的称作败者,靠近D的称作胜者。另外两项与败者得分相同但额外-0.5。"); - texts.push_back("败者和胜者选项分别生效,随后胜者+4。"); + texts.push_back("目击杀人案,你选择成为"); } virtual void initOptions() override { - options.push_back("-0"); - options.push_back("-2"); - options.push_back("-4"); - options.push_back("-6"); + options.push_back("主犯:如果只有一人选择这项,+3分"); + options.push_back("帮凶:如果存在主犯,+1分"); + options.push_back("受害者:没人选择主犯,+4分;否则-1分"); + options.push_back("目击者:如果ABC都有人选,+2分"); } virtual void initExpects() override { - expects.push_back("abcd"); + expects.push_back("abbbccddd"); } virtual void calc(vector& players) override { - vector optionSort = optionCount; - vector orig_indexes(optionCount.size()); - iota(orig_indexes.begin(), orig_indexes.end(), 0); - partial_sort(orig_indexes.begin(), orig_indexes.begin() + 2, orig_indexes.end(), [&optionSort](int i, int j) { - return optionSort[i] > optionSort[j]; - }); - - int o1 = orig_indexes[0]; - int o2 = orig_indexes[1]; - if (o1 > o2) swap(o1, o2); - tempScore[o1] = -o1 * 2; - tempScore[o2] = -o2 * 2 + 4; - for (int i = 0; i < 4; i++) { - if (i != o1 && i != o2) { - tempScore[i] = tempScore[o1] - 0.5; - } - } + tempScore[0] = optionCount[0] == 1 ? 3 : 0; + tempScore[1] = optionCount[0] > 0 ? 1 : 0; + tempScore[2] = optionCount[0] > 0 ? -1 : 4; + tempScore[3] = optionCount[0] > 0 && optionCount[1] > 0 && optionCount[2] > 0 ? 2 : 0; } }; -class Q78 : public Question // [测试]随机性太高 +class Q78 : public Question { public: Q78() { id = 78; - author = "Chance"; - title = "我是大土块"; + author = "克里斯丁"; + title = "名侦探柯南"; } virtual void initTexts(vector& players) override { - texts.push_back("选择一项。"); + texts.push_back("酒厂抓卧底,专抓选择的人数最多的那人,若存在并列最多则一起抓。你选择成为"); } virtual void initOptions() override { - if (playerNum > 8) { - vars["percent"] = 10; - } else { - vars["percent"] = 16; - } - options.push_back("+2,有 [(B+C) *" + str(vars["percent"]) + "]% 的概率-4"); - options.push_back("+1,有 [(A-B) *" + str(vars["percent"]) + "]% 的概率使C+2"); - options.push_back("-2,有 [(A+D) *" + str(vars["percent"]) + "]% 的概率+6"); - options.push_back("-3,有 [(A+B-D) *" + str(vars["percent"]) + "]% 的概率使A和B-4,D+5"); + options.push_back("琴酒:只要抓到卧底则+3"); + options.push_back("伏特加:+1.5"); + options.push_back("波本(卧底):获得选择该选项人数的分数,被抓到则不得分。"); + options.push_back("基尔(卧底):平分5分,被抓到则不得分。"); + options.push_back("黑麦威士忌(卧底):+5,被抓到则不得分。E的人数视作乘2计算"); } virtual void initExpects() override { - expects.push_back("abcd"); + expects.push_back("aaabbbbcccdde"); } virtual void calc(vector& players) override { - tempScore[0] = 2; - tempScore[1] = 1; - tempScore[2] = -2; - tempScore[3] = -3; - if (rand() % 100 < (optionCount[1] + optionCount[2]) * vars["percent"]) { - tempScore[0] -= 4; - } - if (rand() % 100 < (optionCount[0] - optionCount[1]) * vars["percent"]) { - tempScore[2] += 2; - } - if (rand() % 100 < (optionCount[0] + optionCount[3]) * vars["percent"]) { - tempScore[2] += 6; - } - if (rand() % 100 < (optionCount[0] + optionCount[1] - optionCount[3]) * vars["percent"]) { - tempScore[0] -= 4; - tempScore[1] -= 4; - tempScore[2] += 5; - } + tempScore[0] = optionCount[2] == maxSelect || optionCount[3] == maxSelect || optionCount[4] * 2 >= maxSelect ? 3 : 0; + tempScore[1] = 1.5; + tempScore[2] = optionCount[2] < maxSelect ? optionCount[2] : 0; + tempScore[3] = optionCount[3] < maxSelect ? 5 / optionCount[3] : 0; + tempScore[4] = optionCount[4] * 2 < maxSelect ? 5 : 0; } }; -class Q79 : public Question // [待修改]分数增减幅度太大 平衡 1 +// ——————————测试题目—————————— + +class Q79 : public Question // [测试]奇偶存在较大的运气因素 { public: Q79() { id = 79; - author = "圣墓上的倒吊人"; - title = "资源配置"; + author = "Q群管家"; + title = "奇偶2"; } virtual void initTexts(vector& players) override { - vars["start"] = playerNum * 2; - vars["limit"] = int(playerNum * 3.2); - texts.push_back("桌上有 " + str(vars["start"]) + " 个资源,一资源一分,如果投入后总资源超过了 " + str(vars["limit"]) + " ,那获得(你的减分/所有玩家的减分)比例的总资源。如果投入后分数为负数,则选择无效。"); + texts.push_back("选择一项。"); } virtual void initOptions() override { - options.push_back("0"); - options.push_back("-1"); - options.push_back("-2"); - options.push_back("-4"); - options.push_back("-6"); - options.push_back("1.6%获得投入后总资源,否则-15"); + options.push_back("+3"); + options.push_back("-1,若此选项选择人数为奇数,A变为-3"); } virtual void initExpects() override { - expects.push_back("abbcccddddeef"); + expects.push_back("aaab"); } virtual void calc(vector& players) override { - int total = vars["start"]; - total = total + optionCount[1] * 1 + optionCount[2] * 2 + optionCount[3] * 4 + optionCount[4] * 6; + tempScore[0] = int(optionCount[1]) % 2 == 1 ? -3 : 3; tempScore[1] = -1; - tempScore[2] = -2; - tempScore[3] = -4; - tempScore[4] = -6; - for (int i = 0; i < playerNum; i++) { - if (players[i].score < -tempScore[players[i].select]) { - total += tempScore[players[i].select]; - players[i].score -= tempScore[players[i].select]; - } - } - if (rand() % 1000 < 16) { - tempScore[5] = total; - } else { - if (total > vars["limit"]) { - for (int i = 0; i < playerNum; i++) { - if (players[i].score >= -tempScore[players[i].select]) { - players[i].score += -tempScore[players[i].select] / (total - vars["start"]) * total; - } - } - } - tempScore[5] = -15; - } } }; -class Q80 : public Question // [待修改]选项不平衡,选D居多 +class Q80 : public Question // [待定]题目阅读时间较长 { public: Q80() { id = 80; - author = "飘渺"; - title = "电车难题"; + author = "圣墓上的倒吊人"; + title = "坐等反转"; } virtual void initTexts(vector& players) override { - texts.push_back("请选择你的位置。拉杆只有2个状态,即拉动偶数次会还原拉杆"); + texts.push_back("甲 / 乙,被选中的选项执行乙,否则执行甲。选项从前往后结算。"); } virtual void initOptions() override { - options.push_back("乘客:+0.5,但每有一个人拉动拉杆-0.5"); - options.push_back("多数派:+0.5,如果拉杆最终没有拉动,改为-2"); - options.push_back("少数派:+0.5,如果拉杆最终拉动,改为-2.5"); - options.push_back("-0.5,每一个选择此项的人都会拉动一次拉杆"); + options.push_back("选择人数最少的选项+2 / 选择人数最多的选项-2"); + options.push_back("使 D 选项无效化 / 0"); + options.push_back("D-2 / D+2"); + options.push_back("所有玩家均分自己的分数 / -1"); } virtual void initExpects() override { - expects.push_back("abbcdddd"); + expects.push_back("aabbcd"); } virtual void calc(vector& players) override { - tempScore[0] = 0.5 - optionCount[3] * 0.5; - tempScore[1] = 0.5; - if (int(optionCount[3]) % 2 == 0) { - tempScore[1] = -2; + for(int i = 0; i < playerNum; i++) { + if (optionCount[0] == 0 && optionCount[players[i].select] == minSelect) { + players[i].score += 2; + } + if (optionCount[0] > 0 && optionCount[players[i].select] == maxSelect) { + players[i].score -= 2; + } + if (optionCount[2] == 0 && players[i].select == 3) { + players[i].score -= 2; + } + if (optionCount[2] > 0 && players[i].select == 3) { + players[i].score += 2; + } } - tempScore[2] = 0.5; - if (int(optionCount[3]) % 2 == 1) { - tempScore[2] = -2.5; + if (optionCount[1] > 0) { + if (optionCount[3] == 0) { + double sum = 0; + for(int i = 0; i < playerNum; i++) { + sum += players[i].score; + } + for(int i = 0; i < playerNum; i++) { + players[i].score = sum / playerNum; + } + } else { + tempScore[3] -= 1; + } } - tempScore[3] = -0.5; } }; -class Q81 : public Question // [待修改]分数增减幅度太大 +class Q81 : public Question // [待定]积分不够分配时,A选项玩家得分太高 { public: Q81() { id = 81; - author = "Q群管家"; - title = "均分1"; + author = "圣墓上的倒吊人"; + title = "报销"; } virtual void initTexts(vector& players) override { - texts.push_back("选择一项。"); + vars["point"] = playerNum * 1.5; + texts.push_back("场上有 " + str(vars["point"]) + " 的积分,玩家从中获得和选项数值一致的积分,如果积分不够分配至所有玩家,选 A 的玩家平分所有积分"); } virtual void initOptions() override { - options.push_back("当前总积分×0.5"); - options.push_back("选择此选项的玩家均分他们的分数"); - options.push_back("当前总积分×(-0.5)"); + options.push_back("0"); + options.push_back("1"); + options.push_back("2"); + options.push_back("3"); + options.push_back("4"); } virtual void initExpects() override { - expects.push_back("abbbbbc"); - } - virtual void calc(vector& players) override - { - double sum = 0; - for (int i = 0; i < playerNum; i++) { - if (players[i].select == 1) { - sum += players[i].score; - } - } - for (int i = 0; i < playerNum; i++) { - if (players[i].select == 0) { - players[i].score = players[i].score * 0.5; - } - if (players[i].select == 1) { - players[i].score = sum / optionCount[1]; - } - if (players[i].select == 2) { - players[i].score = -players[i].score * 0.5; - } - } - } -}; - -class Q82 : public Question // [待修改]选择性问题,大部分情况玩家可能单选B -{ -public: - Q82() - { - id = 82; - author = "Q群管家"; - title = "均分2"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("选择一项。"); - } - virtual void initOptions() override - { - options.push_back("选择此选项的分数最高与最低的玩家均分他们的分数"); - options.push_back("正分的-2,负分的+2"); - } - virtual void initExpects() override - { - expects.push_back("aaabb"); - } - virtual void calc(vector& players) override - { - double maxS = -99999; - double minS = 99999; - for(int i = 0; i < playerNum; i++) - { - if (players[i].select == 0) { - maxS = max(maxS, players[i].score); - minS = min(minS, players[i].score); - } - } - double sum = 0; - int count = 0; - for(int i = 0; i < playerNum; i++) - { - if (players[i].select == 0 && (players[i].score == minS || players[i].score == maxS)) { - sum += players[i].score; - count++; - } - } - for (int i = 0; i < playerNum; i++) { - if (players[i].select == 0 && (players[i].score == minS || players[i].score == maxS)) { - players[i].score = sum / count; - } - if (players[i].select == 1) { - if (players[i].score > 0) { - players[i].score -= 2; - } - if (players[i].score < 0) { - players[i].score += 2; - } - } - } - } -}; - -class Q83 : public Question // [测试]奇偶存在较大的运气因素 -{ -public: - Q83() - { - id = 83; - author = "Q群管家"; - title = "奇偶2"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("选择一项。"); - } - virtual void initOptions() override - { - options.push_back("+3"); - options.push_back("-1,若此选项选择人数为奇数,A变为-3"); - } - virtual void initExpects() override - { - expects.push_back("aaab"); - } - virtual void calc(vector& players) override - { - tempScore[0] = int(optionCount[1]) % 2 == 1 ? -3 : 3; - tempScore[1] = -1; - } -}; - -class Q84 : public Question // [测试]奇偶存在较大的运气因素,多人时分数增减幅度太大 -{ -public: - Q84() - { - id = 84; - author = "Q群管家"; - title = "奇偶5"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("选择一项。"); - } - virtual void initOptions() override - { - options.push_back("若选择人数为奇数,扣除 [选择本选项人数/2] 的分数;否则获得相应的分数"); - options.push_back("0"); - } - virtual void initExpects() override - { - expects.push_back("aab"); - } - virtual void calc(vector& players) override - { - tempScore[0] = int(optionCount[0]) % 2 == 1 ? -optionCount[0] / 2 : optionCount[0] / 2; - } -}; - -class Q85 : public Question // [待修改]分数增减幅度太大 -{ -public: - Q85() - { - id = 85; - author = "胡扯弟"; - title = "云顶之巢"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("选择你的云巢身份。"); - } - virtual void initOptions() override - { - options.push_back("YAMI:总是能+2分的大佬,但是有5%的可能性失手-1"); - options.push_back("飘渺:10%+24的赌博爱好者"); - options.push_back("黑桃3:+13,但每有一个飘渺-1.75分"); - options.push_back("飞机:+12,但有YAMI就会被gank"); - options.push_back("西东:+A+B-C+D"); - options.push_back("胡扯:-114514,但有2.5%的可能性改为+1919810"); - } - virtual void initExpects() override - { - expects.push_back("aaaabbbccdeeeef"); - } - virtual void calc(vector& players) override - { - tempScore[0] = rand() % 100 < 5 ? -1 : 2; - tempScore[1] = rand() % 100 < 10 ? 24 : 0; - tempScore[2] = 13 - optionCount[1] * 1.75; - tempScore[3] = optionCount[0] == 0 ? 12 : 0; - tempScore[4] = optionCount[0] + optionCount[1] - optionCount[2] + optionCount[3]; - tempScore[5] = rand() % 1000 < 25 ? 1919810 : -114514; - } -}; - -class Q86 : public Question // [待修改]触发E选项取反概率太高 -{ -public: - Q86() - { - id = 86; - author = "圣墓上的倒吊人"; - title = "未来计划"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("(X)代表有且只有 X 人选择该选项时,该选项才能执行。执行顺序为序号顺序。无选项执行时执行E"); - } - virtual void initOptions() override - { - options.push_back("+4(1)"); - options.push_back("+2(2)"); - options.push_back("-3(3)"); - options.push_back("-1(≥2)"); - options.push_back("所有玩家的分数变为相反数(4)"); - } - virtual void initExpects() override - { - expects.push_back("abbbcdee"); - } - virtual void calc(vector& players) override - { - for(int i = 0; i < playerNum; i++) { - if (players[i].select == 0 && optionCount[0] == 1) { - players[i].score += 4; - } - if (players[i].select == 1 && optionCount[1] == 2) { - players[i].score += 2; - } - if (players[i].select == 2 && optionCount[2] == 3) { - players[i].score -= 3; - } - if (players[i].select == 3 && optionCount[3] >= 2) { - players[i].score -= 1; - } - } - if ((optionCount[0] != 1 && optionCount[1] != 2 && optionCount[2] != 3 && optionCount[3] < 2) || optionCount[4] == 4) { - for(int i = 0; i < playerNum; i++) { - players[i].score = -players[i].score; - } - } - } -}; - -class Q87 : public Question // [待定]题目阅读时间较长 -{ -public: - Q87() - { - id = 87; - author = "圣墓上的倒吊人"; - title = "坐等反转"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("甲 / 乙,被选中的选项执行乙,否则执行甲。选项从前往后结算。"); - } - virtual void initOptions() override - { - options.push_back("选择人数最少的选项+2 / 选择人数最多的选项-2"); - options.push_back("使 D 选项无效化 / 0"); - options.push_back("D-2 / D+2"); - options.push_back("所有玩家均分自己的分数 / -1"); - } - virtual void initExpects() override - { - expects.push_back("aabbcd"); - } - virtual void calc(vector& players) override - { - for(int i = 0; i < playerNum; i++) { - if (optionCount[0] == 0 && optionCount[players[i].select] == minSelect) { - players[i].score += 2; - } - if (optionCount[0] > 0 && optionCount[players[i].select] == maxSelect) { - players[i].score -= 2; - } - if (optionCount[2] == 0 && players[i].select == 3) { - players[i].score -= 2; - } - if (optionCount[2] > 0 && players[i].select == 3) { - players[i].score += 2; - } - } - if (optionCount[1] > 0) { - if (optionCount[3] == 0) { - double sum = 0; - for(int i = 0; i < playerNum; i++) { - sum += players[i].score; - } - for(int i = 0; i < playerNum; i++) { - players[i].score = sum / playerNum; - } - } else { - tempScore[3] -= 1; - } - } - } -}; - -class Q88 : public Question // [待定]积分不够分配时,A选项玩家得分太高 -{ -public: - Q88() - { - id = 88; - author = "圣墓上的倒吊人"; - title = "报销"; - } - - virtual void initTexts(vector& players) override - { - vars["point"] = playerNum * 1.5; - texts.push_back("场上有 " + str(vars["point"]) + " 的积分,玩家从中获得和选项数值一致的积分,如果积分不够分配至所有玩家,选 A 的玩家平分所有积分"); - } - virtual void initOptions() override - { - options.push_back("0"); - options.push_back("1"); - options.push_back("2"); - options.push_back("3"); - options.push_back("4"); - } - virtual void initExpects() override - { - expects.push_back("aabccddde"); + expects.push_back("aabccddde"); } virtual void calc(vector& players) override { @@ -3952,12 +3614,12 @@ class Q88 : public Question // [待定]积分不够分配时,A选项玩家 } }; -class Q89 : public Question // 备选题目 平衡 1 +class Q82 : public Question // [待修改]D选项得分过高 平衡 1 { public: - Q89() + Q82() { - id = 89; + id = 82; author = "圣墓上的倒吊人"; title = "奇珍异宝"; } @@ -3986,12 +3648,12 @@ class Q89 : public Question // 备选题目 平衡 1 } }; -class Q90 : public Question // [待定]暂时无法判断选项平衡性 +class Q83 : public Question // [待定]暂时无法判断选项平衡性 { public: - Q90() + Q83() { - id = 90; + id = 83; author = "圣墓上的倒吊人"; title = "站位"; } @@ -4030,12 +3692,12 @@ class Q90 : public Question // [待定]暂时无法判断选项平衡性 } }; -class Q91 : public Question // [待修改]题目过于复杂 +class Q84 : public Question // [待定]题目过于复杂 { public: - Q91() + Q84() { - id = 91; + id = 84; author = "纸团OvO"; title = "差值投标"; } @@ -4060,7 +3722,7 @@ class Q91 : public Question // [待修改]题目过于复杂 virtual void calc(vector& players) override { vector optionSort = optionCount; - vector orig_indexes(optionCount.size()); + vector orig_indexes(options.size()); iota(orig_indexes.begin(), orig_indexes.end(), 0); partial_sort(orig_indexes.begin(), orig_indexes.begin() + 2, orig_indexes.end(), [&optionSort](int i, int j) { return optionSort[i] > optionSort[j]; @@ -4090,12 +3752,12 @@ class Q91 : public Question // [待修改]题目过于复杂 } }; -class Q92 : public Question // [待修改]题目过于复杂 平衡 1 +class Q85 : public Question // [待修改]题目过于复杂 平衡 1 { public: - Q92() + Q85() { - id = 92; + id = 85; author = "xiaogt"; title = "债务危机"; } @@ -4141,12 +3803,12 @@ class Q92 : public Question // [待修改]题目过于复杂 平衡 1 } }; -class Q93 : public Question // [待修改]D选项不好理解,选项不平衡,AD人数居多 +class Q86 : public Question // [待修改]D选项不好理解,选项不平衡,AD人数居多 { public: - Q93() + Q86() { - id = 93; + id = 86; author = "大梦我先觉"; title = "夺宝奇兵"; } @@ -4189,12 +3851,12 @@ class Q93 : public Question // [待修改]D选项不好理解,选项不平 } }; -class Q94 : public Question // 备选题目 +class Q87 : public Question // 备选题目 { public: - Q94() + Q87() { - id = 94; + id = 87; author = "剩菜剩饭"; title = "未命名"; } @@ -4235,12 +3897,12 @@ class Q94 : public Question // 备选题目 } }; -class Q95: public Question // [待修改]分数增减幅度太大 +class Q88: public Question // [待修改]分数增减幅度太大 { public: - Q95() + Q88() { - id = 95; + id = 88; author = "齐齐"; title = "HP杀"; } @@ -4281,12 +3943,12 @@ class Q95: public Question // [待修改]分数增减幅度太大 } }; -class Q96: public Question // [待定]暂时无法判断选项平衡性 +class Q89: public Question // [待定]暂时无法判断选项平衡性 { public: - Q96() + Q89() { - id = 96; + id = 89; author = "Chance"; title = "未命名"; } @@ -4313,12 +3975,12 @@ class Q96: public Question // [待定]暂时无法判断选项平衡性 } }; -class Q97: public Question // [待定]暂时无法判断选项平衡性 +class Q90: public Question // [待定]暂时无法判断选项平衡性 { public: - Q97() + Q90() { - id = 97; + id = 90; author = "Chance"; title = "未命名"; } @@ -4354,110 +4016,12 @@ class Q97: public Question // [待定]暂时无法判断选项平衡性 } }; -class Q98 : public Question // [待修改]人多时分数增减幅度太大 -{ -public: - Q98() - { - id = 98; - author = "圣墓上的倒吊人"; - title = "真理"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("真理掌握在?"); - } - virtual void initOptions() override - { - options.push_back("少数人:+3,如果此项选择人数最多,则改为减 [选择该项人数] 的分数"); - options.push_back("多数人:-2"); - } - virtual void initExpects() override - { - expects.push_back("aabbb"); - } - virtual void calc(vector& players) override - { - tempScore[0] = optionCount[0] == maxSelect ? -optionCount[0] : 3; - tempScore[1] = -2; - } -}; - -class Q99 : public Question // [待修改]分数增减幅度太大 -{ -public: - Q99() - { - id = 99; - author = "丘陵萨满"; - title = "伊甸园"; - } - - virtual void initTexts(vector& players) override - { - vector tmp_score; - for (int i = 0; i < playerNum; i++) { - tmp_score.push_back(players[i].score); - } - sort(tmp_score.begin(),tmp_score.end()); - vars["score"] = tmp_score[2]; - texts.push_back("获胜的选项获得 [选择非此选项人数] 的分数,失败的选项失去 [选择获胜选项人数] 的分数"); - } - virtual void initOptions() override - { - options.push_back("红苹果:如果分数小于等于 " + str(vars["score"]) + " 的玩家都选择红苹果,红苹果获胜。但如果所有人都选择此项,全员分数取反"); - options.push_back("银苹果:如果选择此项的人数少于 C,且红苹果没有获胜,银苹果获胜。"); - options.push_back("金苹果:如果选择此项的人数少于等于 B,且红苹果没有获胜,金苹果获胜。"); - } - virtual void initExpects() override - { - expects.push_back("aaaabc"); - } - virtual void calc(vector& players) override - { - int win, l1, l2; - bool red_win = true; - bool is_invert = false; - win = l1 = l2 = 0; - for (int i = 0; i < playerNum; i++) { - if (players[i].score <= vars["score"] && players[i].select != 0) { - red_win = false; break; - } - } - if (red_win) { - if (optionCount[0] == playerNum) { - is_invert = true; - for (int i = 0; i < playerNum; i++) { - players[i].score = -players[i].score; - } - } else { - win = 0; - l1 = 1; l2 = 2; - } - } else { - if (optionCount[1] < optionCount[2]) { - win = 1; - l1 = 0; l2 = 2; - } else { - win = 2; - l1 = 0; l2 = 1; - } - } - if (!is_invert) { - tempScore[win] = optionCount[l1] + optionCount[l2]; - tempScore[l1] = -optionCount[win]; - tempScore[l2] = -optionCount[win]; - } - } -}; - -class Q100 : public Question // [待修改]A选项不平衡,无人选择 +class Q91 : public Question // [待修改]A选项不平衡,无人选择 { public: - Q100() + Q91() { - id = 100; + id = 91; author = "xiaogt"; title = "怪物糖果"; } @@ -4498,12 +4062,12 @@ class Q100 : public Question // [待修改]A选项不平衡,无人选择 } }; -class Q101: public Question // [待定]对于为玩过伊甸园的玩家读题略有困难 +class Q92: public Question // [待定]C选项达成难度过高 { public: - Q101() + Q92() { - id = 101; + id = 92; author = "九九归一"; title = "伊甸园"; } @@ -4514,8 +4078,8 @@ class Q101: public Question // [待定]对于为玩过伊甸园的玩家读题 } virtual void initOptions() override { - options.push_back("金苹果:若选择此项的人数大于选择B的人数,则平分 [未选择此项的人数] 分,否则减1分。"); - options.push_back("银苹果:若选择此项的人数大于选择A的人数,则平分 [未选择此项的人数] 分,否则减1分。"); + options.push_back("金苹果:若选择此项的人数小于选择B的人数,则平分 [未选择此项的人数] 分,否则减1分。"); + options.push_back("银苹果:若选择此项的人数小于选择A的人数,则平分 [未选择此项的人数] 分,否则减1分。"); options.push_back("红苹果:若选择A的人数等于选择B的人数,则平分 [未选择此项的人数] 分,否则减1分。"); } virtual void initExpects() override @@ -4525,10 +4089,10 @@ class Q101: public Question // [待定]对于为玩过伊甸园的玩家读题 virtual void calc(vector& players) override { int win, l1, l2; - if (optionCount[0] > optionCount[1]) { + if (optionCount[0] < optionCount[1]) { win = 0; l1 = 1; l2 = 2; - } else if (optionCount[1] > optionCount[0]) { + } else if (optionCount[1] < optionCount[0]) { win = 1; l1 = 0; l2 = 2; } else { @@ -4540,12 +4104,12 @@ class Q101: public Question // [待定]对于为玩过伊甸园的玩家读题 } }; -class Q102: public Question // [待定]清0仍需考虑 +class Q93: public Question // [待定]清0仍需考虑 { public: - Q102() + Q93() { - id = 102; + id = 93; author = "剩菜剩饭"; title = "寻宝猎人"; } @@ -4585,12 +4149,12 @@ class Q102: public Question // [待定]清0仍需考虑 } }; -class Q103 : public Question // [待修改]反向执行的结算容易引发歧义 +class Q94 : public Question // [待修改]正向和反向执行,统一结算难以看懂 { public: - Q103() + Q94() { - id = 103; + id = 94; author = "栗子"; title = "利他之众"; } @@ -4644,86 +4208,12 @@ class Q103 : public Question // [待修改]反向执行的结算容易引发 } }; -class Q104 : public Question // [待修改]分数增减幅度太大,A选项风险太高 -{ -public: - Q104() - { - id = 104; - author = "圣墓上的倒吊人"; - title = "战争中的贵族"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("你的国家与敌国正在战争中,选择一项行动。"); - } - virtual void initOptions() override - { - options.push_back("继续抵抗:+5,如果有玩家选择了B,那选择此项的人分数归零"); - options.push_back("有条件投降:-3"); - options.push_back("润出国外:如果有人选 A,则-2"); - } - virtual void initExpects() override - { - expects.push_back("aabccccc"); - } - virtual void calc(vector& players) override - { - if (optionCount[1] == 0) { - tempScore[0] = 5; - } else { - for (int i = 0; i < playerNum; i++) { - if (players[i].select == 0) { - players[i].score = 0; - } - } - } - tempScore[1] = -3; - if (optionCount[0] > 0) { - tempScore[2] = -2; - } - } -}; - -class Q105: public Question // [待修改]多人时分数增减幅度太大 -{ -public: - Q105() - { - id = 105; - author = "飞机鸭卖蛋"; - title = "太多挂机A了…?"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("分别记选择A、B、C选项的人数为a、b、c:"); - } - virtual void initOptions() override - { - options.push_back("获得 a 分"); - options.push_back("获得 a*1.5-b 分"); - options.push_back("获得 a*2-c 分"); - } - virtual void initExpects() override - { - expects.push_back("aaaabbccc"); - } - virtual void calc(vector& players) override - { - tempScore[0] = optionCount[0]; - tempScore[1] = optionCount[0] * 1.5 - optionCount[1]; - tempScore[2] = optionCount[0] * 2 - optionCount[2]; - } -}; - -class Q106: public Question // [待修改]存在50%概率影响 +class Q95: public Question // [待修改]存在50%概率影响 { public: - Q106() + Q95() { - id = 106; + id = 95; author = "飞机鸭卖蛋"; title = "听力练习"; } @@ -4750,12 +4240,12 @@ class Q106: public Question // [待修改]存在50%概率影响 } }; -class Q107: public Question // [待定]暂时无法判断选项平衡性 +class Q96: public Question // 备选题目 { public: - Q107() + Q96() { - id = 107; + id = 96; author = "飞机鸭卖蛋"; title = "有情人终成眷属"; } @@ -4766,8 +4256,8 @@ class Q107: public Question // [待定]暂时无法判断选项平衡性 } virtual void initOptions() override { - options.push_back("若恰有两人选择,-2,否则+2"); - options.push_back("若恰有两人选择,-2,否则+2"); + options.push_back("若恰有两人选择此选项,-2,否则+2"); + options.push_back("若恰有两人选择此选项,-2,否则+2"); options.push_back("若选择人数最多,-2,否则+3"); } virtual void initExpects() override @@ -4782,12 +4272,12 @@ class Q107: public Question // [待定]暂时无法判断选项平衡性 } }; -class Q108: public Question // [待定]暂时无法判断选项平衡性 +class Q97: public Question // [待定]暂时无法判断选项平衡性 { public: - Q108() + Q97() { - id = 108; + id = 97; author = "小黄鸭"; title = "E卡抉择"; } @@ -4823,12 +4313,12 @@ class Q108: public Question // [待定]暂时无法判断选项平衡性 } }; -class Q109 : public Question // [待修改]存在50%概率影响 +class Q98 : public Question // [待修改]存在50%概率影响 { public: - Q109() + Q98() { - id = 109; + id = 98; author = "齐齐"; title = "月下人狼"; } @@ -4870,12 +4360,12 @@ class Q109 : public Question // [待修改]存在50%概率影响 } }; -class Q110 : public Question // [待修改]选项不平衡,选A居多 +class Q99 : public Question // [待修改]选项不平衡,选A居多 { public: - Q110() + Q99() { - id = 110; + id = 99; author = "匿名"; title = "选举"; } @@ -4930,12 +4420,12 @@ class Q110 : public Question // [待修改]选项不平衡,选A居多 } }; -class Q111 : public Question +class Q100 : public Question // 备选题目 { public: - Q111() + Q100() { - id = 111; + id = 100; author = "纤光"; title = "金铃铛"; } @@ -4967,12 +4457,12 @@ class Q111 : public Question } }; -class Q112 : public Question // 备选题目 平衡 4 +class Q101 : public Question // 备选题目 平衡 4 { public: - Q112() + Q101() { - id = 112; + id = 101; author = "xiaogt"; title = "投资"; } @@ -5014,46 +4504,1590 @@ class Q112 : public Question // 备选题目 平衡 4 } }; -class Q113 : public Question // [待修改]人多时分数增减幅度太大 +class Q102: public Question // [待定]暂时无法判断选项平衡性 平衡 1 { public: - Q113() + Q102() { - id = 113; - author = "圣墓上的倒吊人"; - title = "税收"; + id = 102; + author = "Chance"; + title = "未命名"; } virtual void initTexts(vector& players) override { - texts.push_back("选择一项。"); + texts.push_back("选择下面一项,人数最多和最少的选项都成立"); } virtual void initOptions() override { - options.push_back("减45%分数"); - options.push_back("减 [B选项人数] 的分数"); - options.push_back("减 [C选项人数*1.5] 的分数"); + options.push_back("A+1,B+2"); + options.push_back("A-2,C-3"); + options.push_back("C+2,D+2"); + options.push_back("D-1,B-2"); } virtual void initExpects() override { - expects.push_back("abbbcc"); + expects.push_back("aabcccd"); } virtual void calc(vector& players) override { - for (int i = 0; i < playerNum; i++) { - if (players[i].select == 0) { - if (players[i].score > 0) { - players[i].score -= players[i].score * 0.45; - } else { - players[i].score += players[i].score * 0.45; - } - } + if (optionCount[0] == maxSelect || optionCount[0] == minSelect) { + tempScore[0] += 1; + tempScore[1] += 2; + } + if (optionCount[1] == maxSelect || optionCount[1] == minSelect) { + tempScore[0] -= 2; + tempScore[2] -= 3; + } + if (optionCount[2] == maxSelect || optionCount[2] == minSelect) { + tempScore[2] += 2; + tempScore[3] += 2; + } + if (optionCount[3] == maxSelect || optionCount[3] == minSelect) { + tempScore[3] -= 1; + tempScore[1] -= 2; } - tempScore[1] = -optionCount[1]; - tempScore[2] = -optionCount[2] * 1.5; } }; +class Q103 : public Question // [待定]AB玩家居多 平衡 1 +{ +public: + Q103() + { + id = 103; + author = "Chance"; + title = "未命名"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择下面一项,人数最少的一项成立"); + } + virtual void initOptions() override + { + options.push_back("B+2"); + options.push_back("C-0.5"); + options.push_back("D-1"); + options.push_back("A+1"); + } + virtual void initExpects() override + { + expects.push_back("aabbbccd"); + } + virtual void calc(vector& players) override + { + if (optionCount[0] == minSelect) { + tempScore[1] += 2; + } + if (optionCount[1] == minSelect) { + tempScore[2] -= 0.5; + } + if (optionCount[2] == minSelect) { + tempScore[3] -= 1; + } + if (optionCount[3] == minSelect) { + tempScore[0] += 1; + } + } +}; + +class Q104 : public Question // [待定]题目过于复杂 +{ +public: + Q104() + { + id = 104; + author = "纤光"; + title = "平行时空"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("有一项选择人数唯一最多时,此项生效"); + texts.push_back("有两项选择人数同时最多时,第三项生效"); + texts.push_back("有三项选择人数同时最多时,均不生效,所有人分数清零。"); + } + virtual void initOptions() override + { + options.push_back("若有人选B,选C的人+1"); + options.push_back("若有人选C,选A的人+2"); + options.push_back("若有人选A,选B的人+3"); + } + virtual void initExpects() override + { + expects.push_back("abc"); + } + virtual void calc(vector& players) override + { + int maxCount = 0; + int win[2]; + for (int i = 0; i < 3; i++) { + if (optionCount[i] == maxSelect) { + maxCount++; + win[0] = i; + } else { + win[1] = i; + } + } + if (maxCount == 3) { + for (int i = 0; i < playerNum; i++) { + players[i].score = 0; + } + } else { + if (win[maxCount - 1] == 0 && optionCount[1] > 0) { + tempScore[2] = 1; + } + if (win[maxCount - 1] == 1 && optionCount[2] > 0) { + tempScore[0] = 2; + } + if (win[maxCount - 1] == 2 && optionCount[0] > 0) { + tempScore[1] = 3; + } + } + } +}; + +class Q105 : public Question // [待定]题目过于复杂 +{ +public: + Q105() + { + id = 105; + author = "纤光"; + title = "差值投标"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择人数最多的两项对战(多项相同时取靠近A的两项),对战者中靠近A的称作败者,靠近D的称作胜者。另外两项与败者得分相同但额外-0.5。"); + texts.push_back("败者和胜者选项分别生效,随后胜者+4。"); + } + virtual void initOptions() override + { + options.push_back("-0"); + options.push_back("-2"); + options.push_back("-4"); + options.push_back("-6"); + } + virtual void initExpects() override + { + expects.push_back("abcd"); + } + virtual void calc(vector& players) override + { + vector optionSort = optionCount; + vector orig_indexes(options.size()); + iota(orig_indexes.begin(), orig_indexes.end(), 0); + partial_sort(orig_indexes.begin(), orig_indexes.begin() + 2, orig_indexes.end(), [&optionSort](int i, int j) { + return optionSort[i] > optionSort[j]; + }); + + int o1 = orig_indexes[0]; + int o2 = orig_indexes[1]; + if (o1 > o2) swap(o1, o2); + tempScore[o1] = -o1 * 2; + tempScore[o2] = -o2 * 2 + 4; + for (int i = 0; i < 4; i++) { + if (i != o1 && i != o2) { + tempScore[i] = tempScore[o1] - 0.5; + } + } + } +}; + +class Q106 : public Question // 备选题目 +{ +public: + Q106() + { + id = 106; + author = "本仙子很强"; + title = "兵主"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择一项。"); + } + virtual void initOptions() override + { + options.push_back("鲜血:+1"); + options.push_back("冰霜:本回合中,你的扣分变为加分。但是如果没有扣分,你-2"); + options.push_back("邪恶:使所有没有选择邪恶的人-1"); + options.push_back("彩虹:如果ABC均有人选择,+[人数/2]向上取整"); + } + virtual void initExpects() override + { + expects.push_back("aaaabbbccdd"); + } + virtual void calc(vector& players) override + { + tempScore[0] = 1; + if (optionCount[2] > 0) { + tempScore[0] -= 1; + tempScore[1] += 1; + tempScore[3] -= 1; + } else { + tempScore[1] = -2; + } + tempScore[3] = optionCount[0] > 0 && optionCount[1] > 0 && optionCount[2] > 0 ? ceil((optionCount[0] + optionCount[1] + optionCount[2]) / 2.0) : 0; + } +}; + +class Q107 : public Question // [待定]多人暂时无法确定平衡性 +{ +public: + Q107() + { + id = 119; + author = "周小墨"; + title = "炉石传说"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择一项。"); + } + virtual void initOptions() override + { + options.push_back("冲锋:+6,每有一人选择本选项得分-1"); + options.push_back("嘲讽:每有一人选择冲锋得分+1"); + options.push_back("突袭:-1,每有一人选择嘲讽得分+1"); + options.push_back("圣盾:+1"); + } + virtual void initExpects() override + { + expects.push_back("aabbbccdddd"); + } + virtual void calc(vector& players) override + { + tempScore[0] = 6 - optionCount[0]; + tempScore[1] = optionCount[0]; + tempScore[2] = -1 + optionCount[1]; + tempScore[3] = 1; + } +}; + +class Q108 : public Question // [待修改]选项分数变动太大 +{ +public: + Q108() + { + id = 108; + author = "克里斯丁"; + title = "半句话失效"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("有人选择的选项执行对应的效果。"); + } + virtual void initOptions() override + { + options.push_back("-9,你的分数取相反数"); + options.push_back("-1,并令A的后半句话失效"); + options.push_back("-2,并令A的前半句话失效"); + options.push_back("令B、C中选的人更多的选项后半句话失效,选的人更少的选项前半句话失效,B、C一样多则本选项作废"); + } + virtual void initExpects() override + { + expects.push_back("abcd"); + } + virtual void calc(vector& players) override + { + bool reversal = true; + bool minus9 = true; + if (optionCount[1] > 0) { + tempScore[1] = -1; + reversal = false; + } + if (optionCount[2] > 0) { + tempScore[2] = -2; + minus9 = false; + } + if (optionCount[3] > 0 && optionCount[1] != optionCount[2]) { + if (optionCount[1] > optionCount[2]) { + reversal = !reversal; + tempScore[2] = 0; + } else { + minus9 = !minus9; + tempScore[1] = 0; + } + } + for(int i = 0; i < playerNum; i++) { + if (players[i].select == 0) { + if (minus9) players[i].score -= 9; + if (reversal) players[i].score = -players[i].score; + } + } + } +}; + +class Q109 : public Question // 备选题目 +{ +public: + Q109() + { + id = 109; + author = "克里斯丁"; + title = "克里斯丁的印第安扑克"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("对手是一张五点。你无法从他的表情中推断自己的牌面大小。因此你选择"); + texts.push_back("**A、B中选择人数更多的视作本局出牌,若并列则优先级A>B**"); + } + virtual void initOptions() override + { + options.push_back("牌面3"); + options.push_back("牌面10"); + options.push_back("加注,若牌面为10则+2,反之-2"); + options.push_back("开牌,若牌面为10则+1,反之-1"); + options.push_back("弃牌,+2,若牌面为10则-2"); + } + virtual void initExpects() override + { + expects.push_back("aaabbcddde"); + } + virtual void calc(vector& players) override + { + int card = optionCount[0] >= optionCount[1] ? 3 : 10; + tempScore[2] = card == 10 ? 2 : -2; + tempScore[3] = card == 10 ? 1 : -1; + tempScore[4] = card == 10 ? -2 : 2; + } +}; + +class Q110 : public Question // 备选题目 +{ +public: + Q110() + { + id = 110; + author = "克里斯丁"; + title = "逆转裁判"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("律师和检察官合伙针对证人。"); + texts.push_back("若BCD人数总和加起来大于A的人数,证人被抓,反之被告被抓。你选择担任"); + } + virtual void initOptions() override + { + options.push_back("证人:无论是否被抓均+1.5"); + options.push_back("被告:+3,被抓改为-1"); + options.push_back("律师:+2,被告被抓改为+0"); + options.push_back("检察官:+3,若证人被抓改为+0"); + } + virtual void initExpects() override + { + expects.push_back("aaaabbcccd"); + } + virtual void calc(vector& players) override + { + bool witness_arrested = optionCount[1] + optionCount[2] + optionCount[3] > optionCount[0]; + tempScore[0] = 1.5; + tempScore[1] = witness_arrested ? 3 : -1; + tempScore[2] = witness_arrested ? 2 : 0; + tempScore[3] = witness_arrested ? 0 : 3; + } +}; + +class Q111 : public Question // [待定]联邦过强 +{ +public: + Q111() + { + id = 111; + author = "克里斯丁"; + title = "对战"; + } + + virtual void initTexts(vector& players) override + { + vars["num"] = playerNum; + texts.push_back("联邦和帝国对战,战力更大的阵营获胜(正常情况下一人一点战力)并平分" + str(vars["num"]) + "分"); + } + virtual void initOptions() override + { + options.push_back("联邦精锐:-1,一人两点战力。"); + options.push_back("联邦特工局:只要有人选择B,若联邦最终胜利,额外吸取所有选D的人各一分并平分给所有选B的人。"); + options.push_back("帝国官僚:+1,一人0.5战力。"); + options.push_back("帝国军队:+1"); + } + virtual void initExpects() override + { + expects.push_back("abbbbccccdd"); + } + virtual void calc(vector& players) override + { + double team1 = optionCount[0] * 2 + optionCount[1]; + double team2 = optionCount[2] * 0.5 + optionCount[3]; + tempScore[0] = -1; + tempScore[2] = 1; + tempScore[3] = 1; + if (team1 > team2) { + tempScore[0] += vars["num"] / (optionCount[0] + optionCount[1]); + tempScore[1] += vars["num"] / (optionCount[0] + optionCount[1]); + if (optionCount[1] > 0) { + tempScore[1] += optionCount[3] / optionCount[1]; + tempScore[3] -= 1; + } + } else if (team1 < team2) { + tempScore[2] += vars["num"] / (optionCount[2] + optionCount[3]); + tempScore[3] += vars["num"] / (optionCount[2] + optionCount[3]); + } + } +}; + +class Q112 : public Question // 备选题目 +{ +public: + Q112() + { + id = 112; + author = "克里斯丁"; + title = "许愿池"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("往池子里投币许愿,一币一分,若池子里的钱币比玩家人数多,则许愿成功"); + } + virtual void initOptions() override + { + options.push_back("0币"); + options.push_back("1币,许愿成功+3"); + options.push_back("2币,许愿成功+5"); + options.push_back("3币,许愿成功+7"); + options.push_back("搅混水:-0.5并-2币"); + options.push_back("超级搅屎棍:-1并-4币"); + } + virtual void initExpects() override + { + expects.push_back("aaabbbccdeeeff"); + } + virtual void calc(vector& players) override + { + bool success = optionCount[1] + optionCount[2]*2 + optionCount[3]*3 - optionCount[4]*2 - optionCount[5]*4 > playerNum; + tempScore[1] = success ? 2 : -1; + tempScore[2] = success ? 3 : -2; + tempScore[3] = success ? 4 : -3; + tempScore[4] = -0.5; + tempScore[5] = -1; + } +}; + +class Q113 : public Question // [待定]靠后选项无人选择 +{ +public: + Q113() + { + id = 113; + author = "克里斯丁"; + title = "贪婪的代价"; + } + + virtual void initTexts(vector& players) override + { + vars["num"] = playerNum * 2; + texts.push_back("克里斯丁看到你可怜的分数大发慈悲,决定施舍你 " + str(vars["num"]) + " 分。若索取的总分数超了则一分也得不到。你决定"); + } + virtual void initOptions() override + { + options.push_back("不取意外之财,克里斯丁赞赏你的品德并赠送2分"); + options.push_back("取3分"); + options.push_back("取4分"); + options.push_back("取5分"); + options.push_back("取6分"); + } + virtual void initExpects() override + { + expects.push_back("aaabbbccdde"); + } + virtual void calc(vector& players) override + { + bool success = optionCount[1]*3 + optionCount[2]*4 + optionCount[3]*5 + optionCount[4]*6 <= vars["num"]; + tempScore[0] = 2; + tempScore[1] = success ? 3 : 0; + tempScore[2] = success ? 4 : 0; + tempScore[3] = success ? 5 : 0; + tempScore[4] = success ? 6 : 0; + } +}; + +class Q114 : public Question // [待定]题目阅读时间较长 +{ +public: + Q114() + { + id = 114; + author = "大红大紫"; + title = "外星危机"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("分别记每个选项的人数为abcd"); + } + virtual void initOptions() override + { + options.push_back("防御:若a+c>b+d防御成功A+2且B+1,否则A-1"); + options.push_back("逃亡:若b+c>a+d成功逃亡B+1.5,否则B-0.5"); + options.push_back("中立:C+1,若成功防御或逃亡C+0.5(可叠加)"); + options.push_back("背叛:有人选D时ABC都额外-1。若成功逃亡或防御,D-1.5;若同时逃亡和防御,D-3.5"); + } + virtual void initExpects() override + { + expects.push_back("aabbcccdd"); + } + virtual void calc(vector& players) override + { + bool defense = optionCount[0] + optionCount[2] > optionCount[1] + optionCount[3]; + bool escape = optionCount[1] + optionCount[2] > optionCount[0] + optionCount[3]; + if (defense) { + tempScore[0] += 2; + tempScore[1] += 1; + } else { + tempScore[0] -= 1; + } + if (escape) { + tempScore[1] += 1.5; + } else { + tempScore[1] -= 0.5; + } + tempScore[2] = 1 + (defense + escape) * 0.5; + if (optionCount[3] > 0) { + tempScore[0] -= 1; + tempScore[1] -= 1; + tempScore[2] -= 1; + if (defense && escape) { + tempScore[3] = -3.5; + } else if (defense || escape) { + tempScore[3] = -1.5; + } + } + } +}; + +class Q115 : public Question +{ +public: + Q115() + { + id = 115; + author = "剩菜剩饭"; + title = "绑架"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("你们被绑架了,头目正在思考要不要杀你们(选项按照 A到E 依次结算)"); + } + + virtual void initOptions() override + { + options.push_back("反抗:如果至少有一半人选择此项,+3分,否则-1分"); + options.push_back("服软:+2分"); + options.push_back("卖人:+1分,选择的人数每比B多一人,B扣1分"); + options.push_back("沉默:如果所有人中AB中扣分的人数达到一半,+2分"); + options.push_back("卧底:如果本选项选择的人数最少(不含0人),结算后加分最多的人都-3分"); + } + + virtual void initExpects() override + { + expects.push_back("aaabbbccde"); + } + + virtual void calc(vector& players) override + { + tempScore[0] = optionCount[0] >= playerNum / 2 ? 3 : -1; + tempScore[1] = 2; + tempScore[2] = 1; + if (optionCount[2] > optionCount[1]) { + tempScore[1] -= optionCount[2] - optionCount[1]; + } + int deduct_num = 0; + if (tempScore[0] > 0) deduct_num += optionCount[0]; + if (tempScore[1] > 0) deduct_num += optionCount[1]; + tempScore[3] = deduct_num >= playerNum / 2 ? 2 : 0; + if (optionCount[4] == nonZero_minSelect) { + int maxTemp = *max_element(tempScore.begin(), tempScore.begin() + 5); + for (int i = 0; i < 5; i++) { + if (tempScore[i] == maxTemp) { + tempScore[i] -= 3; + } + } + } + } +}; + +class Q116 : public Question +{ +public: + Q116() + { + id = 116; + author = "剩菜剩饭"; + title = "狐狸觅食"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("狐狸要开始觅食了!分数大于等于中位数的玩家是“兔子”,反之是“狐狸”(目前中位数=" + str(medianScore) + ")"); + texts.push_back("“兔子”受到选项影响。若“兔子”所选选项的人数超过玩家的1/3(向上舍入)且有“狐狸”选择此项时,被吃掉,改为-2分"); + texts.push_back("“狐狸”不执行选项。若所选选项有“兔子”被吃,+2分,否则-1分"); + } + + virtual void initOptions() override + { + options.push_back("+2"); + options.push_back("+1"); + options.push_back("0"); + options.push_back("-1"); + } + + virtual void initExpects() override + { + expects.push_back("abcd"); + } + + virtual void calc(vector& players) override + { + const int score[4] = {2, 1, 0, -1}; + const int limit = ceil(playerNum / 3.0); + for (int i = 0; i < 4; i++) { + bool fox = false; + bool eaten = false; + for (const auto& player : players) { + if (player.select == i && player.score < medianScore) { + fox = true; + break; + } + } + for (auto& player : players) { + if (player.select == i && player.score >= medianScore) { + if (optionCount[i] > limit && fox) { + player.score -= 2; + eaten = true; + } else { + player.score += score[i]; + } + } + } + for (auto& player : players) { + if (player.select == i && player.score < medianScore) { + player.score += eaten ? 2 : -1; + } + } + } + } +}; + +class Q117 : public Question +{ +public: + Q117() + { + id = 117; + author = "大梦我先觉"; + title = "金币抉择"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("初始金币为0,1金币1分。请选择以下选项"); + } + + virtual void initOptions() override + { + options.push_back("+1,金币+3,不平分金币"); + options.push_back("0,金币-1,参与金币平分"); + options.push_back("0(金币=0)/+2(金币>0)/-2(金币<0)"); + options.push_back("获得 [亏空金币数] 的数量(金币<0)"); + } + + virtual void initExpects() override + { + expects.push_back("aabbbbcddd"); + } + + virtual void calc(vector& players) override + { + int coins = optionCount[0] * 3 - optionCount[1]; + tempScore[0] = 1; + tempScore[1] = coins / optionCount[1]; + tempScore[2] = coins > 0 ? 2 : (coins < 0 ? -2 : 0); + tempScore[3] = coins < 0 ? -coins : 0; + } +}; + +class Q118 : public Question +{ +public: + Q118() + { + id = 118; + author = "纤光"; + title = "囚徒困境"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("AB中人数更少项的人数记为X"); + texts.push_back("CD中人数更多项的人数记为Y"); + } + + virtual void initOptions() override + { + options.push_back("合作:+(X-Y)"); + options.push_back("合作:+2(X-Y)"); + options.push_back("欺骗:-2(Y-X)"); + options.push_back("欺骗:-Y"); + } + + virtual void initExpects() override + { + expects.push_back("abcd"); + } + + virtual void calc(vector& players) override + { + int X = optionCount[0] < optionCount[1] ? optionCount[0] : optionCount[1]; + int Y = optionCount[2] > optionCount[3] ? optionCount[2] : optionCount[3]; + tempScore[0] = X - Y; + tempScore[1] = 2 * (X - Y); + tempScore[2] = -2 * (Y - X); + tempScore[3] = -Y; + } +}; + +class Q119 : public Question +{ +public: + Q119() + { + id = 119; + author = "冰糖_Cryst"; + title = "无人区"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("如果所有选项都有人选择,所有选项生效且得分改为相反数"); + } + + virtual void initOptions() override + { + options.push_back("0"); + options.push_back("如果没有人选择A,+1"); + options.push_back("如果没有人选择B,+2"); + options.push_back("如果没有人选择C,+3"); + } + + virtual void initExpects() override + { + expects.push_back("abcd"); + } + + virtual void calc(vector& players) override + { + tempScore[1] = optionCount[0] == 0 ? 1 : 0; + tempScore[2] = optionCount[1] == 0 ? 2 : 0; + tempScore[3] = optionCount[2] == 0 ? 3 : 0; + if (optionCount[0] && optionCount[1] && optionCount[2] && optionCount[3]) { + tempScore[1] = -1; + tempScore[2] = -2; + tempScore[3] = -3; + } + } +}; + +class Q120 : public Question +{ +public: + Q120() + { + id = 120; + author = "克里斯丁"; + title = "红楼梦"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("红楼梦,你得到了风月宝鉴,你决定看"); + texts.push_back("(选择人数少的那一项执行,一样便都不执行)"); + } + + virtual void initOptions() override + { + options.push_back("正面:+1"); + options.push_back("反面:-1,然后你的分数取绝对值"); + } + + virtual void initExpects() override + { + expects.push_back("aaaaaaaaab"); + } + + virtual void calc(vector& players) override + { + if (optionCount[0] < optionCount[1]) { + tempScore[0] = 1; + } + if (optionCount[0] > optionCount[1]) { + for (auto& player : players) { + if (player.select == 1) { + player.score = -(player.score - 1); + } + } + } + } +}; + +class Q121 : public Question // [待修改]分数增减幅度太大 +{ +public: + Q121() + { + id = 121; + author = "克里斯丁"; + title = "西游记"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("西游记,你选择成为"); + } + + virtual void initOptions() override + { + options.push_back("唐僧:+5,若有妖怪且没有孙悟空则-5"); + options.push_back("孙悟空:只有强者能当。分数最高者选择该选项+3,其他人选择该选项-100"); + options.push_back("猪八戒:+1"); + options.push_back("妖怪:-1"); + } + + virtual void initExpects() override + { + expects.push_back("aaabccccccccddd"); + } + + virtual void calc(vector& players) override + { + tempScore[0] = (optionCount[3] > 0 && optionCount[1] == 0) ? -5 : 5; + for (auto& player : players) { + if (player.select == 1) { + if (player.score == maxScore) { + player.score += 3; + } else { + player.score -= 100; + } + } + } + tempScore[2] = 1; + tempScore[3] = -1; + } +}; + +class Q122 : public Question // [待修改]分数增减幅度太大 +{ +public: + Q122() + { + id = 122; + author = "克里斯丁"; + title = "水浒传"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("水浒传,你选择成为"); + } + + virtual void initOptions() override + { + options.push_back("西门庆:+4,存在武松-4"); + options.push_back("潘金莲:+2,存在武松-2"); + options.push_back("王婆:+1,存在武松-1"); + options.push_back("武大郎:+6,若武松不在场,选ABC的人数大于1人则-6。如果武松在场,选ABC的人数大于4人则-6。"); + options.push_back("武松:乃是魔星下凡,得分最低者选此项+3,其他人选择该项-100"); + } + + virtual void initExpects() override + { + expects.push_back("abbcccdddddde"); + } + + virtual void calc(vector& players) override + { + tempScore[0] = optionCount[4] == 0 ? 4 : -4; + tempScore[1] = optionCount[4] == 0 ? 2 : -2; + tempScore[2] = optionCount[4] == 0 ? 1 : -1; + if (optionCount[4] == 0) { + tempScore[3] = (optionCount[0] + optionCount[1] + optionCount[2] > 1) ? -6 : 6; + } else { + tempScore[3] = (optionCount[0] + optionCount[1] + optionCount[2] > 4) ? -6 : 6; + } + for (auto& player : players) { + if (player.select == 4) { + if (player.score == minScore) { + player.score += 3; + } else { + player.score -= 100; + } + } + } + } +}; + +class Q123 : public Question // [待修改]分数增减幅度太大 +{ +public: + Q123() + { + id = 123; + author = "克里斯丁"; + title = "三国演义"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("三国演义,你选择成为(0人选择或两股势力选择人数相同均视作势力覆灭,不会纳入最终的人数结算)"); + } + + virtual void initOptions() override + { + options.push_back("刘备:若其势力人数最少则A+6,B-4,C+2"); + options.push_back("曹操:若其势力人数最多则A-3,B+3,C-3"); + options.push_back("孙权:若其势力人数第二多或第二少则A-5,B+2,C+2"); + } + + virtual void initExpects() override + { + expects.push_back("abc"); + } + + virtual void calc(vector& players) override + { + if (optionCount[0] == optionCount[2]) { + tempScore[0] = -3; + tempScore[1] = 3; + tempScore[2] = -3; + } else if (optionCount[1] == optionCount[2]) { + tempScore[0] = 6; + tempScore[1] = -4; + tempScore[2] = 2; + } else if (optionCount[0] != optionCount[1]) { + if (optionCount[0] == nonZero_minSelect) { + tempScore[0] += 6; + tempScore[1] -= 4; + tempScore[2] += 2; + } + if (optionCount[1] == maxSelect) { + tempScore[0] -= 3; + tempScore[1] += 3; + tempScore[2] -= 3; + } + if (((optionCount[2] != maxSelect && optionCount[2] != minSelect) || optionCount[0] == 0 || optionCount[1] == 0) && optionCount[2] > 0) { + tempScore[0] -= 5; + tempScore[1] += 2; + tempScore[2] += 2; + } + } + } +}; + +class Q124 : public Question +{ +public: + Q124() + { + id = 124; + author = "蓝田"; + title = "大混战(10人标准)ver.1"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("大混战开启,选择你的行为"); + } + + virtual void initOptions() override + { + options.push_back("加入混战A组:成员总分数大于B组则平分8分"); + options.push_back("加入混战B组:成员总分数大于A组则平分6分,本组在计算总分数时+2分"); + options.push_back("逃离混战:若人数超过2人,-2;否则+2.5"); + options.push_back("什么?我是吃瓜群众:+0.5"); + } + + virtual void initExpects() override + { + expects.push_back("aaaabbbbcddd"); + } + + virtual void calc(vector& players) override + { + int scoreA = 0, scoreB = 2; + for (auto& player : players) { + if (player.select == 0) { + scoreA += player.score; + } else if (player.select == 1) { + scoreB += player.score; + } + } + if (scoreA > scoreB) { + tempScore[0] = 8 / optionCount[0]; + } else if (scoreB > scoreA) { + tempScore[1] = 6 / optionCount[1]; + } + tempScore[2] = optionCount[2] > 2 ? -2 : 2.5; + tempScore[3] = 0.5; + } +}; + +// 开发者: An idle brain Q125 - Q130 +class Q125 : public Question +{ +public: + Q125() + { + id = 125; + author = "栗子"; + title = "贫富差距"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("在统计分数时,每个玩家分数都取绝对值后再计算总分"); + } + + virtual void initOptions() override + { + options.push_back("选择该选项玩家总分≤玩家总分*0.75,+2;否则-1"); + options.push_back("选择该选项玩家总分≤玩家总分*0.5,+3;否则-2"); + options.push_back("选择该选项玩家总分≤玩家总分*0.25,+4;否则-3"); + } + + virtual void initExpects() override + { + expects.push_back("aaabbc"); + } + + virtual void calc(vector& players) override + { + double scoreA = 0, scoreB = 0, scoreC = 0, sum = 0; + for (auto& player : players) { + if (player.select == 0) { + scoreA += fabs(player.score); + } else if (player.select == 1) { + scoreB += fabs(player.score); + } else if (player.select == 2) { + scoreC += fabs(player.score); + } + sum += fabs(player.score); + } + if (scoreA * 1.0 <= 0.75 * sum) { + tempScore[0] = 2; + } else { + tempScore[0] = -1; + } + if (scoreB * 1.0 <= 0.5 * sum) { + tempScore[1] = 3; + } else { + tempScore[1] = -2; + } + if (scoreC * 1.0 <= 0.25 * sum) { + tempScore[2] = 4; + } else { + tempScore[2] = -3; + } + } +}; + +class Q126 : public Question +{ +public: + Q126() + { + id = 126; + author = "剩菜剩饭"; + title = "好心喂了狗"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择一项"); + } + + virtual void initOptions() override + { + options.push_back("-1分,如果没有人选择此项,BCD+都3分"); + options.push_back("如果没有人选择此项,选D的玩家-2分"); + options.push_back("如果没有人选择此项,选B,D的玩家+2分"); + options.push_back("如果没有人选择此项,选B,C的玩家-2分"); + options.push_back("如果没有人选择此项,第一名+4分"); + } + + virtual void initExpects() override + { + expects.push_back("abbbcccdddd"); + } + + virtual void calc(vector& players) override + { + if (optionCount[0] > 0) { + tempScore[0] -= 1; + } else { + tempScore[1] += 3; + tempScore[2] += 3; + tempScore[3] += 3; + } + if (optionCount[1] == 0) { + tempScore[3] -= 2; + } + if (optionCount[2] == 0) { + tempScore[1] += 2; + tempScore[3] += 2; + } + if (optionCount[3] == 0) { + tempScore[1] -= 2; + tempScore[2] -= 2; + } + if (optionCount[4] == 0) { + for(int i = 0; i < playerNum; i++) + { + if(players[i].score == maxScore) + { + players[i].score += 4; + } + } + } + } +}; + + +class Q127 : public Question +{ +public: + Q127() + { + id = 127; + author = "克里斯丁"; + title = "海盗纷争"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("遇到一波海盗(共 " + str(playerNum) + " 人),你选择"); + } + + virtual void initOptions() override + { + options.push_back("加入,海盗人数+1,若海盗胜利则瓜分选择战斗和逃跑的人数两倍的分数。若无人战斗则所有海盗-1"); + options.push_back("战斗,杀死一个海盗,若战胜海盗+3,否则-1"); + options.push_back("战斗,杀死三个海盗,若战胜海盗+2,否则-1"); + options.push_back("战斗,杀死五个海盗,若战胜海盗+1,否则-1"); + options.push_back("逃跑"); + } + + virtual void initExpects() override + { + expects.push_back("aaaaaaaabbbbcccdde"); + } + + virtual void calc(vector& players) override + { + double pirate = optionCount[0] + playerNum; + double killPirate = optionCount[1] + optionCount[2] * 3 + optionCount[3] * 5; + if(pirate > killPirate) { + tempScore[0] = playerNum * 2.0 / optionCount[0] - 2; + tempScore[1] = -1; + tempScore[2] = -1; + tempScore[3] = -1; + } else { + // 注:克里斯丁:相等算作战斗者胜利 + tempScore[0] = -1; + tempScore[1] = 3; + tempScore[2] = 2; + tempScore[3] = 1; + } + } +}; + +class Q128 : public Question +{ +public: + Q128() + { + id = 128; + author = "纸团OvO"; + title = "隹投票最精确?"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("共有甲乙丙丁四个对象,所有玩家将投出自己选项内的票,统计完票后,每位投票中包括“最高票的对象(可并列)”的玩家获得对应分数"); + } + + virtual void initOptions() override + { + options.push_back("不投票直接+0.5分"); + options.push_back("甲乙丙,+1分"); + options.push_back("甲乙,+2分"); + options.push_back("甲丁,+2分"); + options.push_back("丙丁,+2分"); + options.push_back("丙,+3分"); + options.push_back("丁,+3分"); + } + + virtual void initExpects() override + { + expects.push_back("abbcccddddeeeffffffgggggg"); + } + + virtual void calc(vector& players) override + { + double jia = 0, yi = 0, bing = 0, ding = 0; + jia = optionCount[1] + optionCount[2] + optionCount[3]; + yi = optionCount[1] + optionCount[2]; + bing = optionCount[1] + optionCount[4] + optionCount[5]; + ding = optionCount[3] + optionCount[4] + optionCount[6]; + // tempScore 表示每个选项的分数 optionCount 表示每个选项的人数 + tempScore[0] = 0.5; + + double maxVote = max({jia, yi, bing, ding}); + // 标记最高票的对象 + bool topJia = abs(jia - maxVote) < 1e-6; + bool topYi = abs(yi - maxVote) < 1e-6; + bool topBing = abs(bing - maxVote) < 1e-6; + bool topDing = abs(ding - maxVote) < 1e-6; + if (topJia || topYi || topBing) { + tempScore[1] = 1; + } + if (topJia || topYi) { + tempScore[2] = 2; + } + if (topJia || topDing) { + tempScore[3] = 2; + } + if (topBing || topDing) { + tempScore[4] = 2; + if (topBing) { + tempScore[5] = 3; + } + if (topDing) { + tempScore[6] = 3; + } + } + } +}; + +class Q129 : public Question +{ +public: + Q129() + { + id = 129; + author = "aka展博"; + title = "未命名"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("你和你的队伍正在寻找一座传说中的宝藏,每个选项代表不同的行动策略。选择人数最多的选项将决定整个队伍的行动方式,并根据队伍的整体表现获得相应的分数。"); + } + + virtual void initOptions() override + { + options.push_back("潜行探索:+2分。如果人数多于B,额外获得1分;如果人数少于B,额外失去1分。"); + options.push_back("直接挖掘:+1分。每有1人选择,本选项得分增加0.5分"); + options.push_back("寻求帮助:-1分。如果人数多于D,改为2分"); + options.push_back("保持警惕:-2分。如果人数少于C,改为2分"); + } + + virtual void initExpects() override + { + expects.push_back("aaabbbbbbbbbbbbccd"); + } + + virtual void calc(vector& players) override + { + // 统计每个选项的人数 + double a = optionCount[0]; + double b = optionCount[1]; + double c = optionCount[2]; + double d = optionCount[3]; + + // A + tempScore[0] = 2; + if (a > b) { + tempScore[0] += 1; + } + else if (a < b) { + tempScore[0] -= 1; + } + + // B + tempScore[1] = 1 + b * 0.5; + + // C + if (c > d) { + tempScore[2] = 2; + } + else { + tempScore[2] = -1; + } + + // D + if (d < c) { + tempScore[3] = 2; + } + else { + tempScore[3] = -2; + } + } +}; + +class Q130 : public Question +{ +public: + Q130() + { + id = 130; + author = "aka展博"; + title = "未命名"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("在一个绿洲中,所有玩家被困在了这里,在这里也仅剩下 " + str(playerNum * 3) + " 分的物资,根据ABCD的顺序依次结算。"); + } + + virtual void initOptions() override + { + options.push_back("获取选择该选项人数*1分物资。如果物资不够,则都不得分"); + options.push_back("获取选择该选项人数*0.5分物资。如果物资不够,则都不得分"); + options.push_back("-2分,并给绿洲增加2分物资。如果选择该选项人数比A多,改为+3分(不再提供物资)"); + options.push_back("如果资源池里的分数最后大于4,+3分;但如果选择该选项人数比B多,-2分。"); + } + + virtual void initExpects() override + { + expects.push_back("aaaaabbbcdd"); + } + + virtual void calc(vector& players) override + { + double resource = playerNum * 3; + double a = optionCount[0]; + double b = optionCount[1]; + double c = optionCount[2]; + double d = optionCount[3]; + + // A结算 + if (resource >= a * a) { + tempScore[0] = a; + resource -= a * a; + } else { + tempScore[0] = 0; + } + + // B结算 + if (resource >= b * b * 0.5) { + tempScore[1] = b * 0.5; + resource -= b * b * 0.5; + } + else { + tempScore[1] = 0; + } + + // C结算 + if (c > a) { + tempScore[2] = 3; + } + else { + tempScore[2] = -2; + resource += 2; + } + + // D结算 + if (d > b) { + tempScore[3] = -2; + } else { + if (resource > 4) { + tempScore[3] = 3; + } + else { + tempScore[3] = 0; + } + } + } +}; + +// 开发者:问号 +class Q131 : public Question +{ +public: + Q131() + { + id = 131; + author = "纤光"; + title = "潘多拉魔盒"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("ABC中人数相对最多的一项生效"); + texts.push_back("DEF仅在不超过2人选择时生效"); + texts.push_back("所有不生效的选项-0.5"); + } + virtual void initOptions() override + { + options.push_back("A+1,B-1"); + options.push_back("B+1,C+1"); + options.push_back("C-1,A-1"); + options.push_back("+1,每有一人选此项,本题所有得分翻一次倍"); + options.push_back("-1,每有一人选此项,本题所有得分取一次反"); + options.push_back("每有一人选此项,A与B便互换一次效果"); + } + virtual void initExpects() override + { + expects.push_back("aaabbbcccdef"); + } + virtual void calc(vector& players) override + { + bool shen_xiao[6] = {0}; + if (optionCount[0] >= optionCount[1] && optionCount[0] >= optionCount[2]) shen_xiao[0] = 1; + if (optionCount[1] >= optionCount[0] && optionCount[1] >= optionCount[2]) shen_xiao[1] = 1; + if (optionCount[2] >= optionCount[1] && optionCount[2] >= optionCount[0]) shen_xiao[2] = 1; + for (int i = 3; i < 6; i++) { + if(optionCount[i] <= 2) shen_xiao[i] = 1; + } + + for (int i = 0; i < 6; i++) { + if(!shen_xiao[i]) tempScore[i] =- 0.5; + else tempScore[i] = 0; + } + + if (!optionCount[5]) { + if (shen_xiao[0]) { + tempScore[0]++; + tempScore[1]--; + } + if (shen_xiao[1]) { + tempScore[1]++; + tempScore[2]++; + } + } else { + if(shen_xiao[1]) { + tempScore[0]++; + tempScore[1]--; + } + if (shen_xiao[0]) { + tempScore[1]++; + tempScore[2]++; + } + } + if(shen_xiao[2]) { + tempScore[2]--; + tempScore[0]--; + } + if(shen_xiao[3]) { + tempScore[3]++; + } + if(shen_xiao[4]) { + tempScore[4]--; + } + + if (shen_xiao[3] && optionCount[3]) { + for (int i = 0; i < 6; i++) { + if (optionCount[3] == 1) { + tempScore[i] *= 2; + } else { + tempScore[i] *= 4; + } + } + } + + if (shen_xiao[4] == 1) { + for(int i = 0; i < 6; i++) { + tempScore[i] = -tempScore[i]; + } + } + } +}; + +// 开发者:苣屋逊太郎 +class Q132 : public Question +{ +public: + Q132() + { + id = 132; + author = "苣屋逊太郎"; + title = "绝望推杆"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("A和B两队分别有一根杆子。哪方有更多进攻者成功抵达敌方杆子,哪方就获得胜利。数量相等则无队胜利"); + } + virtual void initOptions() override + { + options.push_back("加入A队进攻B杆:-2,若己方胜利且存在神则+4"); + options.push_back("加入B队进攻A杆:-2,若己方胜利且存在神则+4"); + options.push_back("加入A队防守:拦截一名敌方的进攻者。若己方胜利,+2,否则-2"); + options.push_back("加入B队防守:拦截一名敌方的进攻者。若己方胜利,+2,否则-2"); + options.push_back("神:若无队胜利,+2,否则每有一位胜利方的进攻者抵达杆子扣1分"); + } + virtual void initExpects() override + { + expects.push_back("aabbcccdddeee"); + } + virtual void calc(vector& players) override + { + int n1 = max(static_cast(optionCount[0] - optionCount[3]), 0); + int n2 = max(static_cast(optionCount[1] - optionCount[2]), 0); + if (n1 > n2) + { + if (optionCount[4] > 0) tempScore[0] = 4; + else tempScore[0] = -2; + tempScore[1] = -2; + tempScore[2] = 2; + tempScore[3] = -2; + tempScore[4] = (-1)*n1; + } + else if (n1 < n2) + { + if (optionCount[4] > 0) tempScore[1] = 4; + else tempScore[1] = -2; + tempScore[0] = -2; + tempScore[3] = 2; + tempScore[4] = (-1)*n2; + } + else + { + tempScore[0] = -2; + tempScore[1] = -2; + tempScore[2] = -2; + tempScore[3] = -2; + tempScore[4] = 2; + } + } +}; + + +// 【新特性需修改】 +// *齐齐 +// 乌合之众 +// A.若你选择此项,则下一题得分+1 +// B.若你选择此项,则下一题失分不扣分 +// C.若你选择此项,则下一题得分翻倍 + /* diff --git a/games/crowd/problems_t.h b/games/crowd/problems_t.h new file mode 100644 index 00000000..6a0313bf --- /dev/null +++ b/games/crowd/problems_t.h @@ -0,0 +1,1083 @@ +// 39题囚徒困境修改前两版备份(目前废弃) +class Qt1 : public Question +{ +public: + Qt1() + { + id = 1; + author = "纤光"; + title = "囚徒困境第1版【废弃】"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择一项。"); + } + virtual void initOptions() override + { + options.push_back("认罪:-1,但若所有人都认罪,改为分数最高的人分数清零"); + options.push_back("不认罪:-2,但若所有人都不认罪,改为+2"); + } + virtual void initExpects() override + { + expects.push_back("ab"); + } + virtual void calc(vector& players) override + { + if (optionCount[0] == players.size()) { + const double max_score = MaxPlayerScore(players); + for (auto& player : players) { + if (player.score == max_score) { + player.score = 0; + } + } + } else if (optionCount[1] == players.size()) { + for (auto& player : players) { + player.score += 2; + } + } else { + tempScore[0] = -1; + tempScore[1] = -2; + } + } +}; + +class Qt2 : public Question +{ +public: + Qt2() + { + id = 2; + author = "纤光"; + title = "囚徒困境第2版【废弃】"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择一项。"); + } + virtual void initOptions() override + { + options.push_back("认罪:-1,结算完成后,若你的分数大于选此项的人数,则分数清零"); + options.push_back("不认罪:-2,结算完成后,若你的分数小于选此项的人数,则分数清零"); + } + virtual void initExpects() override + { + expects.push_back("ab"); + } + virtual void calc(vector& players) override + { + tempScore[0] = -1; + tempScore[1] = -2; + for (auto& player : players) { + if (player.select == 0 && player.score > optionCount[0]) { + player.score = 1; + } + if (player.select == 1 && player.score < optionCount[1]) { + player.score = 2; + } + } + } +}; + +class Qt3 : public Question +{ +public: + Qt3() + { + id = 3; + author = "大梦我先觉"; + title = "战场的厮杀【废弃】"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("在战场上,一念之差都会扭转战局。阵营AB对立,CD对立,各阵营人数多的一方获胜,胜利者可以获得 [失败一方人数/2] 的分数"); + } + virtual void initOptions() override + { + options.push_back("劫营"); + options.push_back("守营"); + options.push_back("抢粮"); + options.push_back("守粮"); + } + virtual void initExpects() override + { + expects.push_back("abcd"); + } + virtual void calc(vector& players) override + { + if (optionCount[0] > optionCount[1]) { + tempScore[0] = optionCount[1] / 2; + // tempScore[1] = -0.5; + } + if (optionCount[0] < optionCount[1]) { + tempScore[1] = optionCount[0] / 2; + // tempScore[0] = -0.5; + } + if (optionCount[2] > optionCount[3]) { + tempScore[2] = optionCount[3] / 2; + // tempScore[3] = -0.5; + } + if (optionCount[2] < optionCount[3]) { + tempScore[3] = optionCount[2] / 2; + // tempScore[2] = -0.5; + } + } +}; + +class Qt4 : public Question +{ +public: + Qt4() + { + id = 4; + author = "蔡徐坤"; + title = "鉴宝师和收藏家"; + } + + virtual void initTexts(vector& players) override + { + string half = ""; + if (playerNum > 5) { + half = "的 一半 "; + } + texts.push_back("将选择对应选项玩家数" + half + "分别记为 A B C D"); + } + virtual void initOptions() override + { + options.push_back("鉴定为假:+2D,若D>C,将所有选择A的玩家的积分平分给选择D的玩家,然后选A玩家分数清0"); + options.push_back("鉴定为真:若C>D,你-2,否则你+D"); + options.push_back("带来赝品:若B>A,你+2,否则你-A"); + options.push_back("带来真品:若A>B,你-2,否则你+B"); + } + virtual void initExpects() override + { + expects.push_back("abcd"); + } + virtual void calc(vector& players) override + { + vector pnum; + for (int i = 0; i < playerNum; i++) { + if (playerNum > 5) { + pnum.push_back(optionCount[i] / 2); + } else { + pnum.push_back(optionCount[i]); + } + } + tempScore[0] = 2 * pnum[3]; + if (pnum[3] > pnum[2]){ + double sum_score = 0; + for (int i = 0; i < playerNum; i++) { + if (players[i].select == 0) { + sum_score = sum_score + players[i].score + tempScore[0]; + players[i].score = 0; + } + } + tempScore[0] = 0; + tempScore[3] = sum_score / optionCount[3]; + } + if (pnum[2] > pnum[3]) { + tempScore[1] -= 2; + } else { + tempScore[1] += pnum[3]; + } + if (pnum[1] > pnum[0]) { + tempScore[2] += 2; + } else { + tempScore[2] -= pnum[0]; + } + if (pnum[0] > pnum[1]) { + tempScore[3] -= 2; + } else { + tempScore[3] += pnum[1]; + } + } +}; + +class Qt5 : public Question +{ +public: + Qt5() + { + id = 5; + author = "大梦我先觉"; + title = "焦灼的击剑【废弃】"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("a和b两人击剑时进行进攻和躲闪。a和b的最终行为分别由选择人数最多项确定(如果人数相等分数不变)。"); + texts.push_back("若两人均进攻,则选进攻者-2分;两人均躲闪,则选躲闪者-1分。若一人进攻一人躲闪,则进攻方选进攻者+2,躲闪方分数不变。"); + } + virtual void initOptions() override + { + options.push_back("a选择进攻"); + options.push_back("a选择躲闪"); + options.push_back("b选择进攻"); + options.push_back("b选择躲闪"); + } + virtual void initExpects() override + { + expects.push_back("abcd"); + } + virtual void calc(vector& players) override + { + int a_action = (optionCount[0] > optionCount[1])? 1 : (optionCount[1] > optionCount[0])? 0 : -1; + int b_action = (optionCount[2] > optionCount[3])? 1 : (optionCount[3] > optionCount[2])? 0 : -1; + if (a_action == b_action) { + if (a_action == 1) { + tempScore[0] = tempScore[2] = -2; + } else if (a_action == 0) { + tempScore[1] = tempScore[3] = -1; + } + } else { + // a无行动时,b攻击>躲闪加分,否则都不加 + if (a_action > b_action && a_action == 1) { + tempScore[0] = 2; + } + if (b_action > a_action && b_action == 1) { + tempScore[2] = 2; + } + } + } +}; + +class Qt6 : public Question +{ +public: + Qt6() + { + id = 6; + author = "剩菜剩饭"; + title = "HP杀"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择一项,若死亡则不得分,但仍执行攻击效果"); + } + virtual void initOptions() override + { + options.push_back("大狼:+1分,每有一只小狼+1分。若大狼只有 1 只,神职死亡"); + options.push_back("小狼:+1分,选择的人数比神职多时平民死亡,若大狼存活,额外+1分"); + options.push_back("神职:+1分,选择人数比小狼多时,大狼死亡,此项额外+1分"); + options.push_back("平民:+3分,若选择的人数比神职多,平民死亡"); + options.push_back("第三方:+0分,若狼人方和好人方都有阵亡消息,你+4分 "); + } + virtual void initExpects() override + { + expects.push_back("aabbbcccddde"); + } + virtual void calc(vector& players) override + { + bool A_die, B_die, C_die, D_die; + A_die = B_die = C_die = D_die = false; + tempScore[0] = 1 + optionCount[1]; + tempScore[1] = 1; + tempScore[2] = 1; + tempScore[3] = 3; + if (optionCount[0] == 1) { + C_die = true; + } + if (optionCount[1] > optionCount[2] || optionCount[3] > optionCount[2]) { + D_die = true; + } + if (optionCount[2] > optionCount[1]) { + A_die = true; + tempScore[2] += 1; + } + if (A_die) tempScore[0] = 0; else tempScore[1] += 1; + // if (B_die) tempScore[1] = 0; + if (C_die) tempScore[2] = 0; + if (D_die) tempScore[3] = 0; + if ((A_die || B_die) && (C_die || D_die)) { + tempScore[4] = 4; + } + } +}; + +class Qt7 : public Question +{ +public: + Qt7() + { + id = 7; + author = "An idle brain"; + title = "未命名"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择一项。"); + } + virtual void initOptions() override + { + options.push_back("+1,若有人选B,则改为获得 [D-B] 人数的分数"); + options.push_back("+1.5,若有人选C,则改为获得 [A-C] 人数的分数"); + options.push_back("+2,若有人选D,则改为获得 [B-D] 人数的分数"); + options.push_back("+2.5,若有人选A,则改为获得 [C-A] 人数的分数"); + } + virtual void initExpects() override + { + expects.push_back("abbcdd"); + } + virtual void calc(vector& players) override + { + tempScore[0] = 1; + if (optionCount[1] > 0) { + tempScore[0] = optionCount[3] - optionCount[1]; + } + tempScore[1] = 1.5; + if (optionCount[2] > 0) { + tempScore[1] = optionCount[0] - optionCount[2]; + } + tempScore[2] = 2; + if (optionCount[3] > 0) { + tempScore[2] = optionCount[1] - optionCount[3]; + } + tempScore[3] = 2.5; + if (optionCount[0] > 0) { + tempScore[3] = optionCount[2] - optionCount[0]; + } + } +}; + +class Qt8 : public Question +{ +public: + Qt8() + { + id = 8; + author = "圣墓上的倒吊人"; + title = "让步【废弃】"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("如果所有人都选择一个选项,那只执行它。否则执行被选的加分最少的选项,且选择该选项玩家+2,其他人-2。"); + } + virtual void initOptions() override + { + options.push_back("+0"); + options.push_back("+1"); + options.push_back("+2"); + options.push_back("+3"); + } + virtual void initExpects() override + { + expects.push_back("aaabbccd"); + } + virtual void calc(vector& players) override + { + int selected = 0; + bool add_point = true; + for (int i = 0; i < 4; i++) { + if (optionCount[i] > 0) { + if (add_point) { + tempScore[i] = i + 2; + add_point = false; + } else { + tempScore[i] = -2; + } + selected++; + } + } + if (selected == 1) { + for (int i = 0; i < 4; i++) { + if (optionCount[i] > 0) { + tempScore[i] -= 2; + } + } + } + } +}; + +class Qt9 : public Question +{ +public: + Qt9() + { + id = 9; + author = "Q群管家"; + title = "奇偶1【废弃】"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("对应选项选择人数奇数取反,偶数取正。"); + } + virtual void initOptions() override + { + options.push_back("+3"); + options.push_back("0"); + } + virtual void initExpects() override + { + expects.push_back("aaaab"); + } + virtual void calc(vector& players) override + { + if (int(optionCount[0]) % 2 == 1) { + tempScore[0] = -3; + } else { + tempScore[0] = 3; + } + } +}; + +class Qt10 : public Question +{ +public: + Qt10() + { + id = 10; + author = "Q群管家"; + title = "奇偶3【废弃】"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选项得分与选择人数之和若为奇数则得分取反。"); + } + virtual void initOptions() override + { + options.push_back("+1"); + options.push_back("+2"); + options.push_back("+3"); + } + virtual void initExpects() override + { + expects.push_back("aaabbc"); + } + virtual void calc(vector& players) override + { + for (int i = 0; i < 3; i++) { + if (int(optionCount[i] + i + 1) % 2 == 1) { + tempScore[i] = -(i + 1); + } else { + tempScore[i] = i + 1; + } + } + } +}; + +class Qt11 : public Question +{ +public: + Qt11() + { + id = 11; + author = "Q群管家"; + title = "奇偶4【废弃】"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("一张正面是+2,背面是-2的卡牌,初始正面向上,最终状态是本题得分,你选择:"); + } + virtual void initOptions() override + { + options.push_back("翻面,你加对应的分数"); + options.push_back("不翻面,你减对应的分数"); + } + virtual void initExpects() override + { + expects.push_back("aab"); + } + virtual void calc(vector& players) override + { + if (int(optionCount[0]) % 2 == 0) { + tempScore[0] = 2; + tempScore[1] = -2; + } else { + tempScore[0] = -2; + tempScore[1] = 2; + } + } +}; + +class Qt12 : public Question // [待修改]分数增减幅度太大 平衡 1 +{ +public: + Qt12() + { + id = 12; + author = "圣墓上的倒吊人"; + title = "资源配置"; + } + + virtual void initTexts(vector& players) override + { + vars["start"] = playerNum * 2; + vars["limit"] = int(playerNum * 3.2); + texts.push_back("桌上有 " + str(vars["start"]) + " 个资源,一资源一分,如果投入后总资源超过了 " + str(vars["limit"]) + " ,那获得(你的减分/所有玩家的减分)比例的总资源。如果投入后分数为负数,则选择无效。"); + } + virtual void initOptions() override + { + options.push_back("0"); + options.push_back("-1"); + options.push_back("-2"); + options.push_back("-4"); + options.push_back("-6"); + options.push_back("1.6%获得投入后总资源,否则-15"); + } + virtual void initExpects() override + { + expects.push_back("abbcccddddeef"); + } + virtual void calc(vector& players) override + { + int total = vars["start"]; + total = total + optionCount[1] * 1 + optionCount[2] * 2 + optionCount[3] * 4 + optionCount[4] * 6; + tempScore[1] = -1; + tempScore[2] = -2; + tempScore[3] = -4; + tempScore[4] = -6; + for (int i = 0; i < playerNum; i++) { + if (players[i].score < -tempScore[players[i].select]) { + total += tempScore[players[i].select]; + players[i].score -= tempScore[players[i].select]; + } + } + if (rand() % 1000 < 16) { + tempScore[5] = total; + } else { + if (total > vars["limit"]) { + for (int i = 0; i < playerNum; i++) { + if (players[i].score >= -tempScore[players[i].select]) { + players[i].score += -tempScore[players[i].select] / (total - vars["start"]) * total; + } + } + } + tempScore[5] = -15; + } + } +}; + +class Qt13 : public Question // [待修改]选项不平衡,选D居多 +{ +public: + Qt13() + { + id = 13; + author = "飘渺"; + title = "电车难题"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("请选择你的位置。拉杆只有2个状态,即拉动偶数次会还原拉杆"); + } + virtual void initOptions() override + { + options.push_back("乘客:+0.5,但每有一个人拉动拉杆-0.5"); + options.push_back("多数派:+0.5,如果拉杆最终没有拉动,改为-2"); + options.push_back("少数派:+0.5,如果拉杆最终拉动,改为-2.5"); + options.push_back("-0.5,每一个选择此项的人都会拉动一次拉杆"); + } + virtual void initExpects() override + { + expects.push_back("abbcdddd"); + } + virtual void calc(vector& players) override + { + tempScore[0] = 0.5 - optionCount[3] * 0.5; + tempScore[1] = 0.5; + if (int(optionCount[3]) % 2 == 0) { + tempScore[1] = -2; + } + tempScore[2] = 0.5; + if (int(optionCount[3]) % 2 == 1) { + tempScore[2] = -2.5; + } + tempScore[3] = -0.5; + } +}; + +class Qt14 : public Question // [待修改]分数增减幅度太大 +{ +public: + Qt14() + { + id = 14; + author = "Q群管家"; + title = "均分1"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择一项。"); + } + virtual void initOptions() override + { + options.push_back("当前总积分×0.5"); + options.push_back("选择此选项的玩家均分他们的分数"); + options.push_back("当前总积分×(-0.5)"); + } + virtual void initExpects() override + { + expects.push_back("abbbbbc"); + } + virtual void calc(vector& players) override + { + double sum = 0; + for (int i = 0; i < playerNum; i++) { + if (players[i].select == 1) { + sum += players[i].score; + } + } + for (int i = 0; i < playerNum; i++) { + if (players[i].select == 0) { + players[i].score = players[i].score * 0.5; + } + if (players[i].select == 1) { + players[i].score = sum / optionCount[1]; + } + if (players[i].select == 2) { + players[i].score = -players[i].score * 0.5; + } + } + } +}; + +class Qt15 : public Question // [待修改]选择性问题,大部分情况玩家可能单选B +{ +public: + Qt15() + { + id = 15; + author = "Q群管家"; + title = "均分2"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择一项。"); + } + virtual void initOptions() override + { + options.push_back("选择此选项的分数最高与最低的玩家均分他们的分数"); + options.push_back("正分的-2,负分的+2"); + } + virtual void initExpects() override + { + expects.push_back("aaabb"); + } + virtual void calc(vector& players) override + { + double maxS = -99999; + double minS = 99999; + for(int i = 0; i < playerNum; i++) + { + if (players[i].select == 0) { + maxS = max(maxS, players[i].score); + minS = min(minS, players[i].score); + } + } + double sum = 0; + int count = 0; + for(int i = 0; i < playerNum; i++) + { + if (players[i].select == 0 && (players[i].score == minS || players[i].score == maxS)) { + sum += players[i].score; + count++; + } + } + for (int i = 0; i < playerNum; i++) { + if (players[i].select == 0 && (players[i].score == minS || players[i].score == maxS)) { + players[i].score = sum / count; + } + if (players[i].select == 1) { + if (players[i].score > 0) { + players[i].score -= 2; + } + if (players[i].score < 0) { + players[i].score += 2; + } + } + } + } +}; + +class Qt16 : public Question // [测试]奇偶存在较大的运气因素,多人时分数增减幅度太大 +{ +public: + Qt16() + { + id = 16; + author = "Q群管家"; + title = "奇偶5"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择一项。"); + } + virtual void initOptions() override + { + options.push_back("若选择人数为奇数,扣除 [选择本选项人数/2] 的分数;否则获得相应的分数"); + options.push_back("0"); + } + virtual void initExpects() override + { + expects.push_back("aab"); + } + virtual void calc(vector& players) override + { + tempScore[0] = int(optionCount[0]) % 2 == 1 ? -optionCount[0] / 2 : optionCount[0] / 2; + } +}; + +class Qt17 : public Question // [待修改]分数增减幅度太大 +{ +public: + Qt17() + { + id = 17; + author = "胡扯弟"; + title = "云顶之巢"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择你的云巢身份。"); + } + virtual void initOptions() override + { + options.push_back("YAMI:总是能+2分的大佬,但是有5%的可能性失手-1"); + options.push_back("飘渺:10%+24的赌博爱好者"); + options.push_back("黑桃3:+13,但每有一个飘渺-1.75分"); + options.push_back("飞机:+12,但有YAMI就会被gank"); + options.push_back("西东:+A+B-C+D"); + options.push_back("胡扯:-114514,但有2.5%的可能性改为+1919810"); + } + virtual void initExpects() override + { + expects.push_back("aaaabbbccdeeeef"); + } + virtual void calc(vector& players) override + { + tempScore[0] = rand() % 100 < 5 ? -1 : 2; + tempScore[1] = rand() % 100 < 10 ? 24 : 0; + tempScore[2] = 13 - optionCount[1] * 1.75; + tempScore[3] = optionCount[0] == 0 ? 12 : 0; + tempScore[4] = optionCount[0] + optionCount[1] - optionCount[2] + optionCount[3]; + tempScore[5] = rand() % 1000 < 25 ? 1919810 : -114514; + } +}; + +class Qt18 : public Question // [待修改]触发E选项取反概率太高 +{ +public: + Qt18() + { + id = 18; + author = "圣墓上的倒吊人"; + title = "未来计划"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("(X)代表有且只有 X 人选择该选项时,该选项才能执行。执行顺序为序号顺序。无选项执行时执行E"); + } + virtual void initOptions() override + { + options.push_back("+4(1)"); + options.push_back("+2(2)"); + options.push_back("-3(3)"); + options.push_back("-1(≥2)"); + options.push_back("所有玩家的分数变为相反数(4)"); + } + virtual void initExpects() override + { + expects.push_back("abbbcdee"); + } + virtual void calc(vector& players) override + { + for(int i = 0; i < playerNum; i++) { + if (players[i].select == 0 && optionCount[0] == 1) { + players[i].score += 4; + } + if (players[i].select == 1 && optionCount[1] == 2) { + players[i].score += 2; + } + if (players[i].select == 2 && optionCount[2] == 3) { + players[i].score -= 3; + } + if (players[i].select == 3 && optionCount[3] >= 2) { + players[i].score -= 1; + } + } + if ((optionCount[0] != 1 && optionCount[1] != 2 && optionCount[2] != 3 && optionCount[3] < 2) || optionCount[4] == 4) { + for(int i = 0; i < playerNum; i++) { + players[i].score = -players[i].score; + } + } + } +}; + +class Qt19 : public Question // [待修改]人多时分数增减幅度太大 +{ +public: + Qt19() + { + id = 19; + author = "圣墓上的倒吊人"; + title = "真理"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("真理掌握在?"); + } + virtual void initOptions() override + { + options.push_back("少数人:+3,如果此项选择人数最多,则改为减 [选择该项人数] 的分数"); + options.push_back("多数人:-2"); + } + virtual void initExpects() override + { + expects.push_back("aabbb"); + } + virtual void calc(vector& players) override + { + tempScore[0] = optionCount[0] == maxSelect ? -optionCount[0] : 3; + tempScore[1] = -2; + } +}; + +class Qt20 : public Question // [待修改]分数增减幅度太大 +{ +public: + Qt20() + { + id = 20; + author = "丘陵萨满"; + title = "伊甸园"; + } + + virtual void initTexts(vector& players) override + { + vector tmp_score; + for (int i = 0; i < playerNum; i++) { + tmp_score.push_back(players[i].score); + } + sort(tmp_score.begin(),tmp_score.end()); + vars["score"] = tmp_score[2]; + texts.push_back("获胜的选项获得 [选择非此选项人数] 的分数,失败的选项失去 [选择获胜选项人数] 的分数"); + } + virtual void initOptions() override + { + options.push_back("红苹果:如果分数小于等于 " + str(vars["score"]) + " 的玩家都选择红苹果,红苹果获胜。但如果所有人都选择此项,全员分数取反"); + options.push_back("银苹果:如果选择此项的人数少于 C,且红苹果没有获胜,银苹果获胜。"); + options.push_back("金苹果:如果选择此项的人数少于等于 B,且红苹果没有获胜,金苹果获胜。"); + } + virtual void initExpects() override + { + expects.push_back("aaaabc"); + } + virtual void calc(vector& players) override + { + int win, l1, l2; + bool red_win = true; + bool is_invert = false; + win = l1 = l2 = 0; + for (int i = 0; i < playerNum; i++) { + if (players[i].score <= vars["score"] && players[i].select != 0) { + red_win = false; break; + } + } + if (red_win) { + if (optionCount[0] == playerNum) { + is_invert = true; + for (int i = 0; i < playerNum; i++) { + players[i].score = -players[i].score; + } + } else { + win = 0; + l1 = 1; l2 = 2; + } + } else { + if (optionCount[1] < optionCount[2]) { + win = 1; + l1 = 0; l2 = 2; + } else { + win = 2; + l1 = 0; l2 = 1; + } + } + if (!is_invert) { + tempScore[win] = optionCount[l1] + optionCount[l2]; + tempScore[l1] = -optionCount[win]; + tempScore[l2] = -optionCount[win]; + } + } +}; + +class Qt21 : public Question // [待修改]分数增减幅度太大,A选项风险太高 +{ +public: + Qt21() + { + id = 21; + author = "圣墓上的倒吊人"; + title = "战争中的贵族"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("你的国家与敌国正在战争中,选择一项行动。"); + } + virtual void initOptions() override + { + options.push_back("继续抵抗:+5,如果有玩家选择了B,那选择此项的人分数归零"); + options.push_back("有条件投降:-3"); + options.push_back("润出国外:如果有人选 A,则-2"); + } + virtual void initExpects() override + { + expects.push_back("aabccccc"); + } + virtual void calc(vector& players) override + { + if (optionCount[1] == 0) { + tempScore[0] = 5; + } else { + for (int i = 0; i < playerNum; i++) { + if (players[i].select == 0) { + players[i].score = 0; + } + } + } + tempScore[1] = -3; + if (optionCount[0] > 0) { + tempScore[2] = -2; + } + } +}; + +class Qt22: public Question // [待修改]多人时分数增减幅度太大 +{ +public: + Qt22() + { + id = 22; + author = "飞机鸭卖蛋"; + title = "太多挂机A了…?"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("分别记选择A、B、C选项的人数为a、b、c:"); + } + virtual void initOptions() override + { + options.push_back("获得 a 分"); + options.push_back("获得 a×1.5-b 分"); + options.push_back("获得 a×2-c 分"); + } + virtual void initExpects() override + { + expects.push_back("aaaabbccc"); + } + virtual void calc(vector& players) override + { + tempScore[0] = optionCount[0]; + tempScore[1] = optionCount[0] * 1.5 - optionCount[1]; + tempScore[2] = optionCount[0] * 2 - optionCount[2]; + } +}; + +class Qt23 : public Question // [待修改]人多时分数增减幅度太大 +{ +public: + Qt23() + { + id = 23; + author = "圣墓上的倒吊人"; + title = "税收"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择一项。"); + } + virtual void initOptions() override + { + options.push_back("减45%分数"); + options.push_back("减 [B选项人数] 的分数"); + options.push_back("减 [C选项人数*1.5] 的分数"); + } + virtual void initExpects() override + { + expects.push_back("abbbcc"); + } + virtual void calc(vector& players) override + { + for (int i = 0; i < playerNum; i++) { + if (players[i].select == 0) { + if (players[i].score > 0) { + players[i].score -= players[i].score * 0.45; + } else { + players[i].score += players[i].score * 0.45; + } + } + } + tempScore[1] = -optionCount[1]; + tempScore[2] = -optionCount[2] * 1.5; + } +}; + +class Qt24 : public Question // [测试]随机性太高 +{ +public: + Qt24() + { + id = 24; + author = "Chance"; + title = "我是大土块"; + } + + virtual void initTexts(vector& players) override + { + texts.push_back("选择一项。"); + } + virtual void initOptions() override + { + if (playerNum > 8) { + vars["percent"] = 10; + } else { + vars["percent"] = 16; + } + options.push_back("+2,有 [(B+C) ×" + str(vars["percent"]) + "]% 的概率-4"); + options.push_back("+1,有 [(A-B) ×" + str(vars["percent"]) + "]% 的概率使C+2"); + options.push_back("-2,有 [(A+D) ×" + str(vars["percent"]) + "]% 的概率+6"); + options.push_back("-3,有 [(A+B-D) ×" + str(vars["percent"]) + "]% 的概率使A和B-4,D+5"); + } + virtual void initExpects() override + { + expects.push_back("abcd"); + } + virtual void calc(vector& players) override + { + tempScore[0] = 2; + tempScore[1] = 1; + tempScore[2] = -2; + tempScore[3] = -3; + if (rand() % 100 < (optionCount[1] + optionCount[2]) * vars["percent"]) { + tempScore[0] -= 4; + } + if (rand() % 100 < (optionCount[0] - optionCount[1]) * vars["percent"]) { + tempScore[2] += 2; + } + if (rand() % 100 < (optionCount[0] + optionCount[3]) * vars["percent"]) { + tempScore[2] += 6; + } + if (rand() % 100 < (optionCount[0] + optionCount[1] - optionCount[3]) * vars["percent"]) { + tempScore[0] -= 4; + tempScore[1] -= 4; + tempScore[2] += 5; + } + } +}; \ No newline at end of file diff --git a/games/crowd/resource/exportAllQuestions.cpp b/games/crowd/resource/exportAllQuestions.cpp index f05c069a..aadb213d 100644 --- a/games/crowd/resource/exportAllQuestions.cpp +++ b/games/crowd/resource/exportAllQuestions.cpp @@ -5,13 +5,16 @@ #include #include "../problems.h" +#include "../problems_t.h" using namespace std; // formal questions -constexpr static uint32_t k_question_num = 72; -// with test questions -constexpr static uint32_t all_question_num = 113; +constexpr static uint32_t k_question_num = 78; +// with test1 questions +constexpr static uint32_t all_question_num = 132; +// test2 questions +constexpr static uint32_t t_question_num = 24; static const std::array create_question{ @@ -87,13 +90,13 @@ static const std::array create_question{ []() -> Question* { return new Q70(); }, []() -> Question* { return new Q71(); }, []() -> Question* { return new Q72(); }, - // test questions []() -> Question* { return new Q73(); }, []() -> Question* { return new Q74(); }, []() -> Question* { return new Q75(); }, []() -> Question* { return new Q76(); }, []() -> Question* { return new Q77(); }, []() -> Question* { return new Q78(); }, + // test questions []() -> Question* { return new Q79(); }, []() -> Question* { return new Q80(); }, []() -> Question* { return new Q81(); }, @@ -129,25 +132,79 @@ static const std::array create_question{ []() -> Question* { return new Q111(); }, []() -> Question* { return new Q112(); }, []() -> Question* { return new Q113(); }, + []() -> Question* { return new Q114(); }, + []() -> Question* { return new Q115(); }, + []() -> Question* { return new Q116(); }, + []() -> Question* { return new Q117(); }, + []() -> Question* { return new Q118(); }, + []() -> Question* { return new Q119(); }, + []() -> Question* { return new Q120(); }, + []() -> Question* { return new Q121(); }, + []() -> Question* { return new Q122(); }, + []() -> Question* { return new Q123(); }, + []() -> Question* { return new Q124(); }, + []() -> Question* { return new Q125(); }, + []() -> Question* { return new Q126(); }, + []() -> Question* { return new Q127(); }, + []() -> Question* { return new Q128(); }, + []() -> Question* { return new Q129(); }, + []() -> Question* { return new Q130(); }, + []() -> Question* { return new Q131(); }, + []() -> Question* { return new Q132(); }, +}; + +// test mode 2 +static const std::array test_question{ + []() -> Question* { return new Qt1(); }, + []() -> Question* { return new Qt2(); }, + []() -> Question* { return new Qt3(); }, + []() -> Question* { return new Qt4(); }, + []() -> Question* { return new Qt5(); }, + []() -> Question* { return new Qt6(); }, + []() -> Question* { return new Qt7(); }, + []() -> Question* { return new Qt8(); }, + []() -> Question* { return new Qt9(); }, + []() -> Question* { return new Qt10(); }, + []() -> Question* { return new Qt11(); }, + []() -> Question* { return new Qt12(); }, + []() -> Question* { return new Qt13(); }, + []() -> Question* { return new Qt14(); }, + []() -> Question* { return new Qt15(); }, + []() -> Question* { return new Qt16(); }, + []() -> Question* { return new Qt17(); }, + []() -> Question* { return new Qt18(); }, + []() -> Question* { return new Qt19(); }, + []() -> Question* { return new Qt20(); }, + []() -> Question* { return new Qt21(); }, + []() -> Question* { return new Qt22(); }, + []() -> Question* { return new Qt23(); }, + []() -> Question* { return new Qt24(); }, }; -string init_question(int id) +string init_question(const int id, const int mode) { vector players; Player tempP; for (int i = 0; i < 10; i++) { players.push_back(tempP); } - Question* question = create_question[id - 1](); + Question* question; + if (mode != 2) { + question = create_question[id - 1](); + } else { + question = test_question[id - 1](); + } question -> init(players); question -> initTexts(players); question -> initOptions(); thread_local static string str; str = ""; - str += "题号:#" + to_string(question -> id) + "\n"; + str += string("题号:#") + (mode == 2 ? "t" : "") + to_string(question -> id) + "\n"; str += "出题者:" + (question -> author) + "\n"; - if (id > k_question_num) { + if (mode == 1) { str += "[测试] "; + } else if (mode == 2) { + str += "[其他/废弃] "; } str += "题目:" + (question -> title) + "\n\n"; str += question -> String(); @@ -165,7 +222,14 @@ string export_questions() questions_str += "\n++下方为测试题库,平衡性尚未测试完全,不会出现在正式游戏中++"; questions_str += "\n**********************************************************************\n\n"; } - questions_str += init_question(i); + questions_str += init_question(i, i > k_question_num); + } + questions_str += "\n**********************************************************************"; + questions_str += "\n++下方为其他/废弃题库,可能不再考虑修改或更新,不会出现在正式游戏中++"; + questions_str += "\n**********************************************************************\n\n"; + for (int i = 1; i <= t_question_num; i++) { + questions_str += "------------------------------------------------------------\n"; + questions_str += init_question(i, 2); } return questions_str; } @@ -178,12 +242,12 @@ int main() p = localtime(&timep); char file[50]; - sprintf(file, "乌合之众题库_%d.%d.%d_%d-%d.txt", 1900 + p->tm_year, 1+ p->tm_mon, p->tm_mday, k_question_num, all_question_num); + sprintf(file, "乌合之众题库_%d.%d.%d_%d-%d-%d.txt", 1900 + p->tm_year, 1+ p->tm_mon, p->tm_mday, k_question_num, all_question_num, t_question_num); freopen(file, "w", stdout); cout << "乌合之众题库" << endl << endl; cout << "本记录导出时间:" << ctime(&timep) << endl; - cout << "总题目数:" << all_question_num << endl; + cout << "总题目数:" << (all_question_num + t_question_num) << "(包含 " << t_question_num << " 个废弃题目)" << endl; cout << "正式题库题目数:" << k_question_num << endl << endl; cout << "注:部分题目数字会根据参与游戏的人数和当前分数发生变化,此展示题库默认10人参与游戏且所有人分数为0" << endl << endl; diff --git a/games/crowd/resource/in.txt b/games/crowd/resource/in.txt index f456fa99..7da1a6f8 100644 --- a/games/crowd/resource/in.txt +++ b/games/crowd/resource/in.txt @@ -287,8 +287,36 @@ 71 1 6 4 0 0 1 2 4 4 100 100 400 300 0 0 71 1 6 4 0 0 3 3 4 4 0 0 -100 -100 100 100 71 1 6 4 0 0 1 1 3 4 0 0 -100 -100 200 100 -71 1 6 4 0 2 3 3 4 4 0 0 -100 -100 100 100 +71 1 6 4 0 2 3 3 4 4 0 300 -100 -100 100 100 72 1 6 4 0 0 1 1 3 4 0 0 200 200 200 -100 72 1 6 4 0 0 1 1 2 3 200 200 0 0 400 100 -72 1 6 4 0 1 1 2 3 4 0 200 200 -200 100 200 \ No newline at end of file +72 1 6 4 0 1 1 2 3 4 0 200 200 -200 100 200 + +73 1 5 4 0 0 1 2 3 100 100 200 -300 0 +73 1 5 4 0 1 1 2 3 100 -200 -200 400 100 +73 1 5 4 0 0 1 1 2 100 100 200 200 -300 + +74 1 6 4 0 1 1 1 2 3 -100 -200 -200 -200 700 300 +74 1 6 4 0 1 2 2 3 3 -100 0 50 50 -300 -300 +74 1 6 4 1 2 2 2 2 3 0 -300 -300 -300 -300 300 + +75 0 4 3 200 100 300 +75 1 4 3 0 0 2 2 200 200 -100 -100 +75 1 4 3 0 1 2 2 0 100 -100 -100 +75 1 4 3 1 1 2 2 100 100 300 300 + +76 1 5 4 0 0 0 1 2 0 0 0 400 300 +76 1 5 4 0 0 1 1 2 0 0 -100 -100 300 +76 1 5 4 0 0 1 2 3 0 0 -100 300 200 +76 1 5 4 1 2 3 3 3 -100 -200 -300 -300 -300 + +77 1 5 4 0 1 1 2 3 300 100 100 -100 200 +77 1 5 4 1 1 2 2 3 0 0 400 400 0 +77 1 5 4 0 0 1 1 3 0 0 100 100 0 + +78 1 6 5 0 2 2 2 3 4 300 0 0 0 500 500 +78 1 6 5 0 1 2 3 3 4 300 150 100 0 0 0 +78 1 6 5 0 0 1 2 2 4 300 300 150 0 0 0 +78 1 6 5 0 0 0 3 3 4 0 0 0 250 250 500 +78 1 6 5 0 0 0 2 2 4 0 0 0 200 200 500 \ No newline at end of file diff --git a/games/crowd/resource/removed_questions.cpp b/games/crowd/resource/removed_questions.cpp deleted file mode 100644 index 2f28fd5a..00000000 --- a/games/crowd/resource/removed_questions.cpp +++ /dev/null @@ -1,497 +0,0 @@ -// 39题囚徒困境修改前两版备份(目前已移除) -class Q39 : public Question -{ -public: - Q39() - { - id = 39; - author = "纤光"; - title = "囚徒困境"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("选择一项。"); - } - virtual void initOptions() override - { - options.push_back("认罪:-1,但若所有人都认罪,改为分数最高的人分数清零"); - options.push_back("不认罪:-2,但若所有人都不认罪,改为+2"); - } - virtual void initExpects() override - { - expects.push_back("ab"); - } - virtual void calc(vector& players) override - { - if (optionCount[0] == players.size()) { - const double max_score = MaxPlayerScore(players); - for (auto& player : players) { - if (player.score == max_score) { - player.score = 0; - } - } - } else if (optionCount[1] == players.size()) { - for (auto& player : players) { - player.score += 2; - } - } else { - tempScore[0] = -1; - tempScore[1] = -2; - } - } -}; -class Q39 : public Question -{ -public: - Q39() - { - id = 39; - author = "纤光"; - title = "囚徒困境"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("选择一项。"); - } - virtual void initOptions() override - { - options.push_back("认罪:-1,结算完成后,若你的分数大于选此项的人数,则分数清零"); - options.push_back("不认罪:-2,结算完成后,若你的分数小于选此项的人数,则分数清零"); - } - virtual void initExpects() override - { - expects.push_back("ab"); - } - virtual void calc(vector& players) override - { - tempScore[0] = -1; - tempScore[1] = -2; - for (auto& player : players) { - if (player.select == 0 && player.score > optionCount[0]) { - player.score = 1; - } - if (player.select == 1 && player.score < optionCount[1]) { - player.score = 2; - } - } - } -}; - -class Qrm01 : public Question -{ -public: - Q45() - { - id = 45; - author = "大梦我先觉"; - title = "战场的厮杀"; - } - - virtual void initTexts() override - { - texts.push_back("在战场上,一念之差都会扭转战局。阵营AB对立,CD对立,各阵营人数多的一方获胜,胜利者可以获得 [失败一方人数/2] 的分数"); - } - virtual void initOptions() override - { - options.push_back("劫营"); - options.push_back("守营"); - options.push_back("抢粮"); - options.push_back("守粮"); - } - virtual void initExpects() override - { - expects.push_back("abcd"); - } - virtual void calc(vector& players) override - { - if (optionCount[0] > optionCount[1]) { - tempScore[0] = optionCount[1] / 2; - // tempScore[1] = -0.5; - } - if (optionCount[0] < optionCount[1]) { - tempScore[1] = optionCount[0] / 2; - // tempScore[0] = -0.5; - } - if (optionCount[2] > optionCount[3]) { - tempScore[2] = optionCount[3] / 2; - // tempScore[3] = -0.5; - } - if (optionCount[2] < optionCount[3]) { - tempScore[3] = optionCount[2] / 2; - // tempScore[2] = -0.5; - } - } -}; - -class Qrm02 : public Question -{ -public: - Q58() - { - id = 58; - author = "蔡徐坤"; - title = "鉴宝师和收藏家"; - } - - virtual void initTexts(vector& players) override - { - string half = ""; - if (playerNum > 5) { - half = "的 一半 "; - } - texts.push_back("将选择对应选项玩家数" + half + "分别记为 A B C D"); - } - virtual void initOptions() override - { - options.push_back("鉴定为假:+2D,若D>C,将所有选择A的玩家的积分平分给选择D的玩家,然后选A玩家分数清0"); - options.push_back("鉴定为真:若C>D,你-2,否则你+D"); - options.push_back("带来赝品:若B>A,你+2,否则你-A"); - options.push_back("带来真品:若A>B,你-2,否则你+B"); - } - virtual void initExpects() override - { - expects.push_back("abcd"); - } - virtual void calc(vector& players) override - { - vector pnum; - for (int i = 0; i < playerNum; i++) { - if (playerNum > 5) { - pnum.push_back(optionCount[i] / 2); - } else { - pnum.push_back(optionCount[i]); - } - } - tempScore[0] = 2 * pnum[3]; - if (pnum[3] > pnum[2]){ - double sum_score = 0; - for (int i = 0; i < playerNum; i++) { - if (players[i].select == 0) { - sum_score = sum_score + players[i].score + tempScore[0]; - players[i].score = 0; - } - } - tempScore[0] = 0; - tempScore[3] = sum_score / optionCount[3]; - } - if (pnum[2] > pnum[3]) { - tempScore[1] -= 2; - } else { - tempScore[1] += pnum[3]; - } - if (pnum[1] > pnum[0]) { - tempScore[2] += 2; - } else { - tempScore[2] -= pnum[0]; - } - if (pnum[0] > pnum[1]) { - tempScore[3] -= 2; - } else { - tempScore[3] += pnum[1]; - } - } -}; - -class Qrm03 : public Question -{ -public: - Q61() - { - id = 61; - author = "大梦我先觉"; - title = "焦灼的击剑"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("a和b两人击剑时进行进攻和躲闪。a和b的最终行为分别由选择人数最多项确定(如果人数相等分数不变)。"); - texts.push_back("若两人均进攻,则选进攻者-2分;两人均躲闪,则选躲闪者-1分。若一人进攻一人躲闪,则进攻方选进攻者+2,躲闪方分数不变。"); - } - virtual void initOptions() override - { - options.push_back("a选择进攻"); - options.push_back("a选择躲闪"); - options.push_back("b选择进攻"); - options.push_back("b选择躲闪"); - } - virtual void initExpects() override - { - expects.push_back("abcd"); - } - virtual void calc(vector& players) override - { - int a_action = (optionCount[0] > optionCount[1])? 1 : (optionCount[1] > optionCount[0])? 0 : -1; - int b_action = (optionCount[2] > optionCount[3])? 1 : (optionCount[3] > optionCount[2])? 0 : -1; - if (a_action == b_action) { - if (a_action == 1) { - tempScore[0] = tempScore[2] = -2; - } else if (a_action == 0) { - tempScore[1] = tempScore[3] = -1; - } - } else { - // a无行动时,b攻击>躲闪加分,否则都不加 - if (a_action > b_action && a_action == 1) { - tempScore[0] = 2; - } - if (b_action > a_action && b_action == 1) { - tempScore[2] = 2; - } - } - } -}; - -class Qrm04 : public Question -{ -public: - Q62() - { - id = 62; - author = "剩菜剩饭"; - title = "HP杀"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("选择一项,若死亡则不得分,但仍执行攻击效果"); - } - virtual void initOptions() override - { - options.push_back("大狼:+1分,每有一只小狼+1分。若大狼只有 1 只,神职死亡"); - options.push_back("小狼:+1分,选择的人数比神职多时平民死亡,若大狼存活,额外+1分"); - options.push_back("神职:+1分,选择人数比小狼多时,大狼死亡,此项额外+1分"); - options.push_back("平民:+3分,若选择的人数比神职多,平民死亡"); - options.push_back("第三方:+0分,若狼人方和好人方都有阵亡消息,你+4分 "); - } - virtual void initExpects() override - { - expects.push_back("aabbbcccddde"); - } - virtual void calc(vector& players) override - { - bool A_die, B_die, C_die, D_die; - A_die = B_die = C_die = D_die = false; - tempScore[0] = 1 + optionCount[1]; - tempScore[1] = 1; - tempScore[2] = 1; - tempScore[3] = 3; - if (optionCount[0] == 1) { - C_die = true; - } - if (optionCount[1] > optionCount[2] || optionCount[3] > optionCount[2]) { - D_die = true; - } - if (optionCount[2] > optionCount[1]) { - A_die = true; - tempScore[2] += 1; - } - if (A_die) tempScore[0] = 0; else tempScore[1] += 1; - // if (B_die) tempScore[1] = 0; - if (C_die) tempScore[2] = 0; - if (D_die) tempScore[3] = 0; - if ((A_die || B_die) && (C_die || D_die)) { - tempScore[4] = 4; - } - } -}; - -class Qrm05 : public Question -{ -public: - Q64() - { - id = 64; - author = "An idle brain"; - title = "未命名"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("选择一项。"); - } - virtual void initOptions() override - { - options.push_back("+1,若有人选B,则改为获得 [D-B] 人数的分数"); - options.push_back("+1.5,若有人选C,则改为获得 [A-C] 人数的分数"); - options.push_back("+2,若有人选D,则改为获得 [B-D] 人数的分数"); - options.push_back("+2.5,若有人选A,则改为获得 [C-A] 人数的分数"); - } - virtual void initExpects() override - { - expects.push_back("abbcdd"); - } - virtual void calc(vector& players) override - { - tempScore[0] = 1; - if (optionCount[1] > 0) { - tempScore[0] = optionCount[3] - optionCount[1]; - } - tempScore[1] = 1.5; - if (optionCount[2] > 0) { - tempScore[1] = optionCount[0] - optionCount[2]; - } - tempScore[2] = 2; - if (optionCount[3] > 0) { - tempScore[2] = optionCount[1] - optionCount[3]; - } - tempScore[3] = 2.5; - if (optionCount[0] > 0) { - tempScore[3] = optionCount[2] - optionCount[0]; - } - } -}; - -class Qrm06 : public Question -{ -public: - Q72() - { - id = 72; - author = "圣墓上的倒吊人"; - title = "让步"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("如果所有人都选择一个选项,那只执行它。否则执行被选的加分最少的选项,且选择该选项玩家+2,其他人-2。"); - } - virtual void initOptions() override - { - options.push_back("+0"); - options.push_back("+1"); - options.push_back("+2"); - options.push_back("+3"); - } - virtual void initExpects() override - { - expects.push_back("aaabbccd"); - } - virtual void calc(vector& players) override - { - int selected = 0; - bool add_point = true; - for (int i = 0; i < 4; i++) { - if (optionCount[i] > 0) { - if (add_point) { - tempScore[i] = i + 2; - add_point = false; - } else { - tempScore[i] = -2; - } - selected++; - } - } - if (selected == 1) { - for (int i = 0; i < 4; i++) { - if (optionCount[i] > 0) { - tempScore[i] -= 2; - } - } - } - } -}; - -class Qrm07 : public Question -{ -public: - Q81() - { - id = 81; - author = "Q群管家"; - title = "奇偶1"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("对应选项选择人数奇数取反,偶数取正。"); - } - virtual void initOptions() override - { - options.push_back("+3"); - options.push_back("0"); - } - virtual void initExpects() override - { - expects.push_back("aaaab"); - } - virtual void calc(vector& players) override - { - if (int(optionCount[0]) % 2 == 1) { - tempScore[0] = -3; - } else { - tempScore[0] = 3; - } - } -}; - -class Qrm08 : public Question -{ -public: - Q83() - { - id = 83; - author = "Q群管家"; - title = "奇偶3"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("选项得分与选择人数之和若为奇数则得分取反。"); - } - virtual void initOptions() override - { - options.push_back("+1"); - options.push_back("+2"); - options.push_back("+3"); - } - virtual void initExpects() override - { - expects.push_back("aaabbc"); - } - virtual void calc(vector& players) override - { - for (int i = 0; i < 3; i++) { - if (int(optionCount[i] + i + 1) % 2 == 1) { - tempScore[i] = -(i + 1); - } else { - tempScore[i] = i + 1; - } - } - } -}; - -class Qrm09 : public Question -{ -public: - Q84() - { - id = 84; - author = "Q群管家"; - title = "奇偶4"; - } - - virtual void initTexts(vector& players) override - { - texts.push_back("一张正面是+2,背面是-2的卡牌,初始正面向上,最终状态是本题得分,你选择:"); - } - virtual void initOptions() override - { - options.push_back("翻面,你加对应的分数"); - options.push_back("不翻面,你减对应的分数"); - } - virtual void initExpects() override - { - expects.push_back("aab"); - } - virtual void calc(vector& players) override - { - if (int(optionCount[0]) % 2 == 0) { - tempScore[0] = 2; - tempScore[1] = -2; - } else { - tempScore[0] = -2; - tempScore[1] = 2; - } - } -}; \ No newline at end of file diff --git a/games/crowd/resource/test_37.txt b/games/crowd/resource/test_37.txt index 4bd06a6f..dabddc67 100644 --- a/games/crowd/resource/test_37.txt +++ b/games/crowd/resource/test_37.txt @@ -10,7 +10,7 @@ GAME_TEST(5, Q37_Basic_ADDDD_AABDD){ ASSERT_PRI_MSG(OK, 2, "B"); ASSERT_PRI_MSG(OK, 3, "D"); ASSERT_PRI_MSG(CHECKOUT, 4, "D"); - ASSERT_SCORE(50,0,50,100,100);} + ASSERT_SCORE(150,100,50,100,100);} GAME_TEST(5, Q37_Basic_CDDDD_AAAAB_AAABB){ ASSERT_PUB_MSG(OK, 0, "回合数 3"); ASSERT_PUB_MSG(OK, 0, "测试 37"); StartGame(); diff --git a/games/crowd/resource/test_questions_local.cpp b/games/crowd/resource/test_questions_local.cpp new file mode 100644 index 00000000..ecce60f7 --- /dev/null +++ b/games/crowd/resource/test_questions_local.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include +#include +#include + +#include "problems.h" // 此文件应放在一个文件夹内 + +REGISTER_QUESTION(125, Q125) +REGISTER_QUESTION(126, Q126) +REGISTER_QUESTION(127, Q127) +REGISTER_QUESTION(128, Q128) +REGISTER_QUESTION(129, Q129) +REGISTER_QUESTION(130, Q130) + +int getRandomChoiceBasedOnExpect(const std::string& expect) { + //std::cout << expect << std::endl; + int k = std::rand(); + int n = expect.size(); + int randChoice = k % n; + //std::cout << k << " " << n << " " << randChoice << std::endl; + return expect[randChoice] - 'a'; +} + +int main() { + std::srand(static_cast(std::time(0))); + int playerNum; + std::cout << "请输入玩家人数:"; + if (!(std::cin >> playerNum) || playerNum <= 0) { + std::cerr << "无效的玩家人数。" << std::endl; + return 1; + } + + std::cout << "请输入题目号:"; + int questionNum; + if (!(std::cin >> questionNum) || questionNum < 125 || questionNum > 130) { + std::cerr << "找不到对应题目。" << std::endl; + return 1; + } + + std::vector players; + Player tempP; + for (int i = 0; i < playerNum; i++) { + players.push_back(tempP); + players[i].lastSelect = -1; + } + + const int totalRounds = 5; + for (int round = 1; round <= totalRounds; ++round) { + Question* q = QuestionFactory::get().create(questionNum); + if (!q) { + std::cerr << "找不到对应题目。" << std::endl; + return 1; + } + q->init(players); + q->initTexts(players); + q->initOptions(); + q->initExpects(); + + std::cout << q->String() << std::endl; + + // 输入是否启用随机模式 + int randomModeInput; + std::cout << "第 " << round << " 回合,请选择是否启用随机模式(0: 不启用,1: 启用):"; + std::cin >> randomModeInput; + bool randomMode = (randomModeInput == 1); + std::cout << "第 " << round << " 回合,请输入每位玩家的选择(用空格分隔):\n"; + for (int i = 0; i < playerNum; ++i) { + if (randomMode) { + const std::string& expect = q->expects[0]; + players[i].select = getRandomChoiceBasedOnExpect(expect); + std::cout << "玩家 " << i + 1 << " 随机选择了选项: " << (char)(players[i].select + 'a') << std::endl; + } else { + char ch; + std::cin >> ch; + if (ch >= 'A' && ch < 'A' + q->options.size()) { + players[i].select = ch - 'A'; + } else if (ch >= 'a' && ch < 'a' + q->options.size()) { + players[i].select = ch - 'a'; + } else { + std::cerr << "无效选项 '" << ch << "',使用默认A。" << std::endl; + players[i].select = 0; + } + } + } + + for (int i = 0; i < playerNum; ++i) + players[i].lastSelect = players[i].select; + for (int i = 0; i < playerNum; ++i) + players[i].realLastScore = players[i].lastScore; + for (int i = 0; i < playerNum; ++i) + players[i].lastScore = players[i].score; + + q->initCalc(players); + q->calc(players); + q->quickScore(players); + + std::cout << "\n第 " << round << " 回合分数:\n"; + for (int i = 0; i < playerNum; ++i) + std::cout << i << " " << players[i].score << std::endl; + + delete q; + } + + std::cout << "游戏结束。" << std::endl; + return 0; +} diff --git "a/games/crowd/resource/\344\271\214\345\220\210\344\271\213\344\274\227\345\274\200\345\217\221\346\226\207\346\241\243.cpp" "b/games/crowd/resource/\344\271\214\345\220\210\344\271\213\344\274\227\345\274\200\345\217\221\346\226\207\346\241\243.cpp" new file mode 100644 index 00000000..ba5b1f27 --- /dev/null +++ "b/games/crowd/resource/\344\271\214\345\220\210\344\271\213\344\274\227\345\274\200\345\217\221\346\226\207\346\241\243.cpp" @@ -0,0 +1,266 @@ +// 本文档仅保留出题会用到的常用函数定义,完整源码请查看`problems.h` + +/* 玩家结构 */ +class Player +{ +public: + Player() + { + lastSelect = 0; + select = 0; + lastScore = 0; + score = 0; + } + + int lastSelect; // 上回合选择 + int select; // 本回合选择 + double realLastScore; // 上回合的分数,calc计算结束后变化 + double score; // 本回合分数 + + string getScore() + { + return to_string(score); + } +}; + +class Question +{ +public: + + Question() + { + vars.clear(); + } + + virtual ~Question() {}; + + // 问题相关配置 + int id; + string author; + string title; + vector texts; + vector options; + vector expects; + + // **【重点看这里】** + // 可以当作全局参数 + map vars; + // 可快速调用的变量 + double playerNum; // 玩家人数 + double maxScore; // 最高分数 + double minScore; // 最低分数 + double medianScore; // 分数中位数 + vector optionCount; // 每个选项选择的人数数量 + double maxSelect; // 选择最多选项的人数 + double minSelect; // 选择最少选项的人数 + double nonZero_minSelect; // 选择最少选项的人数(不包括0) + vector tempScore; // 记录这个选项可以获得的分数,算分专用 + + // 各种初始化 + void init(vector& players); + virtual void initTexts(vector& players){} + virtual void initOptions(){} + virtual void initExpects(){} + void initCalc(vector& players); + // 图片UI相关(无需使用) + string Markdown(); + string String(); +}; + +// double保留两位小数 +double dec2(double x); +// double转字符串 +string str(double x); + + + +/* 您仅需实现如下部分 */ +class Q114 : public Question +{ +public: + Q114() + { + id = 114; // 题目ID,和类名相同 + author = "NULL"; // 题目作者名称 + title = "测试题目"; // 题目名称 + } + + virtual void initTexts(vector& players) override // 题干文本 + { + texts.push_back("我是第一行文本"); + texts.push_back("我是第二行文本"); // 大部分题目仅需要一行文本,如需多行再添加 + } + virtual void initOptions() override // 选项文本 + { + vars["v1"] = 1.36; // 将变量保存至vars中,可以在其他函数(如calc)中重复使用 + options.push_back("我是选项【A】,选我获得" + str(vars["v1"]) + "分"); + options.push_back("我是选项【B】,选我获得2.17分"); + // 下方可以增加更多选项(最多26个) + } + virtual void initExpects() override // 电脑伪随机概率 + { + // 每个电脑玩家都会从下方字符串中随机一个字母 + expects.push_back("abbb"); // 展示的例子为电脑1/4概率选A,3/4概率选B + } + virtual void calc(vector& players) override // 分数计算实现 + { + // 基础分数 + // 将选项的得分保存在tempScore中,每个选择该选项的玩家都会按照这个分数变动 + tempScore[0] = vars["v1"]; // A选项得分记为 vars["v1"] (从0编号) + tempScore[1] = 2.17; // B选项得分记为 2.17 + } +}; + + + +/* 下方为一般题目实现的具体例子和细节说明 */ + +/* 条件判断(条件类、均分类题目写法例子) */ +// Q2:B选项 +"战争:如果恰有两名玩家选择这个选项,则+6分。" +// 实现为: +if(optionCount[1] == 2) tempScore[1] = 6; // optionCount[1]获取B选项的人数 + +// Q4:E选项 +vars["E"] = (int)playerNum / 2 + 1; // E的分数根据人数动态实现:人数/2+1 +"公正:选择这项的人平分 " + str(vars["E"]) + " 分。" +// 实现为: +tempScore[4] = vars["E"] / optionCount[4]; // E选项得分/E选项人数 + +// Q6 +virtual void initTexts(vector& players) override +{ + texts.push_back("选择一项。"); + texts.push_back(" [ 只有选择人数最多的选项会生效 ]"); +} +virtual void initOptions() override +{ + options.push_back("破坏:-1,然后使选 B 的玩家 -3"); + options.push_back("合作:+2,然后使选 C 的玩家 +1"); + options.push_back("平衡:-0.5。"); +} +// 实现为: +virtual void calc(vector& players) override +{ + if(optionCount[0] == maxSelect) // 选项A人数最多 + { + tempScore[0] -= 1; + tempScore[1] -= 3; + } + if(optionCount[1] == maxSelect) // 选项B人数最多 + { + tempScore[1] += 2; + tempScore[2] += 1; + } + if(optionCount[2] == maxSelect) // 选项C人数最多 + { + tempScore[2] -= 0.5; + } +} + + + +/* 关于高级操作(涉及单个玩家的分数检测或变化) */ +// Q7:A选项 +"均衡:如果有 3 或更多名玩家选择本项,则得分最高的玩家 -3" +// 实现为: +if(optionCount[0] >= 3) // 当A选项人数大于3时 +{ + for(int i = 0; i < playerNum; i++) // 遍历所有玩家 + { + if(players[i].score == maxScore) // 如果玩家的分数等于最高分数 + { + players[i].score -= 3; // 这个玩家的分数直接-3 + } + } +} + +// Q21:B选项 +"强硬:如果所有玩家全部选择该项,则所有玩家分数取反。" +// 实现为: +if(optionCount[1] == playerNum) // 如果B选项人数等于玩家总人数(所有人选择此项) +{ + for(int i = 0; i < playerNum; i++) // 遍历所有玩家 + { + players[i].score = -players[i].score; // 玩家分数变为相反数 + } +} + +// Q23:A选项 +"同化:+1,然后所有选择此项的玩家会均分他们的分数。" +// 实现为: +double sum = 0; +for(int i = 0; i < playerNum; i++) // 遍历所有玩家 +{ + if(players[i].select == 0) // 如果玩家选择了A选项 + { + sum += players[i].score; // 计算玩家的总分 + players[i].score = 0; // 先清零此玩家的分数 + } +} +tempScore[0] = 1 + sum / optionCount[0]; // 最终结果存入 tempScore[0] + +// Q30:D选项(人数最多时触发) +"摧毁:所有人得分变为 0 。然后你 -30" +// 实现为: +if(optionCount[3] == maxSelect) // 如果D选项等于人数最多的选项 +{ + for(int i = 0; i < playerNum; i++) + { + players[i].score = 0; // 玩家分数清零 + if(players[i].select == 3) // 如果该玩家选择D选项 + players[i].score -= 30; // 额外-30分 + } +} + + +/* 部分特殊题目需要专门针对于题目编写逻辑(离谱题目可以选择先吃灰) */ +// Q53 +virtual void initTexts(vector& players) override +{ + int large_med_count = 0; + vars["med"] = ceil(medianScore); // C选项分数限制 + for (int i = 0; i < playerNum; i++) { + if (players[i].score >= vars["med"]) { + large_med_count++; + } + } + vars["limit"] = large_med_count / 2 + 1; // C选项人数限制 + texts.push_back("战争爆发,你的策略是?"); +} +virtual void initOptions() override +{ + options.push_back("和平:+1。但如果核武器研制成功,改为-1"); + options.push_back("人海战术:+2。但如果核武器研制成功,改为-3"); + options.push_back("研究核武器:如果当前分数达到 " + str(vars["med"]) + " 的玩家至少有 " + str(vars["limit"]) + " 人选择此选项,则研制成功,所有选择此选项的玩家+1,否则-2"); + options.push_back("投降:-0.5"); +} +virtual void initExpects() override +{ + expects.push_back("aabcccdd"); +} +virtual void calc(vector& players) override +{ + tempScore[0] = 1; + tempScore[1] = 2; + tempScore[2] = -2; + tempScore[3] = -0.5; + int count = 0; + for(int i = 0; i < playerNum; i++) { // 统计达标人数 + if (players[i].score >= vars["med"] && players[i].select == 2) { + count++; + } + } + if (count >= vars["limit"]) { // 判断C选项是否成功 + tempScore[0] = -1; + tempScore[1] = -3; + tempScore[2] = 1; + } +} + + + + + +// 感谢您阅读本文档,更多内容请查看题库文件`problems.h`,然后参考已有题目 +// 如遇到问题请联系铁蛋 diff --git "a/games/crowd/resource/\344\271\214\345\220\210\344\271\213\344\274\227\351\242\230\345\272\223_2024.9.12_72-113.txt" "b/games/crowd/resource/\344\271\214\345\220\210\344\271\213\344\274\227\351\242\230\345\272\223_2025.7.15_78-132-24.txt" similarity index 64% rename from "games/crowd/resource/\344\271\214\345\220\210\344\271\213\344\274\227\351\242\230\345\272\223_2024.9.12_72-113.txt" rename to "games/crowd/resource/\344\271\214\345\220\210\344\271\213\344\274\227\351\242\230\345\272\223_2025.7.15_78-132-24.txt" index 32a9d239..79bff615 100644 --- "a/games/crowd/resource/\344\271\214\345\220\210\344\271\213\344\274\227\351\242\230\345\272\223_2024.9.12_72-113.txt" +++ "b/games/crowd/resource/\344\271\214\345\220\210\344\271\213\344\274\227\351\242\230\345\272\223_2025.7.15_78-132-24.txt" @@ -1,9 +1,9 @@ 乌合之众题库 -本记录导出时间:Thu Sep 12 19:12:30 2024 +本记录导出时间:Tue Jul 15 19:05:15 2025 -总题目数:113 -正式题库题目数:72 +总题目数:156(包含 24 个废弃题目) +正式题库题目数:78 注:部分题目数字会根据参与游戏的人数和当前分数发生变化,此展示题库默认10人参与游戏且所有人分数为0 @@ -409,7 +409,7 @@ D: 婆罗门:如果只有你一人选此项,+2  选择一项。 -A: 恰有两人选此项,你们的分数互换 +A: 恰有两人选此项,你们的分数互换,然后+1 B: 恰有一人选此项,将你的分数取绝对值 C: 恰有一人选此项,在结算完上述两个选项后,分数最高者-2 D: +0.5 @@ -589,7 +589,7 @@ D: 投降:-0.5 出题者:saiwei 题目:空中楼阁 - 如果你盖的楼能盖的起来,不是空中楼阁,则你加所盖楼层的分数,否则你扣所盖楼层的分数。 + 如果你盖的楼能盖的起来,不是空中楼阁(下方的楼层均有玩家选择),则你加所盖楼层的分数,否则你扣所盖楼层的分数。 A: 盖1楼 B: 盖2楼 @@ -646,8 +646,8 @@ D: -1  你的选择是? A: 役满:如果没有人选择B,+3.2 -B: 速攻:+0.5,如果有人选择A,改为+1.5 -C: 平进:+1,如果B的人数最多,则B改为-1 +B: 速攻:+0.5,如果有人选择A,改为+1.5;但如果有人选择C且B的人数最多,则B改为-1 +C: 平进:+1 D: 九种九牌:-4,如果任何人选择此项,则使其他选项无效;但如果有人选择C,此选项分数改为+2 E: 天选之人:1.6%的概率获得64分 ------------------------------------------------------------ @@ -792,7 +792,7 @@ D: 品鉴美食:平分这杯水的分数 A: 亚瑟的忠臣[好]:好人胜利则+1分 B: 梅林[好]:好人胜利则+4分;如果有人选刺客,改为-1 -C: 派西维尔[好]:如果好人胜利,且本选项人数在有人选择的选项里最少,+3分 +C: 派西维尔[好]:如果本选项人数在有人选择的选项里最少,+3分 D: 刺客[坏]:如果有人选梅林,坏人阵营必定胜利并且本选项+2分;否则-1分 E: 莫甘娜[坏]:坏人胜利则+1分 ------------------------------------------------------------ @@ -808,122 +808,78 @@ C: 出老千,+4分,有人选E则-2分 D: 坐庄:获得 [2-出老千人数] 分 E: 指认:有选择C的+2分,否则-1分 ------------------------------------------------------------ - -********************************************************************** -++下方为测试题库,平衡性尚未测试完全,不会出现在正式游戏中++ -********************************************************************** - 题号:#73 -出题者:圣墓上的倒吊人 -[测试] 题目:云顶之巢 +出题者:剩菜剩饭 +题目:枪打出头鸟  选择一项。 -A: 速攻:如果没有中速,+3 -B: 中速:+1.5 -C: 贪贪贪:如果没有速攻,+5;反之-3 +A: +1分 +B: +2分 +C: 如果选择B的人数大于A,+4分。反之-3分 +D: 如果A选项人数最少且有人选择C选项,+C分,并使B选项改为-2分 ------------------------------------------------------------ 题号:#74 -出题者:Chance -[测试] 题目:未命名 +出题者:orange juice +题目:大战讨口橘 - 选择下面一项,人数最多和最少的选项都成立 + 街上偶遇个讨口子orange juice,拼尽全力无法战胜,你选择 -A: A+1,B+2 -B: A-2,C-3 -C: C+2,D+2 -D: D-1,B-2 +A: 乖乖投降:-1 +B: 尝试绕路:+0,该选项选择人最多则改为-2 +C: 同流合污:平分选择AB玩家失去的总分数,选择人数大于3人则改为-3 +D: 抢劫橘子:恰好有1人选择该选项,+3,否则-3 ------------------------------------------------------------ 题号:#75 -出题者:Chance -[测试] 题目:未命名 +出题者:圣墓上的倒吊人 +题目:云顶之巢 - 选择下面一项,人数最少的一项成立 + 你正在参加一局云顶之巢游戏,请选择你的策略 -A: B+2 -B: C-0.5 -C: D-1 -D: A+1 +A: 速攻:如果没有中速,+2 +B: 中速:+1 +C: 贪贪贪:如果没有速攻,+3;反之-1 ------------------------------------------------------------ 题号:#76 -出题者:纤光 -[测试] 题目:平行时空 +出题者:大梦我先觉 +题目:买票 - 有一项选择人数唯一最多时,此项生效 - 有两项选择人数同时最多时,第三项生效 - 有三项选择人数同时最多时,均不生效,所有人分数清零。 + 一张票+5分,出价高的先购买,但只有前50%可以买到票(若同一批出价的人数高于剩余票的数量,则此批次和靠后的票都不会出售)请选择以下选项。 -A: 若有人选B,选C的人+1 -B: 若有人选C,选A的人+2 -C: 若有人选A,选B的人+3 +A: 不买 +B: -1分 +C: -2分 +D: -3分 ------------------------------------------------------------ 题号:#77 -出题者:纤光 -[测试] 题目:差值投标 +出题者:克里斯丁 +题目:杀人案 - 选择人数最多的两项对战(多项相同时取靠近A的两项),对战者中靠近A的称作败者,靠近D的称作胜者。另外两项与败者得分相同但额外-0.5。 - 败者和胜者选项分别生效,随后胜者+4。 + 目击杀人案,你选择成为 -A: -0 -B: -2 -C: -4 -D: -6 +A: 主犯:如果只有一人选择这项,+3分 +B: 帮凶:如果存在主犯,+1分 +C: 受害者:没人选择主犯,+4分;否则-1分 +D: 目击者:如果ABC都有人选,+2分 ------------------------------------------------------------ 题号:#78 -出题者:Chance -[测试] 题目:我是大土块 - - 选择一项。 - -A: +2,有 [(B+C) *10]% 的概率-4 -B: +1,有 [(A-B) *10]% 的概率使C+2 -C: -2,有 [(A+D) *10]% 的概率+6 -D: -3,有 [(A+B-D) *10]% 的概率使A和B-4,D+5 ------------------------------------------------------------- -题号:#79 -出题者:圣墓上的倒吊人 -[测试] 题目:资源配置 - - 桌上有 20 个资源,一资源一分,如果投入后总资源超过了 32 ,那获得(你的减分/所有玩家的减分)比例的总资源。如果投入后分数为负数,则选择无效。 - -A: 0 -B: -1 -C: -2 -D: -4 -E: -6 -F: 1.6%获得投入后总资源,否则-15 ------------------------------------------------------------- -题号:#80 -出题者:飘渺 -[测试] 题目:电车难题 - - 请选择你的位置。拉杆只有2个状态,即拉动偶数次会还原拉杆 - -A: 乘客:+0.5,但每有一个人拉动拉杆-0.5 -B: 多数派:+0.5,如果拉杆最终没有拉动,改为-2 -C: 少数派:+0.5,如果拉杆最终拉动,改为-2.5 -D: -0.5,每一个选择此项的人都会拉动一次拉杆 ------------------------------------------------------------- -题号:#81 -出题者:Q群管家 -[测试] 题目:均分1 +出题者:克里斯丁 +题目:名侦探柯南 - 选择一项。 + 酒厂抓卧底,专抓选择的人数最多的那人,若存在并列最多则一起抓。你选择成为 -A: 当前总积分×0.5 -B: 选择此选项的玩家均分他们的分数 -C: 当前总积分×(-0.5) +A: 琴酒:只要抓到卧底则+3 +B: 伏特加:+1.5 +C: 波本(卧底):获得选择该选项人数的分数,被抓到则不得分。 +D: 基尔(卧底):平分5分,被抓到则不得分。 +E: 黑麦威士忌(卧底):+5,被抓到则不得分。E的人数视作乘2计算 ------------------------------------------------------------ -题号:#82 -出题者:Q群管家 -[测试] 题目:均分2 - 选择一项。 +********************************************************************** +++下方为测试题库,平衡性尚未测试完全,不会出现在正式游戏中++ +********************************************************************** -A: 选择此选项的分数最高与最低的玩家均分他们的分数 -B: 正分的-2,负分的+2 ------------------------------------------------------------- -题号:#83 +题号:#79 出题者:Q群管家 [测试] 题目:奇偶2 @@ -932,41 +888,7 @@ B: 正分的-2,负分的+2 A: +3 B: -1,若此选项选择人数为奇数,A变为-3 ------------------------------------------------------------ -题号:#84 -出题者:Q群管家 -[测试] 题目:奇偶5 - - 选择一项。 - -A: 若选择人数为奇数,扣除 [选择本选项人数/2] 的分数;否则获得相应的分数 -B: 0 ------------------------------------------------------------- -题号:#85 -出题者:胡扯弟 -[测试] 题目:云顶之巢 - - 选择你的云巢身份。 - -A: YAMI:总是能+2分的大佬,但是有5%的可能性失手-1 -B: 飘渺:10%+24的赌博爱好者 -C: 黑桃3:+13,但每有一个飘渺-1.75分 -D: 飞机:+12,但有YAMI就会被gank -E: 西东:+A+B-C+D -F: 胡扯:-114514,但有2.5%的可能性改为+1919810 ------------------------------------------------------------- -题号:#86 -出题者:圣墓上的倒吊人 -[测试] 题目:未来计划 - - (X)代表有且只有 X 人选择该选项时,该选项才能执行。执行顺序为序号顺序。无选项执行时执行E - -A: +4(1) -B: +2(2) -C: -3(3) -D: -1(≥2) -E: 所有玩家的分数变为相反数(4) ------------------------------------------------------------- -题号:#87 +题号:#80 出题者:圣墓上的倒吊人 [测试] 题目:坐等反转 @@ -977,7 +899,7 @@ B: 使 D 选项无效化 / 0 C: D-2 / D+2 D: 所有玩家均分自己的分数 / -1 ------------------------------------------------------------ -题号:#88 +题号:#81 出题者:圣墓上的倒吊人 [测试] 题目:报销 @@ -989,7 +911,7 @@ C: 2 D: 3 E: 4 ------------------------------------------------------------ -题号:#89 +题号:#82 出题者:圣墓上的倒吊人 [测试] 题目:奇珍异宝 @@ -1000,7 +922,7 @@ B: 银质餐具(3) C: 国王宝球(5) D: 奇怪的书(选择该选项的玩家数的平方-2) ------------------------------------------------------------ -题号:#90 +题号:#83 出题者:圣墓上的倒吊人 [测试] 题目:站位 @@ -1011,7 +933,7 @@ A: B人数大于 5 则胜利 B: 中立 C: A人数大于B则胜利,否则输 ------------------------------------------------------------ -题号:#91 +题号:#84 出题者:纸团OvO [测试] 题目:差值投标 @@ -1024,7 +946,7 @@ C: 市民流:2级筹码 D: 皇帝流:3级筹码 E: 悠悠流:0级或1级筹码(当对面是0时为1,否则为0) ------------------------------------------------------------ -题号:#92 +题号:#85 出题者:xiaogt [测试] 题目:债务危机 @@ -1037,7 +959,7 @@ D: 0分,第二轮起每轮-3分 E: -1.5分,第二轮起每轮+0分 F: -6分,若在第一轮就分摊完毕,改为+1分 ------------------------------------------------------------ -题号:#93 +题号:#86 出题者:大梦我先觉 [测试] 题目:夺宝奇兵 @@ -1048,7 +970,7 @@ B: 如果此选项的人数>=4,平分 10 分的宝藏 C: 地雷:如果仅 1 人选择,使A和B的得分变为相反数,你获得 [A+B] 人数的分数 D: 战术弃权:每有一人选择此选项,则使选择A的人数减少一人(最低不低于1),使选择B的人数增加一人 ------------------------------------------------------------ -题号:#94 +题号:#87 出题者:剩菜剩饭 [测试] 题目:未命名 @@ -1060,7 +982,7 @@ B: +2 C: +3 D: +4 ------------------------------------------------------------ -题号:#95 +题号:#88 出题者:齐齐 [测试] 题目:HP杀 @@ -1071,7 +993,7 @@ B: 平民:当B>A时,+2,否则-2 C: 内奸:当A=B时,+2,否则-2 D: 特工:当A=B时,+10,否则-10 ------------------------------------------------------------ -题号:#96 +题号:#89 出题者:Chance [测试] 题目:未命名 @@ -1081,7 +1003,7 @@ A: 平分 [B选项人数-5绝对值] 的分数 B: 平分 [A选项人数-6绝对值] 的分数 C: 平分 [人数最多的选项人数] 的分数 ------------------------------------------------------------ -题号:#97 +题号:#90 出题者:Chance [测试] 题目:未命名 @@ -1096,26 +1018,7 @@ F: +2.5 G: +2.75 H: +3 ------------------------------------------------------------ -题号:#98 -出题者:圣墓上的倒吊人 -[测试] 题目:真理 - - 真理掌握在? - -A: 少数人:+3,如果此项选择人数最多,则改为减 [选择该项人数] 的分数 -B: 多数人:-2 ------------------------------------------------------------- -题号:#99 -出题者:丘陵萨满 -[测试] 题目:伊甸园 - - 获胜的选项获得 [选择非此选项人数] 的分数,失败的选项失去 [选择获胜选项人数] 的分数 - -A: 红苹果:如果分数小于等于 0 的玩家都选择红苹果,红苹果获胜。但如果所有人都选择此项,全员分数取反 -B: 银苹果:如果选择此项的人数少于 C,且红苹果没有获胜,银苹果获胜。 -C: 金苹果:如果选择此项的人数少于等于 B,且红苹果没有获胜,金苹果获胜。 ------------------------------------------------------------- -题号:#100 +题号:#91 出题者:xiaogt [测试] 题目:怪物糖果 @@ -1126,17 +1029,17 @@ B: 2分 C: 3分 D: 4分 ------------------------------------------------------------ -题号:#101 +题号:#92 出题者:九九归一 [测试] 题目:伊甸园  选择下列一项: -A: 金苹果:若选择此项的人数大于选择B的人数,则平分 [未选择此项的人数] 分,否则减1分。 -B: 银苹果:若选择此项的人数大于选择A的人数,则平分 [未选择此项的人数] 分,否则减1分。 +A: 金苹果:若选择此项的人数小于选择B的人数,则平分 [未选择此项的人数] 分,否则减1分。 +B: 银苹果:若选择此项的人数小于选择A的人数,则平分 [未选择此项的人数] 分,否则减1分。 C: 红苹果:若选择A的人数等于选择B的人数,则平分 [未选择此项的人数] 分,否则减1分。 ------------------------------------------------------------ -题号:#102 +题号:#93 出题者:剩菜剩饭 [测试] 题目:寻宝猎人 @@ -1147,7 +1050,7 @@ B: 珍宝:如果选择人数小于等于3,+2 C: 奇遇:选择A,B的玩家减 [选择C的人数] 的分数 D: 怪物:+1分,如果没有人选本项,选C的玩家分数归0 ------------------------------------------------------------ -题号:#103 +题号:#94 出题者:栗子 [测试] 题目:利他之众 @@ -1158,27 +1061,7 @@ B: C平分3分 C: AD平分3分 D: AC平分3分 ------------------------------------------------------------ -题号:#104 -出题者:圣墓上的倒吊人 -[测试] 题目:战争中的贵族 - - 你的国家与敌国正在战争中,选择一项行动。 - -A: 继续抵抗:+5,如果有玩家选择了B,那选择此项的人分数归零 -B: 有条件投降:-3 -C: 润出国外:如果有人选 A,则-2 ------------------------------------------------------------- -题号:#105 -出题者:飞机鸭卖蛋 -[测试] 题目:太多挂机A了…? - - 分别记选择A、B、C选项的人数为a、b、c: - -A: 获得 a 分 -B: 获得 a*1.5-b 分 -C: 获得 a*2-c 分 ------------------------------------------------------------- -题号:#106 +题号:#95 出题者:飞机鸭卖蛋 [测试] 题目:听力练习 @@ -1188,17 +1071,17 @@ A: 背书:+1,噪音值+1 B: 耳机标准模式:+3-噪音值 C: 耳机降噪模式:+3,但50%概率中途关机,改为+0 ------------------------------------------------------------ -题号:#107 +题号:#96 出题者:飞机鸭卖蛋 [测试] 题目:有情人终成眷属  选择一项。 -A: 若恰有两人选择,-2,否则+2 -B: 若恰有两人选择,-2,否则+2 +A: 若恰有两人选择此选项,-2,否则+2 +B: 若恰有两人选择此选项,-2,否则+2 C: 若选择人数最多,-2,否则+3 ------------------------------------------------------------ -题号:#108 +题号:#97 出题者:小黄鸭 [测试] 题目:E卡抉择 @@ -1208,7 +1091,7 @@ A: 皇帝:+2分 B: 市民:若A比C多则-1分;否则+1分。 C: 奴隶:若有人选择此项且没有人选择B,则+1分且所有选A的玩家的总得分变为0;否则-1分。 ------------------------------------------------------------ -题号:#109 +题号:#98 出题者:齐齐 [测试] 题目:月下人狼 @@ -1219,7 +1102,7 @@ B: 人狼:如果村人扣分,+1,否则-2 C: 狂人:如果村人扣分,+4,否则-2 D: 妖狐:50%概率+2,50%概率-2(所有妖狐得分情况相同) ------------------------------------------------------------ -题号:#110 +题号:#99 出题者:匿名 [测试] 题目:选举 @@ -1230,7 +1113,7 @@ B: 分最高的人+3,负分的玩家分数加至0 C: 选择该项的玩家增加等同于人数的分数,40%概率改为减少 D: 0 ------------------------------------------------------------ -题号:#111 +题号:#100 出题者:纤光 [测试] 题目:金铃铛 @@ -1241,7 +1124,7 @@ B: 摇2次铃 C: 摇3次铃 D: + 2.5 分 ------------------------------------------------------------ -题号:#112 +题号:#101 出题者:xiaogt [测试] 题目:投资 @@ -1253,12 +1136,619 @@ C: 投资 2 积分 D: 投资 3 积分 E: 投资 4 积分 ------------------------------------------------------------ +题号:#102 +出题者:Chance +[测试] 题目:未命名 + + 选择下面一项,人数最多和最少的选项都成立 + +A: A+1,B+2 +B: A-2,C-3 +C: C+2,D+2 +D: D-1,B-2 +------------------------------------------------------------ +题号:#103 +出题者:Chance +[测试] 题目:未命名 + + 选择下面一项,人数最少的一项成立 + +A: B+2 +B: C-0.5 +C: D-1 +D: A+1 +------------------------------------------------------------ +题号:#104 +出题者:纤光 +[测试] 题目:平行时空 + + 有一项选择人数唯一最多时,此项生效 + 有两项选择人数同时最多时,第三项生效 + 有三项选择人数同时最多时,均不生效,所有人分数清零。 + +A: 若有人选B,选C的人+1 +B: 若有人选C,选A的人+2 +C: 若有人选A,选B的人+3 +------------------------------------------------------------ +题号:#105 +出题者:纤光 +[测试] 题目:差值投标 + + 选择人数最多的两项对战(多项相同时取靠近A的两项),对战者中靠近A的称作败者,靠近D的称作胜者。另外两项与败者得分相同但额外-0.5。 + 败者和胜者选项分别生效,随后胜者+4。 + +A: -0 +B: -2 +C: -4 +D: -6 +------------------------------------------------------------ +题号:#106 +出题者:本仙子很强 +[测试] 题目:兵主 + + 选择一项。 + +A: 鲜血:+1 +B: 冰霜:本回合中,你的扣分变为加分。但是如果没有扣分,你-2 +C: 邪恶:使所有没有选择邪恶的人-1 +D: 彩虹:如果ABC均有人选择,+[人数/2]向上取整 +------------------------------------------------------------ +题号:#119 +出题者:周小墨 +[测试] 题目:炉石传说 + + 选择一项。 + +A: 冲锋:+6,每有一人选择本选项得分-1 +B: 嘲讽:每有一人选择冲锋得分+1 +C: 突袭:-1,每有一人选择嘲讽得分+1 +D: 圣盾:+1 +------------------------------------------------------------ +题号:#108 +出题者:克里斯丁 +[测试] 题目:半句话失效 + + 有人选择的选项执行对应的效果。 + +A: -9,你的分数取相反数 +B: -1,并令A的后半句话失效 +C: -2,并令A的前半句话失效 +D: 令B、C中选的人更多的选项后半句话失效,选的人更少的选项前半句话失效,B、C一样多则本选项作废 +------------------------------------------------------------ +题号:#109 +出题者:克里斯丁 +[测试] 题目:克里斯丁的印第安扑克 + + 对手是一张五点。你无法从他的表情中推断自己的牌面大小。因此你选择 + **A、B中选择人数更多的视作本局出牌,若并列则优先级A>B** + +A: 牌面3 +B: 牌面10 +C: 加注,若牌面为10则+2,反之-2 +D: 开牌,若牌面为10则+1,反之-1 +E: 弃牌,+2,若牌面为10则-2 +------------------------------------------------------------ +题号:#110 +出题者:克里斯丁 +[测试] 题目:逆转裁判 + + 律师和检察官合伙针对证人。 + 若BCD人数总和加起来大于A的人数,证人被抓,反之被告被抓。你选择担任 + +A: 证人:无论是否被抓均+1.5 +B: 被告:+3,被抓改为-1 +C: 律师:+2,被告被抓改为+0 +D: 检察官:+3,若证人被抓改为+0 +------------------------------------------------------------ +题号:#111 +出题者:克里斯丁 +[测试] 题目:对战 + + 联邦和帝国对战,战力更大的阵营获胜(正常情况下一人一点战力)并平分10分 + +A: 联邦精锐:-1,一人两点战力。 +B: 联邦特工局:只要有人选择B,若联邦最终胜利,额外吸取所有选D的人各一分并平分给所有选B的人。 +C: 帝国官僚:+1,一人0.5战力。 +D: 帝国军队:+1 +------------------------------------------------------------ +题号:#112 +出题者:克里斯丁 +[测试] 题目:许愿池 + + 往池子里投币许愿,一币一分,若池子里的钱币比玩家人数多,则许愿成功 + +A: 0币 +B: 1币,许愿成功+3 +C: 2币,许愿成功+5 +D: 3币,许愿成功+7 +E: 搅混水:-0.5并-2币 +F: 超级搅屎棍:-1并-4币 +------------------------------------------------------------ 题号:#113 +出题者:克里斯丁 +[测试] 题目:贪婪的代价 + + 克里斯丁看到你可怜的分数大发慈悲,决定施舍你 20 分。若索取的总分数超了则一分也得不到。你决定 + +A: 不取意外之财,克里斯丁赞赏你的品德并赠送2分 +B: 取3分 +C: 取4分 +D: 取5分 +E: 取6分 +------------------------------------------------------------ +题号:#114 +出题者:大红大紫 +[测试] 题目:外星危机 + + 分别记每个选项的人数为abcd + +A: 防御:若a+c>b+d防御成功A+2且B+1,否则A-1 +B: 逃亡:若b+c>a+d成功逃亡B+1.5,否则B-0.5 +C: 中立:C+1,若成功防御或逃亡C+0.5(可叠加) +D: 背叛:有人选D时ABC都额外-1。若成功逃亡或防御,D-1.5;若同时逃亡和防御,D-3.5 +------------------------------------------------------------ +题号:#115 +出题者:剩菜剩饭 +[测试] 题目:绑架 + + 你们被绑架了,头目正在思考要不要杀你们(选项按照 A到E 依次结算) + +A: 反抗:如果至少有一半人选择此项,+3分,否则-1分 +B: 服软:+2分 +C: 卖人:+1分,选择的人数每比B多一人,B扣1分 +D: 沉默:如果所有人中AB中扣分的人数达到一半,+2分 +E: 卧底:如果本选项选择的人数最少(不含0人),结算后加分最多的人都-3分 +------------------------------------------------------------ +题号:#116 +出题者:剩菜剩饭 +[测试] 题目:狐狸觅食 + + 狐狸要开始觅食了!分数大于等于中位数的玩家是“兔子”,反之是“狐狸”(目前中位数=0) + “兔子”受到选项影响。若“兔子”所选选项的人数超过玩家的1/3(向上舍入)且有“狐狸”选择此项时,被吃掉,改为-2分 + “狐狸”不执行选项。若所选选项有“兔子”被吃,+2分,否则-1分 + +A: +2 +B: +1 +C: 0 +D: -1 +------------------------------------------------------------ +题号:#117 +出题者:大梦我先觉 +[测试] 题目:金币抉择 + + 初始金币为0,1金币1分。请选择以下选项 + +A: +1,金币+3,不平分金币 +B: 0,金币-1,参与金币平分 +C: 0(金币=0)/+2(金币>0)/-2(金币<0) +D: 获得 [亏空金币数] 的数量(金币<0) +------------------------------------------------------------ +题号:#118 +出题者:纤光 +[测试] 题目:囚徒困境 + + AB中人数更少项的人数记为X + CD中人数更多项的人数记为Y + +A: 合作:+(X-Y) +B: 合作:+2(X-Y) +C: 欺骗:-2(Y-X) +D: 欺骗:-Y +------------------------------------------------------------ +题号:#119 +出题者:冰糖_Cryst +[测试] 题目:无人区 + + 如果所有选项都有人选择,所有选项生效且得分改为相反数 + +A: 0 +B: 如果没有人选择A,+1 +C: 如果没有人选择B,+2 +D: 如果没有人选择C,+3 +------------------------------------------------------------ +题号:#120 +出题者:克里斯丁 +[测试] 题目:红楼梦 + + 红楼梦,你得到了风月宝鉴,你决定看 + (选择人数少的那一项执行,一样便都不执行) + +A: 正面:+1 +B: 反面:-1,然后你的分数取绝对值 +------------------------------------------------------------ +题号:#121 +出题者:克里斯丁 +[测试] 题目:西游记 + + 西游记,你选择成为 + +A: 唐僧:+5,若有妖怪且没有孙悟空则-5 +B: 孙悟空:只有强者能当。分数最高者选择该选项+3,其他人选择该选项-100 +C: 猪八戒:+1 +D: 妖怪:-1 +------------------------------------------------------------ +题号:#122 +出题者:克里斯丁 +[测试] 题目:水浒传 + + 水浒传,你选择成为 + +A: 西门庆:+4,存在武松-4 +B: 潘金莲:+2,存在武松-2 +C: 王婆:+1,存在武松-1 +D: 武大郎:+6,若武松不在场,选ABC的人数大于1人则-6。如果武松在场,选ABC的人数大于4人则-6。 +E: 武松:乃是魔星下凡,得分最低者选此项+3,其他人选择该项-100 +------------------------------------------------------------ +题号:#123 +出题者:克里斯丁 +[测试] 题目:三国演义 + + 三国演义,你选择成为(0人选择或两股势力选择人数相同均视作势力覆灭,不会纳入最终的人数结算) + +A: 刘备:若其势力人数最少则A+6,B-4,C+2 +B: 曹操:若其势力人数最多则A-3,B+3,C-3 +C: 孙权:若其势力人数第二多或第二少则A-5,B+2,C+2 +------------------------------------------------------------ +题号:#124 +出题者:蓝田 +[测试] 题目:大混战(10人标准)ver.1 + + 大混战开启,选择你的行为 + +A: 加入混战A组:成员总分数大于B组则平分8分 +B: 加入混战B组:成员总分数大于A组则平分6分,本组在计算总分数时+2分 +C: 逃离混战:若人数超过2人,-2;否则+2.5 +D: 什么?我是吃瓜群众:+0.5 +------------------------------------------------------------ +题号:#125 +出题者:栗子 +[测试] 题目:贫富差距 + + 在统计分数时,每个玩家分数都取绝对值后再计算总分 + +A: 选择该选项玩家总分≤玩家总分*0.75,+2;否则-1 +B: 选择该选项玩家总分≤玩家总分*0.5,+3;否则-2 +C: 选择该选项玩家总分≤玩家总分*0.25,+4;否则-3 +------------------------------------------------------------ +题号:#126 +出题者:剩菜剩饭 +[测试] 题目:好心喂了狗 + + 选择一项 + +A: -1分,如果没有人选择此项,BCD+都3分 +B: 如果没有人选择此项,选D的玩家-2分 +C: 如果没有人选择此项,选B,D的玩家+2分 +D: 如果没有人选择此项,选B,C的玩家-2分 +E: 如果没有人选择此项,第一名+4分 +------------------------------------------------------------ +题号:#127 +出题者:克里斯丁 +[测试] 题目:海盗纷争 + + 遇到一波海盗(共 10 人),你选择 + +A: 加入,海盗人数+1,若海盗胜利则瓜分选择战斗和逃跑的人数两倍的分数。若无人战斗则所有海盗-1 +B: 战斗,杀死一个海盗,若战胜海盗+3,否则-1 +C: 战斗,杀死三个海盗,若战胜海盗+2,否则-1 +D: 战斗,杀死五个海盗,若战胜海盗+1,否则-1 +E: 逃跑 +------------------------------------------------------------ +题号:#128 +出题者:纸团OvO +[测试] 题目:隹投票最精确? + + 共有甲乙丙丁四个对象,所有玩家将投出自己选项内的票,统计完票后,每位投票中包括“最高票的对象(可并列)”的玩家获得对应分数 + +A: 不投票直接+0.5分 +B: 甲乙丙,+1分 +C: 甲乙,+2分 +D: 甲丁,+2分 +E: 丙丁,+2分 +F: 丙,+3分 +G: 丁,+3分 +------------------------------------------------------------ +题号:#129 +出题者:aka展博 +[测试] 题目:未命名 + + 你和你的队伍正在寻找一座传说中的宝藏,每个选项代表不同的行动策略。选择人数最多的选项将决定整个队伍的行动方式,并根据队伍的整体表现获得相应的分数。 + +A: 潜行探索:+2分。如果人数多于B,额外获得1分;如果人数少于B,额外失去1分。 +B: 直接挖掘:+1分。每有1人选择,本选项得分增加0.5分 +C: 寻求帮助:-1分。如果人数多于D,改为2分 +D: 保持警惕:-2分。如果人数少于C,改为2分 +------------------------------------------------------------ +题号:#130 +出题者:aka展博 +[测试] 题目:未命名 + + 在一个绿洲中,所有玩家被困在了这里,在这里也仅剩下 30 分的物资,根据ABCD的顺序依次结算。 + +A: 获取选择该选项人数*1分物资。如果物资不够,则都不得分 +B: 获取选择该选项人数*0.5分物资。如果物资不够,则都不得分 +C: -2分,并给绿洲增加2分物资。如果选择该选项人数比A多,改为+3分(不再提供物资) +D: 如果资源池里的分数最后大于4,+3分;但如果选择该选项人数比B多,-2分。 +------------------------------------------------------------ +题号:#131 +出题者:纤光 +[测试] 题目:潘多拉魔盒 + + ABC中人数相对最多的一项生效 + DEF仅在不超过2人选择时生效 + 所有不生效的选项-0.5 + +A: A+1,B-1 +B: B+1,C+1 +C: C-1,A-1 +D: +1,每有一人选此项,本题所有得分翻一次倍 +E: -1,每有一人选此项,本题所有得分取一次反 +F: 每有一人选此项,A与B便互换一次效果 +------------------------------------------------------------ +题号:#132 +出题者:苣屋逊太郎 +[测试] 题目:绝望推杆 + + A和B两队分别有一根杆子。哪方有更多进攻者成功抵达敌方杆子,哪方就获得胜利。数量相等则无队胜利 + +A: 加入A队进攻B杆:-2,若己方胜利且存在神则+4 +B: 加入B队进攻A杆:-2,若己方胜利且存在神则+4 +C: 加入A队防守:拦截一名敌方的进攻者。若己方胜利,+2,否则-2 +D: 加入B队防守:拦截一名敌方的进攻者。若己方胜利,+2,否则-2 +E: 神:若无队胜利,+2,否则每有一位胜利方的进攻者抵达杆子扣1分 + +********************************************************************** +++下方为其他/废弃题库,可能不再考虑修改或更新,不会出现在正式游戏中++ +********************************************************************** + +------------------------------------------------------------ +题号:#t1 +出题者:纤光 +[其他/废弃] 题目:囚徒困境第1版【废弃】 + + 选择一项。 + +A: 认罪:-1,但若所有人都认罪,改为分数最高的人分数清零 +B: 不认罪:-2,但若所有人都不认罪,改为+2 +------------------------------------------------------------ +题号:#t2 +出题者:纤光 +[其他/废弃] 题目:囚徒困境第2版【废弃】 + + 选择一项。 + +A: 认罪:-1,结算完成后,若你的分数大于选此项的人数,则分数清零 +B: 不认罪:-2,结算完成后,若你的分数小于选此项的人数,则分数清零 +------------------------------------------------------------ +题号:#t3 +出题者:大梦我先觉 +[其他/废弃] 题目:战场的厮杀【废弃】 + + 在战场上,一念之差都会扭转战局。阵营AB对立,CD对立,各阵营人数多的一方获胜,胜利者可以获得 [失败一方人数/2] 的分数 + +A: 劫营 +B: 守营 +C: 抢粮 +D: 守粮 +------------------------------------------------------------ +题号:#t4 +出题者:蔡徐坤 +[其他/废弃] 题目:鉴宝师和收藏家 + + 将选择对应选项玩家数的 一半 分别记为 A B C D + +A: 鉴定为假:+2D,若D>C,将所有选择A的玩家的积分平分给选择D的玩家,然后选A玩家分数清0 +B: 鉴定为真:若C>D,你-2,否则你+D +C: 带来赝品:若B>A,你+2,否则你-A +D: 带来真品:若A>B,你-2,否则你+B +------------------------------------------------------------ +题号:#t5 +出题者:大梦我先觉 +[其他/废弃] 题目:焦灼的击剑【废弃】 + + a和b两人击剑时进行进攻和躲闪。a和b的最终行为分别由选择人数最多项确定(如果人数相等分数不变)。 + 若两人均进攻,则选进攻者-2分;两人均躲闪,则选躲闪者-1分。若一人进攻一人躲闪,则进攻方选进攻者+2,躲闪方分数不变。 + +A: a选择进攻 +B: a选择躲闪 +C: b选择进攻 +D: b选择躲闪 +------------------------------------------------------------ +题号:#t6 +出题者:剩菜剩饭 +[其他/废弃] 题目:HP杀 + + 选择一项,若死亡则不得分,但仍执行攻击效果 + +A: 大狼:+1分,每有一只小狼+1分。若大狼只有 1 只,神职死亡 +B: 小狼:+1分,选择的人数比神职多时平民死亡,若大狼存活,额外+1分 +C: 神职:+1分,选择人数比小狼多时,大狼死亡,此项额外+1分 +D: 平民:+3分,若选择的人数比神职多,平民死亡 +E: 第三方:+0分,若狼人方和好人方都有阵亡消息,你+4分 +------------------------------------------------------------ +题号:#t7 +出题者:An idle brain +[其他/废弃] 题目:未命名 + + 选择一项。 + +A: +1,若有人选B,则改为获得 [D-B] 人数的分数 +B: +1.5,若有人选C,则改为获得 [A-C] 人数的分数 +C: +2,若有人选D,则改为获得 [B-D] 人数的分数 +D: +2.5,若有人选A,则改为获得 [C-A] 人数的分数 +------------------------------------------------------------ +题号:#t8 +出题者:圣墓上的倒吊人 +[其他/废弃] 题目:让步【废弃】 + + 如果所有人都选择一个选项,那只执行它。否则执行被选的加分最少的选项,且选择该选项玩家+2,其他人-2。 + +A: +0 +B: +1 +C: +2 +D: +3 +------------------------------------------------------------ +题号:#t9 +出题者:Q群管家 +[其他/废弃] 题目:奇偶1【废弃】 + + 对应选项选择人数奇数取反,偶数取正。 + +A: +3 +B: 0 +------------------------------------------------------------ +题号:#t10 +出题者:Q群管家 +[其他/废弃] 题目:奇偶3【废弃】 + + 选项得分与选择人数之和若为奇数则得分取反。 + +A: +1 +B: +2 +C: +3 +------------------------------------------------------------ +题号:#t11 +出题者:Q群管家 +[其他/废弃] 题目:奇偶4【废弃】 + + 一张正面是+2,背面是-2的卡牌,初始正面向上,最终状态是本题得分,你选择: + +A: 翻面,你加对应的分数 +B: 不翻面,你减对应的分数 +------------------------------------------------------------ +题号:#t12 +出题者:圣墓上的倒吊人 +[其他/废弃] 题目:资源配置 + + 桌上有 20 个资源,一资源一分,如果投入后总资源超过了 32 ,那获得(你的减分/所有玩家的减分)比例的总资源。如果投入后分数为负数,则选择无效。 + +A: 0 +B: -1 +C: -2 +D: -4 +E: -6 +F: 1.6%获得投入后总资源,否则-15 +------------------------------------------------------------ +题号:#t13 +出题者:飘渺 +[其他/废弃] 题目:电车难题 + + 请选择你的位置。拉杆只有2个状态,即拉动偶数次会还原拉杆 + +A: 乘客:+0.5,但每有一个人拉动拉杆-0.5 +B: 多数派:+0.5,如果拉杆最终没有拉动,改为-2 +C: 少数派:+0.5,如果拉杆最终拉动,改为-2.5 +D: -0.5,每一个选择此项的人都会拉动一次拉杆 +------------------------------------------------------------ +题号:#t14 +出题者:Q群管家 +[其他/废弃] 题目:均分1 + + 选择一项。 + +A: 当前总积分×0.5 +B: 选择此选项的玩家均分他们的分数 +C: 当前总积分×(-0.5) +------------------------------------------------------------ +题号:#t15 +出题者:Q群管家 +[其他/废弃] 题目:均分2 + + 选择一项。 + +A: 选择此选项的分数最高与最低的玩家均分他们的分数 +B: 正分的-2,负分的+2 +------------------------------------------------------------ +题号:#t16 +出题者:Q群管家 +[其他/废弃] 题目:奇偶5 + + 选择一项。 + +A: 若选择人数为奇数,扣除 [选择本选项人数/2] 的分数;否则获得相应的分数 +B: 0 +------------------------------------------------------------ +题号:#t17 +出题者:胡扯弟 +[其他/废弃] 题目:云顶之巢 + + 选择你的云巢身份。 + +A: YAMI:总是能+2分的大佬,但是有5%的可能性失手-1 +B: 飘渺:10%+24的赌博爱好者 +C: 黑桃3:+13,但每有一个飘渺-1.75分 +D: 飞机:+12,但有YAMI就会被gank +E: 西东:+A+B-C+D +F: 胡扯:-114514,但有2.5%的可能性改为+1919810 +------------------------------------------------------------ +题号:#t18 +出题者:圣墓上的倒吊人 +[其他/废弃] 题目:未来计划 + + (X)代表有且只有 X 人选择该选项时,该选项才能执行。执行顺序为序号顺序。无选项执行时执行E + +A: +4(1) +B: +2(2) +C: -3(3) +D: -1(≥2) +E: 所有玩家的分数变为相反数(4) +------------------------------------------------------------ +题号:#t19 +出题者:圣墓上的倒吊人 +[其他/废弃] 题目:真理 + + 真理掌握在? + +A: 少数人:+3,如果此项选择人数最多,则改为减 [选择该项人数] 的分数 +B: 多数人:-2 +------------------------------------------------------------ +题号:#t20 +出题者:丘陵萨满 +[其他/废弃] 题目:伊甸园 + + 获胜的选项获得 [选择非此选项人数] 的分数,失败的选项失去 [选择获胜选项人数] 的分数 + +A: 红苹果:如果分数小于等于 0 的玩家都选择红苹果,红苹果获胜。但如果所有人都选择此项,全员分数取反 +B: 银苹果:如果选择此项的人数少于 C,且红苹果没有获胜,银苹果获胜。 +C: 金苹果:如果选择此项的人数少于等于 B,且红苹果没有获胜,金苹果获胜。 +------------------------------------------------------------ +题号:#t21 出题者:圣墓上的倒吊人 -[测试] 题目:税收 +[其他/废弃] 题目:战争中的贵族 + + 你的国家与敌国正在战争中,选择一项行动。 + +A: 继续抵抗:+5,如果有玩家选择了B,那选择此项的人分数归零 +B: 有条件投降:-3 +C: 润出国外:如果有人选 A,则-2 +------------------------------------------------------------ +题号:#t22 +出题者:飞机鸭卖蛋 +[其他/废弃] 题目:太多挂机A了…? + + 分别记选择A、B、C选项的人数为a、b、c: + +A: 获得 a 分 +B: 获得 a×1.5-b 分 +C: 获得 a×2-c 分 +------------------------------------------------------------ +题号:#t23 +出题者:圣墓上的倒吊人 +[其他/废弃] 题目:税收  选择一项。 A: 减45%分数 B: 减 [B选项人数] 的分数 C: 减 [C选项人数*1.5] 的分数 +------------------------------------------------------------ +题号:#t24 +出题者:Chance +[其他/废弃] 题目:我是大土块 + + 选择一项。 + +A: +2,有 [(B+C) ×10]% 的概率-4 +B: +1,有 [(A-B) ×10]% 的概率使C+2 +C: -2,有 [(A+D) ×10]% 的概率+6 +D: -3,有 [(A+B-D) ×10]% 的概率使A和B-4,D+5 diff --git a/games/crowd/unittest.cc b/games/crowd/unittest.cc index 377676a8..0dc28c8b 100644 --- a/games/crowd/unittest.cc +++ b/games/crowd/unittest.cc @@ -1601,7 +1601,7 @@ GAME_TEST(5, Q37_Basic_ADDDD_AABDD){ ASSERT_PRI_MSG(OK, 2, "B"); ASSERT_PRI_MSG(OK, 3, "D"); ASSERT_PRI_MSG(CHECKOUT, 4, "D"); - ASSERT_SCORE(50,0,50,100,100);} + ASSERT_SCORE(150,100,50,100,100);} GAME_TEST(5, Q37_Basic_CDDDD_AAAAB_AAABB){ ASSERT_PUB_MSG(OK, 0, "回合数 3"); ASSERT_PUB_MSG(OK, 0, "测试 37"); StartGame(); @@ -2629,7 +2629,7 @@ GAME_TEST(6, Q71_Basic_ACDDEE){ ASSERT_PRI_MSG(OK, 3, "D"); ASSERT_PRI_MSG(OK, 4, "E"); ASSERT_PRI_MSG(CHECKOUT, 5, "E"); - ASSERT_SCORE(0,0,-100,-100,100,100);} + ASSERT_SCORE(0,300,-100,-100,100,100);} GAME_TEST(6, Q72_Basic_AABBDE){ ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 72"); StartGame(); @@ -2661,6 +2661,218 @@ GAME_TEST(6, Q72_Basic_ABBCDE){ ASSERT_PRI_MSG(CHECKOUT, 5, "E"); ASSERT_SCORE(0,200,200,-200,100,200);} +GAME_TEST(5, Q73_Basic_AABCD){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 73"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "A"); + ASSERT_PRI_MSG(OK, 2, "B"); + ASSERT_PRI_MSG(OK, 3, "C"); + ASSERT_PRI_MSG(CHECKOUT, 4, "D"); + ASSERT_SCORE(100,100,200,-300,0);} + +GAME_TEST(5, Q73_Basic_ABBCD){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 73"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "B"); + ASSERT_PRI_MSG(OK, 2, "B"); + ASSERT_PRI_MSG(OK, 3, "C"); + ASSERT_PRI_MSG(CHECKOUT, 4, "D"); + ASSERT_SCORE(100,-200,-200,400,100);} + +GAME_TEST(5, Q73_Basic_AABBC){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 73"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "A"); + ASSERT_PRI_MSG(OK, 2, "B"); + ASSERT_PRI_MSG(OK, 3, "B"); + ASSERT_PRI_MSG(CHECKOUT, 4, "C"); + ASSERT_SCORE(100,100,200,200,-300);} + +GAME_TEST(6, Q74_Basic_ABBBCD){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 74"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "B"); + ASSERT_PRI_MSG(OK, 2, "B"); + ASSERT_PRI_MSG(OK, 3, "B"); + ASSERT_PRI_MSG(OK, 4, "C"); + ASSERT_PRI_MSG(CHECKOUT, 5, "D"); + ASSERT_SCORE(-100,-200,-200,-200,700,300);} + +GAME_TEST(6, Q74_Basic_ABCCDD){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 74"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "B"); + ASSERT_PRI_MSG(OK, 2, "C"); + ASSERT_PRI_MSG(OK, 3, "C"); + ASSERT_PRI_MSG(OK, 4, "D"); + ASSERT_PRI_MSG(CHECKOUT, 5, "D"); + ASSERT_SCORE(-100,0,50,50,-300,-300);} + +GAME_TEST(6, Q74_Basic_BCCCCD){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 74"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "B"); + ASSERT_PRI_MSG(OK, 1, "C"); + ASSERT_PRI_MSG(OK, 2, "C"); + ASSERT_PRI_MSG(OK, 3, "C"); + ASSERT_PRI_MSG(OK, 4, "C"); + ASSERT_PRI_MSG(CHECKOUT, 5, "D"); + ASSERT_SCORE(0,-300,-300,-300,-300,300);} + +GAME_TEST(4, Q75_All_A){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 75"); StartGame(); + for(int i = 0; i < 3; i++) ASSERT_PRI_MSG(OK, i, "A"); + ASSERT_PRI_MSG(CHECKOUT, 3, "A"); + ASSERT_SCORE(200,200,200,200);} + +GAME_TEST(4, Q75_All_B){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 75"); StartGame(); + for(int i = 0; i < 3; i++) ASSERT_PRI_MSG(OK, i, "B"); + ASSERT_PRI_MSG(CHECKOUT, 3, "B"); + ASSERT_SCORE(100,100,100,100);} + +GAME_TEST(4, Q75_All_C){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 75"); StartGame(); + for(int i = 0; i < 3; i++) ASSERT_PRI_MSG(OK, i, "C"); + ASSERT_PRI_MSG(CHECKOUT, 3, "C"); + ASSERT_SCORE(300,300,300,300);} + +GAME_TEST(4, Q75_Basic_AACC){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 75"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "A"); + ASSERT_PRI_MSG(OK, 2, "C"); + ASSERT_PRI_MSG(CHECKOUT, 3, "C"); + ASSERT_SCORE(200,200,-100,-100);} + +GAME_TEST(4, Q75_Basic_ABCC){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 75"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "B"); + ASSERT_PRI_MSG(OK, 2, "C"); + ASSERT_PRI_MSG(CHECKOUT, 3, "C"); + ASSERT_SCORE(0,100,-100,-100);} + +GAME_TEST(4, Q75_Basic_BBCC){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 75"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "B"); + ASSERT_PRI_MSG(OK, 1, "B"); + ASSERT_PRI_MSG(OK, 2, "C"); + ASSERT_PRI_MSG(CHECKOUT, 3, "C"); + ASSERT_SCORE(100,100,300,300);} + +GAME_TEST(5, Q76_Basic_AAABC){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 76"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "A"); + ASSERT_PRI_MSG(OK, 2, "A"); + ASSERT_PRI_MSG(OK, 3, "B"); + ASSERT_PRI_MSG(CHECKOUT, 4, "C"); + ASSERT_SCORE(0,0,0,400,300);} + +GAME_TEST(5, Q76_Basic_AABBC){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 76"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "A"); + ASSERT_PRI_MSG(OK, 2, "B"); + ASSERT_PRI_MSG(OK, 3, "B"); + ASSERT_PRI_MSG(CHECKOUT, 4, "C"); + ASSERT_SCORE(0,0,-100,-100,300);} + +GAME_TEST(5, Q76_Basic_AABCD){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 76"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "A"); + ASSERT_PRI_MSG(OK, 2, "B"); + ASSERT_PRI_MSG(OK, 3, "C"); + ASSERT_PRI_MSG(CHECKOUT, 4, "D"); + ASSERT_SCORE(0,0,-100,300,200);} + +GAME_TEST(5, Q76_Basic_BCDDD){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 76"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "B"); + ASSERT_PRI_MSG(OK, 1, "C"); + ASSERT_PRI_MSG(OK, 2, "D"); + ASSERT_PRI_MSG(OK, 3, "D"); + ASSERT_PRI_MSG(CHECKOUT, 4, "D"); + ASSERT_SCORE(-100,-200,-300,-300,-300);} + +GAME_TEST(5, Q77_Basic_ABBCD){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 77"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "B"); + ASSERT_PRI_MSG(OK, 2, "B"); + ASSERT_PRI_MSG(OK, 3, "C"); + ASSERT_PRI_MSG(CHECKOUT, 4, "D"); + ASSERT_SCORE(300,100,100,-100,200);} + +GAME_TEST(5, Q77_Basic_BBCCD){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 77"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "B"); + ASSERT_PRI_MSG(OK, 1, "B"); + ASSERT_PRI_MSG(OK, 2, "C"); + ASSERT_PRI_MSG(OK, 3, "C"); + ASSERT_PRI_MSG(CHECKOUT, 4, "D"); + ASSERT_SCORE(0,0,400,400,0);} + +GAME_TEST(5, Q77_Basic_AABBD){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 77"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "A"); + ASSERT_PRI_MSG(OK, 2, "B"); + ASSERT_PRI_MSG(OK, 3, "B"); + ASSERT_PRI_MSG(CHECKOUT, 4, "D"); + ASSERT_SCORE(0,0,100,100,0);} + +GAME_TEST(6, Q78_Basic_ACCCDE){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 78"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "C"); + ASSERT_PRI_MSG(OK, 2, "C"); + ASSERT_PRI_MSG(OK, 3, "C"); + ASSERT_PRI_MSG(OK, 4, "D"); + ASSERT_PRI_MSG(CHECKOUT, 5, "E"); + ASSERT_SCORE(300,0,0,0,500,500);} + +GAME_TEST(6, Q78_Basic_ABCDDE){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 78"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "B"); + ASSERT_PRI_MSG(OK, 2, "C"); + ASSERT_PRI_MSG(OK, 3, "D"); + ASSERT_PRI_MSG(OK, 4, "D"); + ASSERT_PRI_MSG(CHECKOUT, 5, "E"); + ASSERT_SCORE(300,150,100,0,0,0);} + +GAME_TEST(6, Q78_Basic_AABCCE){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 78"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "A"); + ASSERT_PRI_MSG(OK, 2, "B"); + ASSERT_PRI_MSG(OK, 3, "C"); + ASSERT_PRI_MSG(OK, 4, "C"); + ASSERT_PRI_MSG(CHECKOUT, 5, "E"); + ASSERT_SCORE(300,300,150,0,0,0);} + +GAME_TEST(6, Q78_Basic_AAADDE){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 78"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "A"); + ASSERT_PRI_MSG(OK, 2, "A"); + ASSERT_PRI_MSG(OK, 3, "D"); + ASSERT_PRI_MSG(OK, 4, "D"); + ASSERT_PRI_MSG(CHECKOUT, 5, "E"); + ASSERT_SCORE(0,0,0,250,250,500);} + +GAME_TEST(6, Q78_Basic_AAACCE){ + ASSERT_PUB_MSG(OK, 0, "回合数 1"); ASSERT_PUB_MSG(OK, 0, "测试 78"); StartGame(); + ASSERT_PRI_MSG(OK, 0, "A"); + ASSERT_PRI_MSG(OK, 1, "A"); + ASSERT_PRI_MSG(OK, 2, "A"); + ASSERT_PRI_MSG(OK, 3, "C"); + ASSERT_PRI_MSG(OK, 4, "C"); + ASSERT_PRI_MSG(CHECKOUT, 5, "E"); + ASSERT_SCORE(0,0,0,200,200,500);} + } // namespace GAME_MODULE_NAME } // namespace game diff --git a/games/dvalue_tender/mygame.cc b/games/dvalue_tender/mygame.cc index fd5c2938..8532e16a 100644 --- a/games/dvalue_tender/mygame.cc +++ b/games/dvalue_tender/mygame.cc @@ -25,8 +25,8 @@ const GameProperties k_properties { .developer_ = "睦月", .description_ = "简单的拼点游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 1; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 1; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; @@ -49,7 +49,7 @@ std::string myToStrR(int x) return ret; } -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏必须 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -60,7 +60,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; diff --git a/games/ecard/mygame.cc b/games/ecard/mygame.cc index 3ce6afed..5db2da92 100644 --- a/games/ecard/mygame.cc +++ b/games/ecard/mygame.cc @@ -24,12 +24,12 @@ const GameProperties k_properties { .developer_ = "铁蛋", .description_ = "从皇帝、奴隶、市民中选择身份,在不公平的对局中取胜", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 1; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 1; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -52,7 +52,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("设定游戏模式", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options, const uint32_t& mode, const string& single_mode) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const uint32_t& mode, const string& single_mode) { if (mode == 100) { if (single_mode == "经典") GET_OPTION_VALUE(game_options, 模式) = 0; diff --git a/games/expedition/mygame.cc b/games/expedition/mygame.cc index 84b05bda..6141bcba 100644 --- a/games/expedition/mygame.cc +++ b/games/expedition/mygame.cc @@ -27,8 +27,8 @@ const GameProperties k_properties { .developer_ = "dva", .description_ = "通过计算和放置数字,争取分数的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) { return 2; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return 2; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; const std::array map_files = {"random", "wang_guo_bian_jing.txt", @@ -44,11 +44,11 @@ std::map char_op = { {">", 3}, {"大", 3}, {"<", 4}, {"<", 4}, {"小", 4}, }; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { return true; } +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { return true; } const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 6; return NewGameMode::SINGLE_USER; diff --git a/games/gallop_horse_racing/mygame.cc b/games/gallop_horse_racing/mygame.cc index d3a9decb..2b6eadb0 100644 --- a/games/gallop_horse_racing/mygame.cc +++ b/games/gallop_horse_racing/mygame.cc @@ -27,15 +27,15 @@ const GameProperties k_properties { .developer_ = "铁蛋", .description_ = "扮演骑马手,完成目标距离并尽可能获得更高的名次", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 10; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) +uint64_t MaxPlayerNum(const CustomOptions& options) { return 10; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return ceil(GET_OPTION_VALUE(options, 目标) / 10.0 / GET_OPTION_VALUE(options, 上限)); } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 5) { reply() << "该游戏至少 5 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -46,7 +46,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 5; return NewGameMode::SINGLE_USER; diff --git a/games/garden_of_eden/mygame.cc b/games/garden_of_eden/mygame.cc index d4ad1a17..0116e9d0 100644 --- a/games/garden_of_eden/mygame.cc +++ b/games/garden_of_eden/mygame.cc @@ -25,12 +25,12 @@ const GameProperties k_properties { .developer_ = "森高", .description_ = "吃下禁果,获取分数的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return GET_OPTION_VALUE(options, 回合数) / 6; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return GET_OPTION_VALUE(options, 回合数) / 6; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 3) { reply() << "该游戏至少 3 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -41,7 +41,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 10; return NewGameMode::SINGLE_USER; @@ -100,7 +100,7 @@ static const char* AppleTypeName(const AppleType type) struct Player { - Player(const MyGameOptions& option) + Player(const CustomOptions& option) : score_(GET_OPTION_VALUE(option, 回合数), 0) , remain_golden_(GET_OPTION_VALUE(option, 金苹果)) , chosen_apples_(GET_OPTION_VALUE(option, 回合数), AppleType::RED) // RED is the default apple diff --git a/games/gold_coins/mygame.cc b/games/gold_coins/mygame.cc index c45cafb8..9a060bf1 100644 --- a/games/gold_coins/mygame.cc +++ b/games/gold_coins/mygame.cc @@ -24,9 +24,10 @@ const GameProperties k_properties { .name_ = "爆金币", // the game name which should be unique among all the games .developer_ = "铁蛋", .description_ = "选择行动,与他人博弈抢夺金币的游戏", + .shuffled_player_id_ = true, }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 1; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 1; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options{ .is_formal_{false}, }; @@ -38,7 +39,7 @@ const char* const settlement_details[7] = { R"EOF(「撤离」最先判定 - 在当轮[无敌],本轮其他玩家对撤离玩家的任何操作都会[无效化](也不会花费金币) - - 在玩家撤离当轮,会返还曾对此玩家夺血条的一半金币)EOF", + - 在玩家撤离当轮,会返还曾对此玩家夺血条的一半金币(向下取整))EOF", R"EOF(「捡金币」在「撤离」后判定 - 某一数值金币不够发放时,大于等于此数值都会判定[失败])EOF", @@ -70,7 +71,7 @@ const std::vector k_rule_commands = { AlterChecker({{"判定", 0}, {"撤离", 1}, {"捡金币", 2}, {"夺血条", 3}, {"守金币", 4}, {"抢金币", 5}, {"爆金币", 6}})), }; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 4) { reply() << "该游戏至少 4 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -81,7 +82,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 5; return NewGameMode::SINGLE_USER; @@ -118,6 +119,11 @@ class MainStage : public MainGameStage vector player_scores_; int round_; + + // 部分行动的金币数限制(撒币模式发生变化) + int Pick_Up_limit[2] = {0, 5}; + int Snatch_limit[2] = {1, 5}; + int Guard_limit[2] = {1, 5}; vector player_hp_; // 生命值 vector player_coins_; // 金币数 @@ -171,13 +177,13 @@ class RoundStage : public SubGameStage<> RoundStage(MainStage& main_stage, const uint64_t round) : StageFsm(main_stage, "第 " + std::to_string(round) + " 回合", MakeStageCommand(*this, "捡金币", &RoundStage::Pick_Up_Coins_, - VoidChecker("捡"), ArithChecker(0, 5, "金币数")), + VoidChecker("捡"), ArithChecker(main_stage.Pick_Up_limit[0], main_stage.Pick_Up_limit[1], "金币数")), MakeStageCommand(*this, "抢金币", &RoundStage::Snatch_Coins_, - VoidChecker("抢"), ArithChecker(1, main_stage.Global().PlayerNum(), "对象号码"), ArithChecker(1, 5, "金币数")), + VoidChecker("抢"), ArithChecker(1, main_stage.Global().PlayerNum(), "目标"), ArithChecker(main_stage.Snatch_limit[0], main_stage.Snatch_limit[1], "金币数")), MakeStageCommand(*this, "守金币", &RoundStage::Guard_Coins_, - VoidChecker("守"), ArithChecker(1, main_stage.Global().PlayerNum(), "对象号码"), ArithChecker(1, 5, "金币数")), + VoidChecker("守"), ArithChecker(1, main_stage.Global().PlayerNum(), "目标"), ArithChecker(main_stage.Guard_limit[0], main_stage.Guard_limit[1], "金币数")), MakeStageCommand(*this, "夺血条", &RoundStage::Take_HP_, - VoidChecker("夺"), ArithChecker(1, main_stage.Global().PlayerNum(), "对象号码"), ArithChecker(1, 5, "金币数")), + VoidChecker("夺"), ArithChecker(1, main_stage.Global().PlayerNum(), "目标"), ArithChecker(1, 5, "金币数")), MakeStageCommand(*this, "撤离", &RoundStage::Leave_, VoidChecker("撤离"))) {} @@ -330,14 +336,15 @@ class RoundStage : public SubGameStage<> if (Global().IsReady(pid)) { return StageErrCode::OK; } - if ((Main().player_coins_[pid] >= 25 && rand() % 3 < 2) || (Main().player_hp_[pid] <= 5 && rand() % 10 == 0)) { + if (((Main().player_coins_[pid] >= 25 && rand() % 3 < 2) || (Main().player_hp_[pid] <= 5 && rand() % 10 == 0)) && + (GAME_OPTION(特殊规则) != 3 || (Main().player_coins_[pid] >= 60 && rand() % 3 < 2))) { Selected_(pid, reply, 'L', pid + 1, 0); } else if (Main().alive_ == 1) { Selected_(pid, reply, 'P', pid + 1, Main().round_coin); } else { int rd = rand() % 100; int target; - int coinselect = rand() % 5 + 1; + int coinselect = 0; char action; do { target = rand() % Global().PlayerNum() + 1; @@ -354,8 +361,23 @@ class RoundStage : public SubGameStage<> if (rd < 60) { action = 'P'; } else if (rd < 100) { action = 'S'; } } - if (action == 'P' && rand() % 10 == 0) { - coinselect = 0; + // 随机金币数 + if (action == 'P') { + if (GAME_OPTION(特殊规则) == 3) { + coinselect = rand() % (Main().round_coin / 3) + 1; + } else { + coinselect = rand() % 5 + 1; + } + if (rand() % 10 == 0) coinselect = 0; + } + if (action == 'S') { + coinselect = rand() % (Main().Snatch_limit[1] - Main().Snatch_limit[0] + 1) + Main().Snatch_limit[0]; + } + if (action == 'G') { + coinselect = rand() % (Main().Guard_limit[1] - Main().Guard_limit[0] + 1) + Main().Guard_limit[0]; + } + if (action == 'T') { + coinselect = rand() % 5 + 1; } Selected_(pid, reply, action, target, coinselect); } @@ -412,7 +434,6 @@ class RoundStage : public SubGameStage<> vector takehp_success(Global().PlayerNum(), false); // 玩家执行夺血条是否显示成功 string round_details = ""; // 展示回合判定细节 + 下回合金币数 - vector pickCount = {0, 0, 0, 0, 0, 0}; const vector action = Main().player_action_; const vector target = Main().player_target_; const vector coinselect = Main().player_coinselect_; @@ -420,7 +441,19 @@ class RoundStage : public SubGameStage<> // 毒雾模式回合扣血 if (GAME_OPTION(特殊规则) == 2) { for (int i = 0; i < Global().PlayerNum(); i++) { - Main().player_hp_[i] -= 2; + if (Main().player_out_[i] == 0) { + Main().player_hp_[i] -= 2; + } + } + } + // 撒币模式回合扣金币 + int vault_coins = 0; + if (GAME_OPTION(特殊规则) == 3) { + for (int i = 0; i < Global().PlayerNum(); i++) { + if (Main().player_out_[i] == 0) { + vault_coins += Main().player_coins_[i] / 4; + Main().player_coins_[i] -= Main().player_coins_[i] / 4; + } } } @@ -442,15 +475,19 @@ class RoundStage : public SubGameStage<> // 捡金币【P】 int coins, pickStop; + vector pickCount; coins = Main().round_coin; - pickStop = 6; + pickStop = Main().Pick_Up_limit[1] + 1; + for (int i = 0; i < pickStop; i++) { + pickCount.push_back(0); + } for (int i = 0; i < Global().PlayerNum(); i++) { if (action[i] == 'P' && Main().player_out_[i] == 0) { pickCount[coinselect[i]]++; } } - for (int i = 1; i <= 5; i++) { - if(coins >= pickCount[i] * i) { + for (int i = 1; i <= Main().Pick_Up_limit[1]; i++) { + if (coins >= pickCount[i] * i) { coins -= pickCount[i] * i; } else { pickStop = i; @@ -464,6 +501,10 @@ class RoundStage : public SubGameStage<> pickcoins_success[i] = true; } } + // 剩余金币进入撒币模式金库 + if (GAME_OPTION(特殊规则) == 3) { + vault_coins += coins; + } // 夺血条【T】(每个玩家依次判定) for (int i = 0; i < Global().PlayerNum(); i++) { @@ -481,7 +522,7 @@ class RoundStage : public SubGameStage<> // 找到守护最大值 for (int j = 0; j < Global().PlayerNum(); j++) { if (action[j] == 'G' && target[j] == i && Main().player_out_[j] == 0) { - max_guard = max(max_guard, coinselect[i]); + max_guard = max(max_guard, coinselect[j]); // 毒雾模式守护奖励 if (GAME_OPTION(特殊规则) == 2 && is_takenhp) { Main().player_hp_[j] += coinselect[j]; @@ -673,7 +714,12 @@ class RoundStage : public SubGameStage<> if (Main().alive_ > 0 && Main().round_ < GAME_OPTION(回合数)) { Main().round_coin = rand() % (Main().alive_ + 1) + Main().alive_ * 2; - round_details += "· 本轮金币数:" + to_string(Main().round_coin) + "
"; + if (GAME_OPTION(特殊规则) == 3) { + Main().round_coin += vault_coins; + round_details += "· 金库金币数:" + to_string(Main().round_coin) + "
"; + } else { + round_details += "· 本轮金币数:" + to_string(Main().round_coin) + "
"; + } } round_details += "
"; @@ -755,8 +801,13 @@ void MainStage::FirstStageFsm(SubStageFsmSetter setter) for (int i = 0; i < Global().PlayerNum(); i++) { player_hp_[i] = 10; player_last_hp_[i] = 10; - player_coins_[i] = (GAME_OPTION(特殊规则) == 1) ? 0 : GAME_OPTION(金币); - player_last_coins_[i] = (GAME_OPTION(特殊规则) == 1) ? 0 : GAME_OPTION(金币); + if (GAME_OPTION(特殊规则) == 1) { + player_coins_[i] = player_last_coins_[i] = 0; + } else if (GAME_OPTION(特殊规则) == 3) { + player_coins_[i] = player_last_coins_[i] = 40; + } else { + player_coins_[i] = player_last_coins_[i] = GAME_OPTION(金币); + } player_total_damage_[i].resize(Global().PlayerNum()); } @@ -792,9 +843,8 @@ void MainStage::FirstStageFsm(SubStageFsmSetter setter) PreBoard += "本局玩家序号如下:\n"; for (int i = 0; i < Global().PlayerNum(); i++) { PreBoard += to_string(i + 1) + " 号:" + Global().PlayerName(i); - if (i != (int)Global().PlayerNum() - 1) { - PreBoard += "\n"; + PreBoard += "\n"; } } @@ -805,6 +855,14 @@ void MainStage::FirstStageFsm(SubStageFsmSetter setter) Global().Boardcast() << "特殊规则——[狂热模式]\n取消所有【限制】,初始金币为0"; } else if (GAME_OPTION(特殊规则) == 2) { Global().Boardcast() << "特殊规则——[毒雾模式]\n所有玩家每轮减少2血。如果选择守,且对方当轮被夺血条,回复你花费金币数量的血量。本局分数结算改为:金币数 + 存活轮数"; + } else if (GAME_OPTION(特殊规则) == 3) { + Pick_Up_limit[1] = 1000; + Snatch_limit[1] = 20; + Guard_limit[0] = 10; + Guard_limit[1] = 25; + Global().Boardcast() << "特殊规则——[撒币模式]\n每轮每人减少1/4当前持有金币(向上取整),进入下轮金库。初始金币40,捡金币范围改为0-1000,抢金币改为1-20,守金币花费改为10-25,上轮金库没发放完全会进入下一轮金库"; + } else if (GAME_OPTION(特殊规则) == 4) { + Global().Boardcast() << "特殊规则——[HP杀模式]\n???"; } setter.Emplace(*this, ++round_); @@ -825,11 +883,10 @@ void MainStage::NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, for(int i = 0; i < Global().PlayerNum(); i++) { player_scores_[i] = player_coins_[i]; - if (player_out_[i] > 0) { - player_scores_[i] += (GAME_OPTION(特殊规则) == 2) ? player_alive_round_[i] : -ceil(player_alive_round_[i] * 0.5); - } else { - player_scores_[i] += (GAME_OPTION(特殊规则) == 2) ? round_ - 1 : -ceil((round_ - 1) * 0.5); + if (player_out_[i] == 0) { + player_alive_round_[i] = GAME_OPTION(回合数); } + player_scores_[i] += (GAME_OPTION(特殊规则) == 2) ? player_alive_round_[i] : -ceil(player_alive_round_[i] * 0.5); } // Returning empty variant means the game will be over. return; diff --git a/games/gold_coins/options.h b/games/gold_coins/options.h index 97d7405a..ebd79f84 100644 --- a/games/gold_coins/options.h +++ b/games/gold_coins/options.h @@ -1,4 +1,4 @@ EXTEND_OPTION("每回合时间限制", 时限, (ArithChecker(10, 3600, "超时时间(秒)")), 120) EXTEND_OPTION("回合数", 回合数, (ArithChecker(1, 15, "回合数")), 10) -EXTEND_OPTION("玩家初始金币数", 金币, (ArithChecker(0, 20, "血量")), 10) -EXTEND_OPTION("开启特殊规则", 特殊规则, (ArithChecker(0, 2, "特殊规则")), 0) +EXTEND_OPTION("玩家初始金币数", 金币, (ArithChecker(0, 20, "金币")), 10) +EXTEND_OPTION("开启特殊规则", 特殊规则, (AlterChecker({{"无", 0}, {"狂热", 1}, {"毒雾", 2}, {"撒币", 3}})), 0) diff --git a/games/hex/achievements.h b/games/hex/achievements.h new file mode 100644 index 00000000..e69de29b diff --git a/games/hex/board.h b/games/hex/board.h new file mode 100644 index 00000000..21cef1af --- /dev/null +++ b/games/hex/board.h @@ -0,0 +1,239 @@ +#include + +const string color_en[3] = {"gainsboro", "red", "#0078FF"}; +const string color_ch[3] = {"灰", "红", "蓝"}; + +class Board +{ +public: + // 玩家昵称 + string name[2]; + // 颜色对应的玩家编号 + PlayerID player_color[2]; + // 棋盘边长 + int size = 9; + // 棋盘 + int chess[30][30]; + + void Initialize() { + for(int i = 0; i < size; i++) { + for(int j = 0; j < size; j++) { + chess[i][j] = 0; + } + } + } + + // 六边形样式 + inline static const string style = R"EOF( +)EOF"; + + // 构造UI + string GetUI(int currentPlayer) { + string UI = style; + + // 玩家信息 + string bg0 = "", bg1 = ""; + if (currentPlayer == 0) bg0 = " bgcolor=\"" + color_en[0] + "\""; + if (currentPlayer == 1) bg1 = " bgcolor=\"" + color_en[0] + "\""; + UI += ""; + UI += " " + name[0] + ""; + UI += ""; + UI += " " + name[1] + ""; + UI += "
   
"; + + // 棋盘 + UI += "
"; + UI += "
"; + UI += "
"; + UI += "
"; + UI += "
"; + UI += "
"; + UI += "
"; + UI += "
"; + // 根据对角线遍历 + int num = 1; + for (int d = 0; d < 2 * size - 1; ++d) { + UI += "
"; + if (d <= size - 1) { + UI += "
"; + } else { + UI += "
"; + } + for (int i = 0; i <= d; ++i) { + int j = d - i; + if (i < size && j < size) { + UI += "
" + to_string(num++) + "
"; + } + } + if (d < size - 1) { + UI += "
"; + } else { + UI += "
"; + } + UI += "
"; + } + UI += "
"; + UI += "
"; + UI += "
"; + UI += "
"; + UI += "
"; + UI += "
"; + UI += "
"; + UI += "
"; + + return UI; + } + + // 将数字位置转为一个位置pair + pair TranNum(int num) + { + int c = 1; + for (int d = 0; d < 2 * size - 1; ++d) { + for (int i = 0; i <= d; ++i) { + int j = d - i; + if (i < size && j < size) { + if (c == num) { + return {i, j}; + } + c++; + } + } + } + return {-1, -1}; + } + + string PlaceChess(int num, PlayerID player) + { + if (num > size * size || num < 1) { + return "[错误] 数字位置超出棋盘大小"; + } + auto pos = TranNum(num); + int x = pos.first, y = pos.second; + if (x == -1) { + return "[错误] 数字坐标转换时发生了未知错误,请联系管理员中断游戏!"; + } + if(chess[x][y] != 0) { + return "[错误] 这个位置已经有棋子了"; + } + chess[x][y] = player_color[player]; + return "OK"; + } + + void SwapColor() { + player_color[0] = 3 - player_color[0]; + player_color[1] = 3 - player_color[1]; + } + + int WinCheck() { + // 检查 1红色 是否连通上下两端 + for (int col = 0; col < size; ++col) { + if (chess[0][col] == 1 && dfs(1, 0, col)) { + return 1; + } + } + // 检查 2蓝色 是否连通左右两端 + for (int row = 0; row < size; ++row) { + if (chess[row][0] == 2 && dfs(2, row, 0)) { + return 2; + } + } + return -1; + } + + bool dfs(int player, int x, int y) { + bool visited[30][30] = {false}; + stack> s; + s.push({x, y}); + visited[x][y] = true; + + while (!s.empty()) { + auto [cx, cy] = s.top(); s.pop(); + + // 检查是否到达目标边界 + if (player == 1 && cx == size - 1) return true; // 1红色 连接上下两端 + if (player == 2 && cy == size - 1) return true; // 2蓝色 连接左右两端 + + // 遍历六个相邻格子 + for (auto [nx, ny] : getNeighbors(cx, cy)) { + if (!visited[nx][ny] && chess[nx][ny] == player) { + visited[nx][ny] = true; + s.push({nx, ny}); + } + } + } + + return false; + } + + vector> getNeighbors(int x, int y) { + vector> neighbors; + vector> directions = { + {-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, 1}, {1, -1} + }; + for (auto [dx, dy] : directions) { + int nx = x + dx, ny = y + dy; + if (nx >= 0 && nx < size && ny >= 0 && ny < size) { + neighbors.push_back({nx, ny}); + } + } + return neighbors; + } + +}; diff --git a/games/hex/icon.png b/games/hex/icon.png new file mode 100644 index 00000000..f2e4e1ef Binary files /dev/null and b/games/hex/icon.png differ diff --git a/games/hex/mygame.cc b/games/hex/mygame.cc new file mode 100644 index 00000000..b618f69f --- /dev/null +++ b/games/hex/mygame.cc @@ -0,0 +1,258 @@ +// Copyright (c) 2018-present, JiaQi Yu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include "game_framework/stage.h" +#include "game_framework/util.h" +#include "utility/html.h" + +using namespace std; + +#include "board.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +class MainStage; +template using SubGameStage = StageFsm; +template using MainGameStage = StageFsm; +const GameProperties k_properties { + .name_ = "六贯棋", // the game name which should be unique among all the games + .developer_ = "铁蛋", + .description_ = "在由六边形组成的棋盘上轮流落子,联通两侧底盘来获胜", +}; +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { + if (GET_OPTION_VALUE(options, 边长) <= 11) return 1; + if (GET_OPTION_VALUE(options, 边长) <= 14) return 2; + if (GET_OPTION_VALUE(options, 边长) <= 17) return 3; + return 4; +} +const MutableGenericOptions k_default_generic_options; +const std::vector k_rule_commands = {}; + +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +{ + if (generic_options_readonly.PlayerNum() != 2) { + reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); + return false; + } + return true; +} + +const std::vector k_init_options_commands = { + InitOptionsCommand("独自一人开始游戏", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) + { + generic_options.bench_computers_to_player_num_ = 2; + return NewGameMode::SINGLE_USER; + }, + VoidChecker("单机")), + InitOptionsCommand("设置游戏边长", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const int& size) + { + GET_OPTION_VALUE(game_options, 边长) = size; + return NewGameMode::MULTIPLE_USERS; + }, + ArithChecker(7, 19, "边长")), +}; + +// ========== GAME STAGES ========== + +class RoundStage; + +class MainStage : public MainGameStage +{ + public: + MainStage(StageUtility&& utility) + : StageFsm(std::move(utility), MakeStageCommand(*this, "查看当前游戏进展情况", &MainStage::Status_, VoidChecker("赛况"))) + , round_(0) + , player_scores_(Global().PlayerNum(), 0) + {} + + virtual int64_t PlayerScore(const PlayerID pid) const override { return player_scores_[pid]; } + + std::vector player_scores_; + + // 当前行动玩家 + PlayerID currentPlayer; + // 棋盘 + Board board; + // 回合数 + int round_; + + private: + CompReqErrCode Status_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + reply() << Markdown(board.GetUI(currentPlayer)); + // Returning |OK| means the game stage + return StageErrCode::OK; + } + + void FirstStageFsm(SubStageFsmSetter setter) + { + // 随机先后手 + srand((unsigned int)time(NULL)); + currentPlayer = rand() % 2; + + board.size = GAME_OPTION(边长); + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + board.name[pid] = Global().PlayerName(pid); + if (board.name[pid][0] == '<') { + board.name[pid] = board.name[pid].substr(1, board.name[pid].size() - 2); + } + board.player_color[pid] = pid + 1; + } + board.Initialize(); + + setter.Emplace(*this, ++round_); + } + + void NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) + { + currentPlayer = 1 - currentPlayer; + if (player_scores_[0] == 0 && player_scores_[1] == 0) { + setter.Emplace(*this, ++round_); + return; + } + Global().Boardcast() << Markdown(board.GetUI(-1)); + } +}; + +class RoundStage : public SubGameStage<> +{ + public: + RoundStage(MainStage& main_stage, const uint64_t round) + : StageFsm(main_stage, "第 " + std::to_string(round) + " 回合" , + MakeStageCommand(*this, "落子", &RoundStage::PlaceChess_, ArithChecker(1, 361, "位置")), + MakeStageCommand(*this, "在第 4 回合,后手玩家可以选择是否交换", &RoundStage::SwapColor_, VoidChecker("交换")), + MakeStageCommand(*this, "认输", &RoundStage::Concede_, AlterChecker({{"认输", 0}, {"投降", 0}}))) + {} + + bool swap = false; + + virtual void OnStageBegin() override + { + Global().SetReady(1 - Main().currentPlayer); + Global().Boardcast() << Markdown(Main().board.GetUI(Main().currentPlayer)); + if (Main().round_ == 4) { + Global().Boardcast() << "请 " << color_ch[Main().board.player_color[Main().currentPlayer]] << "方" << At(Main().currentPlayer) << " 行动,本回合您有2种选择:\n" + << "1. 发送「交换」:双方玩家交换棋子颜色,然后由您的对手落子\n" + << "2. 选择位置落子:不交换颜色,落子后继续游戏\n" + << "时限 " << GAME_OPTION(时限) << " 秒,超时未行动判负"; + } else { + Global().Boardcast() << "请 " << color_ch[Main().board.player_color[Main().currentPlayer]] << "方" << At(Main().currentPlayer) << " 选择位置落子。" + << "时限 " << GAME_OPTION(时限) << " 秒,超时未行动判负"; + } + Global().StartTimer(GAME_OPTION(时限)); + } + + private: + AtomReqErrCode PlaceChess_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const int num) + { + if (Global().IsReady(pid)) { + reply() << "[错误] 本回合并非您的回合"; + return StageErrCode::FAILED; + } + string result = Main().board.PlaceChess(num, Main().currentPlayer); + if (result != "OK") { + reply() << result; + return StageErrCode::FAILED; + } + swap = false; + return StageErrCode::READY; + } + + AtomReqErrCode SwapColor_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + if (Main().round_ != 4) { + reply() << "[错误] 当前并非第4回合,无法执行交换操作"; + return StageErrCode::FAILED; + } + if (swap) { + reply() << "[错误] 本局游戏颜色已经发生交换,无法再次交换!"; + return StageErrCode::FAILED; + } + if (Global().IsReady(pid)) { + reply() << "[错误] 您本局为先手方,无法执行交换操作"; + return StageErrCode::FAILED; + } + Main().board.SwapColor(); + Global().Boardcast() << "[提示] 后手选择交换,双方玩家颜色互换!"; + swap = true; + return StageErrCode::READY; + } + + AtomReqErrCode Concede_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const uint32_t null) { + Main().player_scores_[pid] = -1; + Global().Boardcast() << color_ch[Main().board.player_color[pid]] << "方" << At(PlayerID(pid)) << "认输,游戏结束。"; + return StageErrCode::CHECKOUT; + } + + virtual CheckoutErrCode OnStageTimeout() override + { + if (!Global().IsReady(0)) { + Main().player_scores_[0] = -1; + Global().Boardcast() << "玩家 " << At(PlayerID(0)) << " 超时判负"; + } else if (!Global().IsReady(1)) { + Main().player_scores_[1] = -1; + Global().Boardcast() << "玩家 " << At(PlayerID(1)) << " 超时判负"; + } + return StageErrCode::CHECKOUT; + } + + virtual CheckoutErrCode OnPlayerLeave(const PlayerID pid) override + { + Global().Boardcast() << color_ch[Main().board.player_color[pid]] << "方" << At(PlayerID(pid)) << " 强退认输。"; + Main().player_scores_[pid] = -1; + return StageErrCode::CHECKOUT; + } + + virtual CheckoutErrCode OnStageOver() override + { + if (swap) { + Main().currentPlayer = 1 - Main().currentPlayer; + Global().ClearReady(Main().currentPlayer); + Global().Boardcast() << Markdown(Main().board.GetUI(Main().currentPlayer)); + Global().Boardcast() << "请 " << color_ch[Main().board.player_color[Main().currentPlayer]] << "方" << At(Main().currentPlayer) << " 选择位置落子。" + << "时限 " << GAME_OPTION(时限) << " 秒,超时未行动判负"; + Global().StartTimer(GAME_OPTION(时限)); + return StageErrCode::CONTINUE; + } + + // 判断当前胜负 + int winner = Main().board.WinCheck(); + if (winner != -1) { + Main().player_scores_[Main().currentPlayer] = 1; + Global().Boardcast() << color_ch[Main().board.player_color[Main().currentPlayer]] << "方" << At(Main().currentPlayer) << " 棋子联通了地图,获得胜利!"; + } + return StageErrCode::CHECKOUT; + } + + virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) override + { + if (Global().IsReady(pid)) { + return StageErrCode::OK; + } + if (pid == Main().currentPlayer) { + string result; + while (result != "OK") { + int num = rand() % (Main().board.size * Main().board.size); + result = Main().board.PlaceChess(num, pid); + } + } + return StageErrCode::READY; + } +}; + +auto* MakeMainStage(MainStageFactory factory) { return factory.Create(); } + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + diff --git a/games/hex/option.cmake b/games/hex/option.cmake new file mode 100644 index 00000000..e69de29b diff --git a/games/hex/options.h b/games/hex/options.h new file mode 100644 index 00000000..a2baa66a --- /dev/null +++ b/games/hex/options.h @@ -0,0 +1,2 @@ +EXTEND_OPTION("每回合时间限制", 时限, (ArithChecker(30, 3600, "超时时间(秒)")), 180) +EXTEND_OPTION("棋盘边长", 边长, (ArithChecker(7, 19, "长度")), 11) diff --git a/games/hex/rule.md b/games/hex/rule.md new file mode 100644 index 00000000..59657290 --- /dev/null +++ b/games/hex/rule.md @@ -0,0 +1,11 @@ +## 六贯棋 + +- **游戏人数:** 2 +- **原作:** James Ernest & Patrick Rothfuss + +### 游戏简介 +- 游戏使用由数个六边形组成的平行四边形棋盘,默认边长为11。当一方将自己两侧的底盘连通即可获胜。 + +### 游戏流程 +- 开局随机先后手,双方玩家各执一色,轮流在棋盘上落子。 +- **当先手下完第二子时(即第 4 回合),后手可以选择是否交换棋子**。 diff --git a/games/hex/unittest.cc b/games/hex/unittest.cc new file mode 100644 index 00000000..93260c74 --- /dev/null +++ b/games/hex/unittest.cc @@ -0,0 +1,47 @@ +// Copyright (c) 2018-present, Chang Liu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include "game_framework/unittest_base.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +GAME_TEST(1, player_not_enough) +{ + ASSERT_FALSE(StartGame()); +} + +GAME_TEST(2, leave_test1) +{ + START_GAME(); + + ASSERT_LEAVE(CHECKOUT, 0); + + ASSERT_SCORE(-1, 0); +} + +GAME_TEST(2, leave_test2) +{ + START_GAME(); + + ASSERT_LEAVE(CHECKOUT, 1); + + ASSERT_SCORE(0, -1); +} + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + gflags::ParseCommandLineFlags(&argc, &argv, true); + return RUN_ALL_TESTS(); +} diff --git a/games/holdem_poker/mygame.cc b/games/holdem_poker/mygame.cc index 6c644c2f..029067b8 100644 --- a/games/holdem_poker/mygame.cc +++ b/games/holdem_poker/mygame.cc @@ -27,15 +27,15 @@ const GameProperties k_properties { .description_ = "同时下注或加注的德州波卡游戏", }; constexpr uint64_t k_max_player = 15; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return k_max_player; } -uint32_t Multiple(const MyGameOptions& options) +uint64_t MaxPlayerNum(const CustomOptions& options) { return k_max_player; } +uint32_t Multiple(const CustomOptions& options) { return GET_OPTION_VALUE(options, 种子).empty() ? GET_OPTION_VALUE(options, 局数) / 4 : 0; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (GET_OPTION_VALUE(game_options, 幸存) >= generic_options_readonly.PlayerNum()) { reply() << "幸存玩家数 " << GET_OPTION_VALUE(game_options, 幸存) << " 必须小于参赛玩家数 " << generic_options_readonly.PlayerNum(); @@ -54,7 +54,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 10; return NewGameMode::SINGLE_USER; diff --git a/games/hp_killer/mygame.cc b/games/hp_killer/mygame.cc index 3ed880a5..d35f1dfa 100644 --- a/games/hp_killer/mygame.cc +++ b/games/hp_killer/mygame.cc @@ -29,8 +29,8 @@ const GameProperties k_properties { .developer_ = "森高", .description_ = "通过对其他玩家造成伤害,杀掉隐藏在玩家中的杀手的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 9; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 3; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game +uint64_t MaxPlayerNum(const CustomOptions& options) { return 9; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 3; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options{ .is_formal_{false}, }; @@ -38,21 +38,21 @@ const MutableGenericOptions k_default_generic_options{ const char* const k_role_rules[Occupation::Count()] = { // killer team [static_cast(Occupation(Occupation::杀手))] = R"EOF(【杀手 | 杀手阵营】 -- 开局时知道所有「平民阵营」角色的代号,但不知道代号与职位的对应关系 +- 开局时知道【杀手】的代号,但不知道代号与职位的对应关系 - 可以选择「攻击 <代号> 25」和「治愈 <代号> 15」)EOF", [static_cast(Occupation(Occupation::替身))] = R"EOF(【替身 | 杀手阵营】 -- 开局时知道所有「平民阵营」角色的代号(五人场除外) +- 开局时知道【杀手】的代号(五人场除外) - 特殊技能「挡刀 <代号>」:令当前回合**攻击**指定角色造成的减 HP 效果转移到**自己**身上,次数不限)EOF", [static_cast(Occupation(Occupation::恶灵))] = R"EOF(【恶灵 | 杀手阵营】 -- 开局时知道所有「平民阵营」角色的代号(五人场除外) +- 开局时知道【杀手】的代号(五人场除外) - 死亡后仍可继续行动(「中之人」仍会被公布),直到触发以下任意一种情况时,从下一回合起失去行动能力: - 被【侦探】侦查到**治愈**或**攻击**操作 - 被【灵媒】通灵)EOF", [static_cast(Occupation(Occupation::刺客))] = R"EOF(【刺客 | 杀手阵营】 -- 开局时知道所有「平民阵营」角色的代号(五人场除外) +- 开局时知道【杀手】的代号(五人场除外) - 特殊技能「攻击 <代号> (<代号>)... <伤害>」:扣除多名角色的 HP,代号不允许重复 - 伤害可以是 0、5、10 或 15 中的一个: - 如果伤害是 0 或 15,则只能指定 1 个代号 @@ -66,7 +66,7 @@ const char* const k_role_rules[Occupation::Count()] = { - 如果【双子】中的一方死亡,另一方存活,则从下一回合起,存活方将加入死亡方的阵营(如果【双子】的死亡导致游戏结束,则存活方阵营**不发生**改变))EOF", [static_cast(Occupation(Occupation::魔女))] = R"EOF(【魔女 | 杀手阵营】 -- 开局时知道所有「平民阵营」角色的代号(五人场除外) +- 开局时知道【杀手】的代号(五人场除外) - 不允许使用「攻击 <代号> 15」指令 - 特殊技能「诅咒 <代号> 5」和「诅咒 <代号> 10」:令指定角色进入诅咒状态 - 处于诅咒状态的角色**执行非 pass 操作的回合**会流失 5 或 10 点 HP,直到被物理攻击(对于单个诅咒状态,进入诅咒状态的回合会流失体力,但是解除诅咒的回合不会,如果物理攻击被挡刀或者盾反则诅咒效果不会被解除) @@ -147,7 +147,7 @@ const std::vector k_rule_commands = { EnumChecker()), }; -static std::vector& GetOccupationList(MyGameOptions& option, const uint32_t player_num) +static std::vector& GetOccupationList(CustomOptions& option, const uint32_t player_num) { return player_num == 5 ? GET_OPTION_VALUE(option, 五人身份) : player_num == 6 ? GET_OPTION_VALUE(option, 六人身份) : @@ -156,12 +156,12 @@ static std::vector& GetOccupationList(MyGameOptions& option, const u player_num == 9 ? GET_OPTION_VALUE(option, 九人身份) : (assert(false), GET_OPTION_VALUE(option, 五人身份)); } -static const std::vector& GetOccupationList(const MyGameOptions& option, const uint32_t player_num) +static const std::vector& GetOccupationList(const CustomOptions& option, const uint32_t player_num) { - return GetOccupationList(const_cast(option), player_num); + return GetOccupationList(const_cast(option), player_num); } -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 5) { reply() << "该游戏至少 5 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -212,7 +212,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 8; return NewGameMode::SINGLE_USER; @@ -339,9 +339,9 @@ struct CureAction struct BlockAttackAction { - std::string ToString() const { return std::string("挡刀") + token_.ToChar(); } + std::string ToString() const { return std::string("挡刀") + (token_.has_value() ? std::string(" ") + token_->ToChar() : "杀手"); } - Token token_; + std::optional token_; }; struct DetectAction @@ -384,8 +384,13 @@ struct FlushHiddenDamangeAction std::vector tokens_; }; +struct GoodNightAction +{ + std::string ToString() const { return "晚安"; } +}; + using ActionVariant = std::variant; + PassAction, ExocrismAction, ShieldAntiAction, AssignHiddenDamangeAction, FlushHiddenDamangeAction, GoodNightAction>; class RoleManager; @@ -441,59 +446,73 @@ class RoleBase public: virtual ~RoleBase() {} - virtual bool Act(const AttackAction& action, MsgSenderBase& reply); + virtual bool Act(const AttackAction& action, MsgSenderBase& reply, StageUtility& utility); - virtual bool Act(const CurseAction& action, MsgSenderBase& reply) + virtual bool Act(const CurseAction& action, MsgSenderBase& reply, StageUtility& utility) { reply() << "攻击失败:您无法使用魔法攻击"; return false; } - virtual bool Act(const CureAction& action, MsgSenderBase& reply); + virtual bool Act(const CureAction& action, MsgSenderBase& reply, StageUtility& utility); - virtual bool Act(const BlockAttackAction& action, MsgSenderBase& reply) + virtual bool Act(const BlockAttackAction& action, MsgSenderBase& reply, StageUtility& utility) { reply() << "侦查失败:您无法执行该类型行动"; return false; } - virtual bool Act(const DetectAction& action, MsgSenderBase& reply) + virtual bool Act(const DetectAction& action, MsgSenderBase& reply, StageUtility& utility) { reply() << "侦查失败:您无法执行该类型行动"; return false; } - virtual bool Act(const ExocrismAction& action, MsgSenderBase& reply) + virtual bool Act(const ExocrismAction& action, MsgSenderBase& reply, StageUtility& utility) { reply() << "通灵失败:您无法执行该类型行动"; return false; } - virtual bool Act(const PassAction& action, MsgSenderBase& reply) + virtual bool Act(const PassAction& action, MsgSenderBase& reply, StageUtility& utility) { reply() << "您本回合决定不行动"; cur_action_ = action; return true; } - virtual bool Act(const ShieldAntiAction& action, MsgSenderBase& reply) + virtual bool Act(const ShieldAntiAction& action, MsgSenderBase& reply, StageUtility& utility) { reply() << "盾反失败:您无法执行该类型行动"; return false; } - virtual bool Act(const AssignHiddenDamangeAction& action, MsgSenderBase& reply) + virtual bool Act(const AssignHiddenDamangeAction& action, MsgSenderBase& reply, StageUtility& utility) { reply() << "蓄力失败:您无法执行该类型行动"; return false; } - virtual bool Act(const FlushHiddenDamangeAction& action, MsgSenderBase& reply) + virtual bool Act(const FlushHiddenDamangeAction& action, MsgSenderBase& reply, StageUtility& utility) { reply() << "释放失败:您无法执行该类型行动"; return false; } + virtual bool Act(const GoodNightAction& action, MsgSenderBase& reply, StageUtility& utility) + { + if (team_ != Team::平民) { + reply() << "晚安失败:您无法执行该类型行动"; + return false; + } + reply() << "晚安玛卡巴卡,您已经无法再行动了"; + DisableActWhenRoundEnd(); + assert(pid_.has_value()); + DisableAct(); + cur_action_ = action; + return true; + } + virtual std::string PrivateInfo(const MainStage& main_stage) const { return std::string("您的代号是 ") + GetToken().ToChar() + ",职业是「" + GetOccupation().ToString() + "」"; @@ -659,7 +678,7 @@ class RoleManager RoleVec roles_; }; -bool RoleBase::Act(const AttackAction& action, MsgSenderBase& reply) +bool RoleBase::Act(const AttackAction& action, MsgSenderBase& reply, StageUtility& utility) { if (action.token_hps_.size() != 1) { reply() << "攻击失败:您需要且只能攻击 1 名角色"; @@ -685,7 +704,7 @@ bool RoleBase::Act(const AttackAction& action, MsgSenderBase& reply) return true; } -bool RoleBase::Act(const CureAction& action, MsgSenderBase& reply) +bool RoleBase::Act(const CureAction& action, MsgSenderBase& reply, StageUtility& utility) { auto& target = role_manager_.GetRole(action.token_); if (!target.is_alive_) { @@ -798,7 +817,7 @@ class MainStage : public MainGameStage<> MakeStageCommand(*this, "[侦探] 检查某名角色上一回合行动", &MainStage::Detect_, VoidChecker("侦查"), BasicChecker("角色代号", "A")), MakeStageCommand(*this, "[替身] 替某名角色承担本回合伤害", &MainStage::BlockHurt_, VoidChecker("挡刀"), - BasicChecker("角色代号(若为空,则为杀手代号)", "A")), + OptionalChecker>("角色代号(若为空,则为杀手代号)", "A")), MakeStageCommand(*this, "[灵媒] 获取某名死者的职业", &MainStage::Exocrism_, VoidChecker("通灵"), BasicChecker("角色代号", "A")), MakeStageCommand(*this, "[守卫] 盾反某几名角色", &MainStage::ShieldAnti_, VoidChecker("盾反"), @@ -810,6 +829,7 @@ class MainStage : public MainGameStage<> BasicChecker("角色代号", "A"), ArithChecker(5, 15, "血量"))), MakeStageCommand(*this, "[特工] 释放隐藏伤害", &MainStage::FlushHiddenDamange_, VoidChecker("释放"), RepeatableChecker>("角色代号", "A")), + MakeStageCommand(*this, "认为已经达成平民胜利条件,不再警惕,放弃行动", &MainStage::GoodNight_, VoidChecker("晚安")), MakeStageCommand(*this, "跳过本回合行动", &MainStage::Pass_, VoidChecker("pass"))) #ifdef TEST_BOT , role_manager_(GAME_OPTION(身份列表).empty() @@ -828,7 +848,14 @@ class MainStage : public MainGameStage<> } virtual void OnStageBegin() override { - Global().Boardcast() << "游戏开始,将私信各位玩家角色代号及职业\n\n第 1 回合开始,请私信裁判行动"; + { + auto sender = Global().Boardcast(); + sender << "游戏开始,将私信各位玩家角色代号及职业\n\n"; + if (GAME_OPTION(晚安模式)) { + sender << "需要注意,该局开启了「晚安模式」,「平民阵营」只有执行「晚安」指令才有可能取得胜利\n\n"; + } + sender << "第 1 回合开始,请私信裁判行动"; + } role_manager_.Foreach([&](const auto& role) { if (role.PlayerId().has_value()) { @@ -877,7 +904,7 @@ class MainStage : public MainGameStage<> } private: - static RoleOption DefaultRoleOption_(const MyGameOptions& option) + static RoleOption DefaultRoleOption_(const CustomOptions& option) { return RoleOption { .hp_ = GET_OPTION_VALUE(option, 血量), @@ -908,7 +935,9 @@ class MainStage : public MainGameStage<> hurt_blocker ? std::get_if(&hurt_blocker->CurAction()) : nullptr; const auto is_blocked_hurt = [&](const RoleBase& role) { - return block_hurt_action && role.GetToken() == block_hurt_action->token_; + return block_hurt_action && + ((!block_hurt_action->token_.has_value() && role.GetOccupation() == Occupation::杀手) || + (block_hurt_action->token_.has_value() && role.GetToken() == *block_hurt_action->token_)); }; const auto is_avoid_hurt = [&](const RoleBase& hurter_role, const RoleBase& hurted_role) { @@ -1039,6 +1068,9 @@ class MainStage : public MainGameStage<> { role_manager_.Foreach([&](auto& role) { + if (!role.CanAct() && role.PlayerId().has_value()) { + Global().Eliminate(*role.PlayerId()); + } role.OnRoundBegin(); }); } @@ -1084,17 +1116,16 @@ class MainStage : public MainGameStage<> Global().Tell(*other_role->PlayerId()) << "另一位双子死亡,您下一回合的阵营变更为:" << role.GetTeam() << "阵营"; } }); - if (has_dead) { - } } bool CheckTeamsLost_(MsgSenderBase::MsgSenderGuard& sender) { bool killer_dead = true; bool traitor_dead = true; - bool all_civilian_team_dead_next_round = true; uint32_t civilian_dead_count = 0; uint32_t civilian_team_dead_count = 0; + uint32_t civilian_team_alive_count = 0; + uint32_t civilian_team_alive_cannot_act_count = 0; role_manager_.Foreach([&](const auto& role) { if (role.IsAlive()) { @@ -1106,7 +1137,8 @@ class MainStage : public MainGameStage<> } if (role.GetNextRoundTeam() == Team::平民) { // civilian twin can convert to killer team next round, so we check the next-round team instead of current team - all_civilian_team_dead_next_round = false; + ++civilian_team_alive_count; + civilian_team_alive_cannot_act_count += !role.CanAct(); } return; } else if (role.GetTeam() == Team::平民) { @@ -1118,8 +1150,9 @@ class MainStage : public MainGameStage<> }); bool civilian_lost = civilian_dead_count >= k_civilian_dead_threshold || civilian_team_dead_count >= k_civilian_team_dead_threshold || - all_civilian_team_dead_next_round; - bool killer_lost = killer_dead; + civilian_team_alive_count == 0; + bool killer_lost = killer_dead && + (!GAME_OPTION(晚安模式) || civilian_team_alive_cannot_act_count > (civilian_team_alive_count / 2)); bool traitor_lost = traitor_dead; if (const auto role = role_manager_.GetRole(Occupation::特工); civilian_lost && killer_lost && role != nullptr) { @@ -1131,10 +1164,11 @@ class MainStage : public MainGameStage<> switch (!civilian_lost + !killer_lost + !traitor_lost) { case 0: // multiple teams lost at the same time sender << "游戏结束,多个阵营的失败条件同时满足,此时根据优先级,判定"; - if (traitor_lost && !last_round_traitor_lost_) { // traitor lost at this round + if (!last_round_traitor_lost_ && + (role_manager_.GetRole(Occupation::内奸) || role_manager_.GetRole(Occupation::初版内奸))) { // traitor lost at this round traitor_lost = false; sender << "内奸"; - } else if (killer_lost && !last_round_killer_lost_) { // killer lost at this round + } else if (!last_round_killer_lost_) { // killer lost at this round killer_lost = false; sender << "杀手阵营"; } else { @@ -1241,7 +1275,7 @@ class MainStage : public MainGameStage<> return v; } - static RoleManager::RoleVec GetRoleVec_(const MyGameOptions& option, const RoleOption& role_option, const uint32_t player_num, RoleManager& role_manager) + static RoleManager::RoleVec GetRoleVec_(const CustomOptions& option, const RoleOption& role_option, const uint32_t player_num, RoleManager& role_manager) { const auto make_roles = [&](const std::initializer_list& occupation_lists) { @@ -1287,15 +1321,15 @@ class MainStage : public MainGameStage<> }); case 8: return make_roles(std::initializer_list>{ {Occupation::杀手, Occupation::替身, Occupation::刺客, Occupation::侦探, Occupation::圣女, Occupation::守卫, Occupation::平民, Occupation::平民, Occupation::人偶}, - {Occupation::杀手, Occupation::替身, Occupation::恶灵, Occupation::侦探, Occupation::圣女, Occupation::灵媒, Occupation::平民, Occupation::平民, Occupation::人偶}, - {Occupation::杀手, Occupation::替身, Occupation::魔女, Occupation::侦探, Occupation::圣女, Occupation::骑士, Occupation::平民, Occupation::平民, Occupation::人偶}, + // {Occupation::杀手, Occupation::替身, Occupation::恶灵, Occupation::侦探, Occupation::圣女, Occupation::灵媒, Occupation::平民, Occupation::平民}, + {Occupation::杀手, Occupation::替身, Occupation::魔女, Occupation::侦探, Occupation::圣女, Occupation::骑士, Occupation::平民, Occupation::平民}, }); case 9: return make_roles(std::initializer_list>{ {Occupation::杀手, Occupation::替身, Occupation::刺客, Occupation::侦探, Occupation::圣女, Occupation::守卫, Occupation::平民, Occupation::平民, Occupation::内奸}, - {Occupation::杀手, Occupation::替身, Occupation::恶灵, Occupation::侦探, Occupation::圣女, Occupation::灵媒, Occupation::平民, Occupation::平民, Occupation::内奸}, + // {Occupation::杀手, Occupation::替身, Occupation::恶灵, Occupation::侦探, Occupation::圣女, Occupation::灵媒, Occupation::平民, Occupation::平民, Occupation::内奸}, {Occupation::杀手, Occupation::替身, Occupation::魔女, Occupation::侦探, Occupation::圣女, Occupation::骑士, Occupation::平民, Occupation::平民, Occupation::内奸}, {Occupation::杀手, Occupation::替身, Occupation::刺客, Occupation::侦探, Occupation::圣女, Occupation::守卫, Occupation::平民, Occupation::平民, Occupation::特工}, - {Occupation::杀手, Occupation::替身, Occupation::恶灵, Occupation::侦探, Occupation::圣女, Occupation::灵媒, Occupation::平民, Occupation::平民, Occupation::特工}, + // {Occupation::杀手, Occupation::替身, Occupation::恶灵, Occupation::侦探, Occupation::圣女, Occupation::灵媒, Occupation::平民, Occupation::平民, Occupation::特工}, {Occupation::杀手, Occupation::替身, Occupation::魔女, Occupation::侦探, Occupation::圣女, Occupation::骑士, Occupation::平民, Occupation::平民, Occupation::特工}, }); default: @@ -1477,7 +1511,9 @@ class MainStage : public MainGameStage<> reply() << "行动失败:您已经失去了行动能力"; return StageErrCode::FAILED; // should not appened for user player } - if (!std::visit([&role, &reply](auto& action) { return role.Act(action, reply); }, action)) { + if (!std::visit( + [&role, &reply, &utility = Global()](auto& action) { return role.Act(action, reply, utility); }, + action)) { return StageErrCode::FAILED; } return StageErrCode::READY; @@ -1557,9 +1593,9 @@ class MainStage : public MainGameStage<> return GenericAct_(pid, is_public, reply, DetectAction{.token_ = token}); } - AtomReqErrCode BlockHurt_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const Token& token) + AtomReqErrCode BlockHurt_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const std::optional& token) { - if (!role_manager_.IsValid(token)) { + if (token.has_value() && !role_manager_.IsValid(*token)) { reply() << "挡刀失败:场上没有该角色"; return StageErrCode::FAILED; } @@ -1608,6 +1644,15 @@ class MainStage : public MainGameStage<> return GenericAct_(pid, is_public, reply, FlushHiddenDamangeAction{.tokens_ = tokens}); } + AtomReqErrCode GoodNight_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + if (!GAME_OPTION(晚安模式)) { + reply() << "晚安失败:当前游戏模式下无需晚安"; + return StageErrCode::FAILED; + } + return GenericAct_(pid, is_public, reply, GoodNightAction{}); + } + AtomReqErrCode Pass_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) { return GenericAct_(pid, is_public, reply, PassAction{}); @@ -1663,12 +1708,16 @@ class BodyDoubleRole : public RoleBase virtual std::string PrivateInfo(const MainStage& main_stage) const { if (main_stage.Global().PlayerNum() > 5) { - return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForTeam(role_manager_, Team::平民); + if (GET_OPTION_VALUE(main_stage.Global().Options(), 身份互通)) { + return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForTeam(role_manager_, Team::平民); + } else { + return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForRole(role_manager_, Occupation::杀手); + } } return RoleBase::PrivateInfo(main_stage); } - virtual bool Act(const BlockAttackAction& action, MsgSenderBase& reply) override + virtual bool Act(const BlockAttackAction& action, MsgSenderBase& reply, StageUtility& utility) override { reply() << "请做好觉悟,本回合对该角色造成的全部伤害将转移到您身上"; cur_action_ = action; @@ -1687,7 +1736,11 @@ class GhostRole : public RoleBase virtual std::string PrivateInfo(const MainStage& main_stage) const { if (main_stage.Global().PlayerNum() > 5) { - return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForTeam(role_manager_, Team::平民); + if (GET_OPTION_VALUE(main_stage.Global().Options(), 身份互通)) { + return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForTeam(role_manager_, Team::平民); + } else { + return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForRole(role_manager_, Occupation::杀手); + } } return RoleBase::PrivateInfo(main_stage); } @@ -1704,12 +1757,16 @@ class AssassinRole : public RoleBase virtual std::string PrivateInfo(const MainStage& main_stage) const { if (main_stage.Global().PlayerNum() > 5) { - return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForTeam(role_manager_, Team::平民); + if (GET_OPTION_VALUE(main_stage.Global().Options(), 身份互通)) { + return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForTeam(role_manager_, Team::平民); + } else { + return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForRole(role_manager_, Occupation::杀手); + } } return RoleBase::PrivateInfo(main_stage); } - virtual bool Act(const AttackAction& action, MsgSenderBase& reply) override + virtual bool Act(const AttackAction& action, MsgSenderBase& reply, StageUtility& utility) override { assert(!action.token_hps_.empty()); assert(std::all_of(action.token_hps_.begin(), action.token_hps_.end(), @@ -1762,18 +1819,22 @@ class WitchRole : public RoleBase virtual std::string PrivateInfo(const MainStage& main_stage) const { if (main_stage.Global().PlayerNum() > 5) { - return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForTeam(role_manager_, Team::平民); + if (GET_OPTION_VALUE(main_stage.Global().Options(), 身份互通)) { + return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForTeam(role_manager_, Team::平民); + } else { + return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForRole(role_manager_, Occupation::杀手); + } } return RoleBase::PrivateInfo(main_stage); } - virtual bool Act(const AttackAction& action, MsgSenderBase& reply) override + virtual bool Act(const AttackAction& action, MsgSenderBase& reply, StageUtility& utility) override { reply() << "攻击失败:您无法使用物理攻击"; return false; } - virtual bool Act(const CurseAction& action, MsgSenderBase& reply) override + virtual bool Act(const CurseAction& action, MsgSenderBase& reply, StageUtility& utility) override { auto& target = role_manager_.GetRole(action.token_); cur_action_ = action; @@ -1807,13 +1868,13 @@ class GoddessRole : public RoleBase { } - virtual bool Act(const AttackAction& action, MsgSenderBase& reply) override + virtual bool Act(const AttackAction& action, MsgSenderBase& reply, StageUtility& utility) override { if (!history_status_.empty() && std::get_if(&history_status_.back().action_)) { reply() << "攻击失败:您无法连续两回合进行攻击"; return false; } - return RoleBase::Act(action, reply); + return RoleBase::Act(action, reply, utility); } }; @@ -1826,7 +1887,7 @@ class DetectiveRole : public RoleBase } public: - virtual bool Act(const DetectAction& action, MsgSenderBase& reply) override + virtual bool Act(const DetectAction& action, MsgSenderBase& reply, StageUtility& utility) override { if (history_status_.empty()) { reply() << "侦查失败:首回合无法侦查"; @@ -1851,7 +1912,7 @@ class SorcererRole : public RoleBase { } - virtual bool Act(const ExocrismAction& action, MsgSenderBase& reply) override + virtual bool Act(const ExocrismAction& action, MsgSenderBase& reply, StageUtility& utility) override { if (exocrismed_) { reply() << "通灵失败:您本局游戏已经通灵过一次了"; @@ -1875,7 +1936,7 @@ class GuardRole : public RoleBase { } - virtual bool Act(const ShieldAntiAction& action, MsgSenderBase& reply) override + virtual bool Act(const ShieldAntiAction& action, MsgSenderBase& reply, StageUtility& utility) override { if (action.token_hps_.size() > 2 || action.token_hps_.empty()) { reply() << "盾反失败:您需要指定 1~2 名角色的血量"; @@ -1936,7 +1997,7 @@ class TwinRole : public RoleBase ",您当前属于" + GetTeam().ToString() + "阵营"; } - virtual bool Act(const AttackAction& action, MsgSenderBase& reply) override + virtual bool Act(const AttackAction& action, MsgSenderBase& reply, StageUtility& utility) override { for (const auto& token_hp : action.token_hps_) { const auto occupation = role_manager_.GetRole(std::get(token_hp)).GetOccupation(); @@ -1945,7 +2006,7 @@ class TwinRole : public RoleBase return false; } } - return RoleBase::Act(action, reply); + return RoleBase::Act(action, reply, utility); } }; @@ -2003,13 +2064,13 @@ class AgentRole : public RoleBase { } - virtual bool Act(const AttackAction& action, MsgSenderBase& reply) override + virtual bool Act(const AttackAction& action, MsgSenderBase& reply, StageUtility& utility) override { reply() << "攻击失败:您只能通过释放隐藏伤害的方式攻击角色"; return false; } - virtual bool Act(const AssignHiddenDamangeAction& action, MsgSenderBase& reply) + virtual bool Act(const AssignHiddenDamangeAction& action, MsgSenderBase& reply, StageUtility& utility) { int32_t sum_hp = 0; for (const auto& [token, hp] : action.token_hps_) { @@ -2037,7 +2098,7 @@ class AgentRole : public RoleBase return true; } - virtual bool Act(const FlushHiddenDamangeAction& action, MsgSenderBase& reply) + virtual bool Act(const FlushHiddenDamangeAction& action, MsgSenderBase& reply, StageUtility& utility) { for (const auto& token : action.tokens_) { if (hidden_damages_[token.id_] == 0) { diff --git a/games/hp_killer/options.h b/games/hp_killer/options.h index d2da1b4d..ad92a0f4 100644 --- a/games/hp_killer/options.h +++ b/games/hp_killer/options.h @@ -3,6 +3,8 @@ EXTEND_OPTION("回合数", 回合数, (ArithChecker(1, 100, "回合数 EXTEND_OPTION("每回合时间限制", 时限, (ArithChecker(10, 3600, "超时时间(秒)")), 300) EXTEND_OPTION("初始血量", 血量, (ArithChecker(1, 100, "HP 值")), 100) EXTEND_OPTION("初始治愈次数", 治愈次数, (ArithChecker(0, 10, "次数")), 3) +EXTEND_OPTION("杀手阵营知道全部平民阵营代号,但不知道杀手", 身份互通, (BoolChecker("开启", "关闭")), false) +EXTEND_OPTION("多数派平民需要声明「晚安」才可取得胜利", 晚安模式, (BoolChecker("开启", "关闭")), false) EXTEND_OPTION("五人场身份列表(身份数量需为 5,否则实际采用默认身份列表)", 五人身份, (RepeatableChecker>()), std::vector{}) EXTEND_OPTION("六人场身份列表(身份数量需为 6,否则实际采用默认身份列表)", 六人身份, (RepeatableChecker>()), std::vector{}) EXTEND_OPTION("七人场身份列表(身份数量需为 7,否则实际采用默认身份列表)", 七人身份, (RepeatableChecker>()), std::vector{}) diff --git a/games/hp_killer/rule.md b/games/hp_killer/rule.md index 58bfb0b6..de107f8b 100644 --- a/games/hp_killer/rule.md +++ b/games/hp_killer/rule.md @@ -25,6 +25,8 @@ ### 游戏结束 +#### 普通模式 + 各个阵营有各自的失败条件,失败阵营的角色失去行动能力,并原则上不允许继续发言: - 当满足以下**任意一项**时,「平民阵营」失败: - 有 3 名或以上「平民阵营」的角色死亡 @@ -37,6 +39,12 @@ 如果所有阵营同时失败,则按照「第三方阵营」→「杀手阵营」→「平民阵营」的优先级,选取**上一回合**未失败的阵营,其角色对应的「中之人」取得胜利。 +#### 晚安模式(测试中):提高「平民阵营」玩家的胜利难度 + +所有「平民阵营」的玩家新增行动指令「晚安」,执行意味着放弃自己的行动能力。 + +「杀手阵营」的失败条件变为:【杀手】死亡,且大于一半的存活中的「平民阵营」角色失去行动能力。 + ### 职位能力一览 #### 杀手阵营 diff --git a/games/hp_killer/unittest.cc b/games/hp_killer/unittest.cc index 545989df..ee0d5a06 100644 --- a/games/hp_killer/unittest.cc +++ b/games/hp_killer/unittest.cc @@ -451,6 +451,18 @@ GAME_TEST(5, civilian_and_killer_and_traitor_dead_at_the_same_time_traitor_win) ASSERT_SCORE(0, 0, 0, 0, 1); } +GAME_TEST(5, both_civilian_and_killer_lose_in_the_first_round) +{ + ASSERT_PUB_MSG(OK, 0, "身份列表 杀手 平民 平民 圣女 守卫"); + ASSERT_PUB_MSG(OK, 0, "血量 15"); + START_GAME(); + ASSERT_PRI_MSG(OK, 0, "攻击 A 15"); + ASSERT_PRI_MSG(OK, 1, "攻击 B 15"); + ASSERT_PRI_MSG(OK, 2, "攻击 C 15"); + ASSERT_TIMEOUT(CHECKOUT); + ASSERT_SCORE(1, 0, 0, 0, 0); +} + GAME_TEST(5, killer_dead_body_double_cannot_act) { ASSERT_PUB_MSG(OK, 0, "身份列表 杀手 替身 平民 平民 内奸"); @@ -1391,6 +1403,61 @@ GAME_TEST(5, agent_achieve_max_round_lose) ASSERT_SCORE(1, 1, 1, 1, 0); } +GAME_TEST(5, cannot_good_night_without_good_night_mode) +{ + ASSERT_PUB_MSG(OK, 0, "身份列表 杀手 替身 平民 圣女 侦探"); + ASSERT_PUB_MSG(OK, 0, "晚安模式 关闭"); + ASSERT_TRUE(StartGame()); + ASSERT_PRI_MSG(FAILED, 3, "晚安"); +} + +GAME_TEST(5, good_night_mode_cannot_act_after_good_night) +{ + ASSERT_PUB_MSG(OK, 0, "身份列表 杀手 替身 平民 圣女 侦探"); + ASSERT_PUB_MSG(OK, 0, "晚安模式 开启"); + ASSERT_TRUE(StartGame()); + ASSERT_PRI_MSG(OK, 3, "晚安"); + ASSERT_TIMEOUT(CONTINUE); + ASSERT_ELIMINATED(3); +} + +GAME_TEST(5, good_night_mode_killer_cannot_good_night) +{ + ASSERT_PUB_MSG(OK, 0, "身份列表 杀手 替身 平民 圣女 侦探"); + ASSERT_PUB_MSG(OK, 0, "晚安模式 开启"); + ASSERT_TRUE(StartGame()); + ASSERT_PRI_MSG(FAILED, 0, "晚安"); +} + +GAME_TEST(5, good_night_mode_civilians_cannot_win_until_quorum_good_night) +{ + ASSERT_PUB_MSG(OK, 0, "身份列表 杀手 替身 平民 圣女 侦探"); + ASSERT_PUB_MSG(OK, 0, "晚安模式 开启"); + ASSERT_PUB_MSG(OK, 0, "血量 15"); + ASSERT_TRUE(StartGame()); + ASSERT_PRI_MSG(OK, 0, "攻击 A 15"); + ASSERT_TIMEOUT(CONTINUE); + ASSERT_PRI_MSG(CONTINUE, 2, "晚安"); + ASSERT_TIMEOUT(CONTINUE); + ASSERT_PRI_MSG(CHECKOUT, 3, "晚安"); + ASSERT_SCORE(0, 0, 1, 1, 1); +} + +GAME_TEST(5, good_night_mode_civilians_cannot_win_until_quorum_good_night_by_killed) +{ + ASSERT_PUB_MSG(OK, 0, "身份列表 杀手 替身 平民 圣女 侦探"); + ASSERT_PUB_MSG(OK, 0, "晚安模式 开启"); + ASSERT_PUB_MSG(OK, 0, "血量 15"); + ASSERT_TRUE(StartGame()); + ASSERT_PRI_MSG(OK, 0, "攻击 A 15"); + ASSERT_PRI_MSG(OK, 2, "晚安"); + ASSERT_TIMEOUT(CONTINUE); + ASSERT_PRI_MSG(CONTINUE, 1, "攻击 D 15"); + ASSERT_PRI_MSG(OK, 4, "攻击 E 15"); + ASSERT_TIMEOUT(CHECKOUT); + ASSERT_SCORE(0, 0, 1, 1, 1); +} + } // namespace GAME_MODULE_NAME diff --git a/games/jewish_chess/mygame.cc b/games/jewish_chess/mygame.cc index f92f225f..29170e7b 100644 --- a/games/jewish_chess/mygame.cc +++ b/games/jewish_chess/mygame.cc @@ -24,12 +24,12 @@ const GameProperties k_properties { .developer_ = "dva", .description_ = "轮流落子,率先占满棋盘的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) { return 1; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return 1; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 2) { reply() << "人数不足。"; return false; @@ -39,7 +39,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; @@ -50,7 +50,7 @@ const std::vector k_init_options_commands = { // ========== UI ============== struct MyTable { - MyTable(const MyGameOptions& option, const std::string_view resource_dir) + MyTable(const CustomOptions& option, const std::string_view resource_dir) : resource_dir_(resource_dir), len(GET_OPTION_VALUE(option, 棋盘大小)), table_(GET_OPTION_VALUE(option, 棋盘大小) + 4, 2 + GET_OPTION_VALUE(option, 棋盘大小)) { diff --git a/games/laser_chess/mygame.cc b/games/laser_chess/mygame.cc index 5b9f565f..cad2edf4 100644 --- a/games/laser_chess/mygame.cc +++ b/games/laser_chess/mygame.cc @@ -33,12 +33,12 @@ const GameProperties k_properties { .developer_ = "森高", .description_ = "调整镜面,发射激光消灭对方棋子的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) { return std::min(3U, GET_OPTION_VALUE(options, 回合数) / 10); } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return std::min(3U, GET_OPTION_VALUE(options, 回合数) / 10); } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为" << generic_options_readonly.PlayerNum(); @@ -49,7 +49,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; @@ -105,6 +105,9 @@ class MainStage : public MainGameStage<> virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) { + if (Global().IsReady(pid)) { + return StageErrCode::OK; + } const Coor coor = [&]() -> Coor { while (true) { diff --git a/games/lie/mygame.cc b/games/lie/mygame.cc index 4c123df4..3023979e 100644 --- a/games/lie/mygame.cc +++ b/games/lie/mygame.cc @@ -25,12 +25,12 @@ const GameProperties k_properties { .description_ = "双方猜测数字的简单游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) { return 1; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return 1; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为" << generic_options_readonly.PlayerNum(); @@ -41,7 +41,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; @@ -62,7 +62,7 @@ class MyTable bool is_lie_; }; - MyTable(const MyGameOptions& option, const std::string_view resource_dir) + MyTable(const CustomOptions& option, const std::string_view resource_dir) : option_(option) , resource_dir_{resource_dir} , table_(GET_OPTION_VALUE(option, 失败数量) * 2 + 3, GET_OPTION_VALUE(option, 数字种类)) @@ -153,7 +153,7 @@ class MyTable } } - const MyGameOptions& option_; + const CustomOptions& option_; std::string_view resource_dir_; html::Table table_; std::array, 2> player_nums_; diff --git a/games/long_night/achievements.h b/games/long_night/achievements.h new file mode 100644 index 00000000..e69de29b diff --git a/games/long_night/board.h b/games/long_night/board.h new file mode 100644 index 00000000..82ae87bc --- /dev/null +++ b/games/long_night/board.h @@ -0,0 +1,1207 @@ + +struct GetBoardOptions { + bool with_player = true; + bool with_content = false; +}; + +class Board +{ + public: + Board(string image_path, const int32_t mode) : image_path_(std::move(image_path)), unitMaps(mode) {} + + // 图片资源文件夹 + const string image_path_; + + // 玩家 + uint32_t playerNum; + vector players; + // 地图内的玩家 + vector>> player_map; + // 地图大小 + int size = 9; + // 地图 + vector> grid_map; + int exit_num; // 逃生舱数量 + string init_html_; + // 区块模板 + UnitMaps unitMaps; + // BOSS + struct Boss { + int x = -1; + int y = -1; + int steps = -1; + PlayerID target; + string all_record; + } boss; + // 特殊区块位置(暂不使用) + // pair special_pos = {12, 12}; + + // 初始化地图 + void Initialize(const bool boss) + { + std::random_device rd; + g = std::mt19937(rd()); + grid_map.resize(size); + player_map.resize(size); + for (int i = 0; i < size; i++) { + grid_map[i].resize(size); + player_map[i].resize(size); + } + InitializeBlock(); // 初始化区块 + FixAdjacentWalls(grid_map); // 相邻墙面修复 + FixInvalidPortals(grid_map); // 封闭传送门替换为水洼 + RandomizePlayers(); // 随机生成玩家 + if (boss) BossSpawn(); // 初始化BOSS + // 保存初始盘面 + init_html_ = GetBoard(grid_map); + } + + // 获取地图html + string GetBoard(const vector>& grid_map, const GetBoardOptions& options = {}) const + { + size_t size = grid_map.size(); + html::Table map(size * 2 + 1, size * 2 + 1); + map.SetTableStyle("align=\"center\" cellpadding=\"0\" cellspacing=\"0\" style=\"border: 2px dashed black;\""); + // 方格信息(包括玩家) + for (int x = 1; x < size * 2; x = x + 2) { + for (int y = 1; y < size * 2; y = y + 2) { + int gridX = (x+1)/2-1, gridY = (y+1)/2-1; + if (size >= 3) { + map.Get(x, y).SetStyle("class=\"grid\" " + GetGridStyle(grid_map[gridX][gridY].Type(), true)); + } else { + map.Get(x, y).SetStyle("class=\"grid\""); + } + if (options.with_content) { + string content = grid_map[gridX][gridY].GetContent().first; + if (!content.empty()) { + map.Get(x, y).SetContent(HTML_SIZE_FONT_HEADER(4) "" + content + "" HTML_FONT_TAIL); + } + } else if (options.with_player) { + string content = ""; + if (boss.x == gridX && boss.y == gridY) { + content += HTML_COLOR_FONT_HEADER(red) "★" HTML_FONT_TAIL; + } + for (auto pid: player_map[gridX][gridY]) { + content += num[pid]; + if (content.length() % 2 == 0) { + content += "
"; + } + } + if (!content.empty()) { + map.Get(x, y).SetContent(HTML_SIZE_FONT_HEADER(4) + content + HTML_FONT_TAIL); + } + } + } + } + // 纵向围墙 + for (int x = 1; x < size * 2; x = x + 2) { + for (int y = 0; y < size * 2 - 1; y = y + 2) { + int gridX = (x+1)/2-1, gridY = y/2; + map.Get(x, y).SetStyle("class=\"wall-col\" style=\"font-size:8px; line-height:8px; padding:0; margin:0;\"") + .SetColor(GetWallColor(grid_map[gridX][gridY].GetWall())); + if (options.with_content) { + string content = GetWallContent(grid_map, gridX, gridY, Direct::LEFT); + if (!content.empty()) map.Get(x, y).SetContent(content); + } + } + map.Get(x, size*2).SetStyle("class=\"wall-col\" style=\"font-size:8px; line-height:8px; padding:0; margin:0;\"") + .SetColor(GetWallColor(grid_map[(x+1)/2-1][size-1].GetWall())); + if (options.with_content) { + string content = GetWallContent(grid_map, (x+1)/2-1, size-1, Direct::RIGHT); + if (!content.empty()) map.Get(x, size*2).SetContent(content); + } + } + // 横向围墙 + for (int y = 1; y < size * 2; y = y + 2) { + for (int x = 0; x < size * 2 - 1; x = x + 2) { + int gridX = x/2, gridY = (y+1)/2-1; + map.Get(x, y).SetStyle("class=\"wall-row\" style=\"font-size:8px; line-height:8px; padding:0; margin:0;\"") + .SetColor(GetWallColor(grid_map[gridX][gridY].GetWall())); + if (options.with_content) { + string content = GetWallContent(grid_map, gridX, gridY, Direct::UP); + if (!content.empty()) map.Get(x, y).SetContent(content); + } + } + map.Get(size*2, y).SetStyle("class=\"wall-row\" style=\"font-size:8px; line-height:8px; padding:0; margin:0;\"") + .SetColor(GetWallColor(grid_map[size-1][(y+1)/2-1].GetWall())); + if (options.with_content) { + string content = GetWallContent(grid_map, size-1, (y+1)/2-1, Direct::DOWN); + if (!content.empty()) map.Get(size*2, y).SetContent(content); + } + } + // 角落方块 + for (int x = 0; x < size * 2 + 1; x = x + 2) { + for (int y = 0; y < size * 2 + 1; y = y + 2) { + map.Get(x, y).SetStyle("class=\"corner\"").SetColor("black"); + } + } + return style + map.ToString(); + } + + // 获取墙壁上的提示文本 + string GetWallContent(const vector>& grid_map, const int x, const int y, const Direct direction) const + { + const int d = static_cast(direction); + const int od = static_cast(opposite(direction)); + const vector vec1 = grid_map[x][y].GetContent().second; + if (d >= 0 && d < (int)vec1.size() && !vec1[d].empty()) { + return vec1[d]; + } + int nx = x + k_DX_Direct[d]; + int ny = y + k_DY_Direct[d]; + if (0 <= nx && nx < grid_map.size() && 0 <= ny && ny < grid_map.size()) { + const vector vec2 = grid_map[nx][ny].GetContent().second; + if (od >= 0 && od < (int)vec2.size() && !vec2[od].empty()) { + return vec2[od]; + } + } + return ""; + } + + string GetFinalBoard() const + { + html::Table finalTable(2, 2); + finalTable.SetTableStyle("align=\"center\" cellpadding=\"0\" cellspacing=\"0\""); + finalTable.Get(0, 0).SetContent(HTML_SIZE_FONT_HEADER(5) "初始地图" HTML_FONT_TAIL); + finalTable.Get(0, 1).SetContent(HTML_SIZE_FONT_HEADER(5) "终局地图" HTML_FONT_TAIL); + finalTable.Get(1, 0).SetStyle("style=\"padding: 10px 20px 10px 10px\"").SetContent(init_html_); + finalTable.Get(1, 1).SetStyle("style=\"padding: 10px 10px 10px 20px\"").SetContent(GetBoard(grid_map)); + return GetPlayerTable(-1) + style + finalTable.ToString(); + } + + // 获取玩家信息 + string GetPlayerTable(const int round) const + { + html::Table playerTable(playerNum + (boss.steps >= 0), 6); + playerTable.SetTableStyle("align=\"center\" cellpadding=\"2\""); + for (int pid = 0; pid < playerNum; pid++) { + playerTable.Get(pid, 0).SetStyle("style=\"width:60px; text-align:right;\"").SetContent(to_string(pid) + "号:"); + playerTable.Get(pid, 1).SetStyle("style=\"width:40px;\"").SetContent(players[pid].avatar); + playerTable.Get(pid, 2).SetStyle("style=\"width:250px; text-align:left;\"").SetContent(players[pid].name); + if (players[pid].out == 2) { + playerTable.MergeRight(pid, 3, 3); + playerTable.Get(pid, 3).SetStyle("style=\"width:120px;\"").SetColor("#FFEBA3").SetContent("【逃生舱撤离】"); + } else if (players[pid].out == 1) { + playerTable.MergeRight(pid, 3, 3); + playerTable.Get(pid, 3).SetStyle("style=\"width:120px;\"").SetColor("#E5E5E5").SetContent("【已出局】"); + } else if (players[pid].target == 100) { + playerTable.MergeRight(pid, 3, 3); + playerTable.Get(pid, 3).SetStyle("style=\"width:150px;\"").SetContent("【单机模式】
寻找唯一的逃生舱!"); + } else if (players[pid].out == 0) { + playerTable.Get(pid, 3).SetStyle("style=\"width:40px;\"").SetContent("捕捉
目标"); + playerTable.Get(pid, 4).SetStyle("style=\"width:40px;\"").SetContent("[" + to_string(players[pid].target) + "号]"); + playerTable.Get(pid, 5).SetStyle("style=\"width:40px;\"").SetContent(players[players[pid].target].avatar); + } else { + playerTable.MergeRight(pid, 3, 3); + playerTable.Get(pid, 3).SetStyle("style=\"width:120px;\"").SetColor("#FFA07A").SetContent("[玩家状态错误]"); + } + } + if (boss.steps >= 0) { + playerTable.Get(playerNum, 0).SetStyle("style=\"width:60px;\"").SetContent(""); + playerTable.MergeRight(playerNum, 1, 2); + playerTable.Get(playerNum, 1).SetStyle("style=\"width:290px;\"").SetColor("lavender").SetContent("[BOSS] 米诺陶斯"); + playerTable.Get(playerNum, 3).SetStyle("style=\"width:40px;\"").SetContent("捕捉
目标"); + playerTable.Get(playerNum, 4).SetStyle("style=\"width:40px;\"").SetContent("[" + to_string(boss.target) + "号]"); + playerTable.Get(playerNum, 5).SetStyle("style=\"width:40px;\"").SetContent(players[boss.target].avatar); + } + return (round > 0 ? "### 第 " + to_string(round) + " 回合" : "") + playerTable.ToString(); + } + + // 全部区块信息展示 + string GetAllBlocksInfo(const int special, const int32_t test_mode = 0) const + { + const vector& maps = test_mode == 0 ? unitMaps.maps : (test_mode == 2 ? unitMaps.rotation_maps : unitMaps.all_maps); + const vector& exits = test_mode == 0 ? unitMaps.exits : (test_mode == 2 ? unitMaps.rotation_exits : unitMaps.all_exits); + const vector& special_maps = unitMaps.special_maps; + const string title = + (test_mode == 0) ? "" + : (test_mode == 2) ? + (HTML_SIZE_FONT_HEADER(6) "《漫漫长夜》幻变模式轮换区块" HTML_FONT_TAIL) + : (HTML_SIZE_FONT_HEADER(6) "《漫漫长夜》狂野+疯狂模式全部区块" HTML_FONT_TAIL); + bool has_special = (test_mode == 3); + for (const auto& map : maps) { + if (map.type == GridType::SPECIAL) { + has_special = true; + } + } + + int line_num = (ceil(maps.size() / 4.0) + ceil(exits.size() / 4.0)) * 2 + + has_special * (ceil(special_maps.size() / 4.0) * 2 + 1); + html::Table blocks(line_num, 4); + blocks.SetTableStyle("align=\"center\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-size: 25px;\""); + int row = 0; + for (int i = 0; i < maps.size(); i++) { + blocks.Get(row, i % 4).SetStyle("style=\"padding: 25px 25px 0px 25px\"").SetContent(GetSingleBlock(0, maps[i].id, special)); + // 特殊地图不显示id + blocks.Get(row + 1, i % 4).SetContent(HTML_COLOR_FONT_HEADER(red) "" + (maps[i].id[0] == 'S' ? "???" : maps[i].id) + "" HTML_FONT_TAIL); + if ((i + 1) % 4 == 0 || i == maps.size() - 1) row += 2; + } + for (int i = 0; i < exits.size(); i++) { + blocks.Get(row, i % 4).SetStyle("style=\"padding: 20px 25px 5px 25px\"").SetContent(GetSingleBlock(1, exits[i].id, special)); + blocks.Get(row + 1, i % 4).SetContent(HTML_COLOR_FONT_HEADER(red) "EXIT " + exits[i].id + "" HTML_FONT_TAIL); + if ((i + 1) % 4 == 0 || i == exits.size() - 1) row += 2; + } + if (has_special) { + blocks.MergeRight(row, 0, 4); + blocks.Get(row++, 0).SetStyle("style=\"padding: 20px 25px 5px 25px\"") + .SetContent(HTML_SIZE_FONT_HEADER(6) "特殊区块列表
" HTML_FONT_TAIL HTML_SIZE_FONT_HEADER(5) "(多传送门按照图中字母代号传送)" HTML_FONT_TAIL "
"); + for (int i = 0; i < special_maps.size(); i++) { + blocks.Get(row, i % 4).SetStyle("style=\"padding: 20px 25px 5px 25px\"") + .SetContent(GetBoard(unitMaps.FindBlockById(special_maps[i].id, false, special == 3), GetBoardOptions{.with_content = true})); + blocks.Get(row + 1, i % 4).SetContent(HTML_COLOR_FONT_HEADER(red) "" + special_maps[i].id + "" HTML_FONT_TAIL); + if ((i + 1) % 4 == 0 || i == special_maps.size() - 1) row += 2; + } + } + const string wall_svg = "" + "" + "" + "" + ""; + auto GenerateWallSvg = [](const std::string& color) -> std::string { + return "" + "" + ""; + }; + const vector> all_walls_info = { + { Wall::DOOR, "【门】初始为关闭状态,关闭时视为墙壁。当关联的按钮被按下时会切换开关状态
如果门发生过变化,在回合结束会进行提示。关闭的门在私信墙壁信息会显示为**普通墙壁**" }, + }; + const vector> all_grids_info = { + { GridType::BUTTON, "【按钮】玩家进入时会触发区块内按钮相关事件。(出生不算)
进入按钮格**没有任何信息提示**,且仅在进入时才会触发按钮" }, + { GridType::GRASS, "【树丛】玩家进入时会发出让其他人听见的**沙沙声**。(出生不算)" }, + { GridType::WATER, "【水洼】玩家进入时会发出让其他人听见的**啪啪声**。(出生不算)" }, + { GridType::PORTAL, "【传送门】玩家进入时会发出其他人听见的**啪啪声**。(出生不算)
进入后,再任意2次移动后就会传送至同区块另1个传送门。
进入后,玩家视作进入亚空间,上述2次移动都在亚空间内。" }, + { GridType::ONEWAYPORTAL, "【传送门出口】玩家进入时会发出其他人听见的**啪啪声**。(出生不算)
传送门的单向出口,进入时不会触发传送(必须从入口进入才会传送至此处)
**玩家在进入同一区块的传送门入口时,传送门会转换方向,入口和出口交换位置**" }, + { GridType::TRAP, "【陷阱】陷阱隐藏在树丛中:被奇数次进入时,会发出让其他人听见的**沙沙声**(出生不算)
被偶数次进入时,不发出声响,并**强制玩家停止**(出生不算)" }, + { GridType::HEAT, "【热源】进入热源周围8格时,将**私信**受到热浪提示。(出生不算)
当进入热源时,将**私信**收到高温烫伤提示(不会出生在热源内)
在整局游戏中,**当第 2 次或更多次进入热源时,会被强制停止行动**" }, + { GridType::BOX, "【箱子】玩家相邻箱子且向箱子移动时,箱子可被推动。(不会出生在箱子内)
**箱子不可移动到本区块外**。若箱子不可推动,则撞墙,箱子本身不会显示为墙。" }, + { GridType::EXIT, "【逃生舱】逃生者使用后,**会消失**。默认逃生舱数=人数的一半" }, + }; + vector all_maps_in_game; + all_maps_in_game.insert(all_maps_in_game.end(), maps.begin(), maps.end()); + all_maps_in_game.insert(all_maps_in_game.end(), exits.begin(), exits.end()); + if (has_special) all_maps_in_game.insert(all_maps_in_game.end(), special_maps.begin(), special_maps.end()); + + html::Table legend(all_grids_info.size() + 1, 2); + legend.SetTableStyle("cellpadding=\"5\" cellspacing=\"0\" style=\"font-size: 22px;\""); + for (int i = 0; i < all_grids_info.size() + 1; i++) { + legend.Get(i, 1).SetStyle("style=\"text-align: left;\""); + } + legend.Get(0, 0).SetContent(wall_svg); + legend.Get(0, 1).SetContent("【墙壁】黑色为墙壁,如图例,玩家不可从上下通过,可以从左右通过"); + row = 1; + for (const auto& wall_info: all_walls_info) { + if (UnitMaps::MapContainWallType(all_maps_in_game, wall_info.first)) { + legend.Get(row, 0).SetContent(GenerateWallSvg(GetWallColor(wall_info.first))); + legend.Get(row, 1).SetContent(wall_info.second); + row++; + } + } + for (const auto& grid_info: all_grids_info) { + if (UnitMaps::MapContainGridType(all_maps_in_game, grid_info.first)) { + if (grid_info.first == GridType::GRASS && special == 3) continue; + legend.Get(row, 0).SetContent(GetGridStyle(grid_info.first, false)); + legend.Get(row, 1).SetContent(grid_info.second); + row++; + } + } + legend.ResizeRow(row); + return title + blocks.ToString() + legend.ToString(); + } + + string GetSingleBlock(const int type, const string& id, const int special) const + { + // 特殊地图不显示预览 + if (id[0] == 'S') return ""; + vector> grid; + if (type == 0) { + grid = unitMaps.FindBlockById(id, false, special == 3); + } else if (type == 1) { + grid = unitMaps.FindBlockById(id, true); + } + return GetBoard(grid, GetBoardOptions{.with_content = true}); + } + + string GetAllRecord() const + { + string all_record; + for (int pid = 0; pid < playerNum; pid++) { + all_record += "[" + to_string(pid) + "号]" + players[pid].name + ""; + all_record += players[pid].all_record + "
"; + if (pid < playerNum - 1) all_record += "
"; + } + if (boss.steps >= 0) { + all_record += "
[BOSS] 米诺陶斯"; + all_record += boss.all_record + "
"; + } + return regex_replace(all_record, regex(R"(\]\()"), "] ("); + } + + string GetAllScore() const + { + html::Table scoreTable(playerNum + 1, 7); + scoreTable.SetTableStyle("cellpadding=\"2\" cellspacing=\"0\" border=\"1\""); + scoreTable.Get(0, 0).SetStyle("style=\"width:50px;\"").SetContent("序号"); + scoreTable.Get(0, 1).SetStyle("style=\"width:250px;\"").SetContent("玩家"); + scoreTable.Get(0, 2).SetStyle("style=\"width:70px;\"").SetContent("抓人分"); + scoreTable.Get(0, 3).SetStyle("style=\"width:70px;\"").SetContent("逃生分"); + scoreTable.Get(0, 4).SetStyle("style=\"width:70px;\"").SetContent("探索分"); + scoreTable.Get(0, 5).SetStyle("style=\"width:70px;\"").SetContent("额外
探索分"); + scoreTable.Get(0, 6).SetStyle("style=\"width:100px;\"").SetContent("【最终总分】"); + for (int pid = 0; pid < playerNum; pid++) { + Score s = players[pid].score; + scoreTable.Get(pid + 1, 0).SetContent(to_string(pid) + "号"); + scoreTable.Get(pid + 1, 1).SetContent(players[pid].name); + if (s.catch_score > 0) scoreTable.Get(pid + 1, 2).SetColor("#BAFFA8"); + if (s.catch_score < 0) scoreTable.Get(pid + 1, 2).SetColor("#FFA07A"); + scoreTable.Get(pid + 1, 2).SetContent(to_string(s.catch_score)); + if (s.exit_score > 0) scoreTable.Get(pid + 1, 3).SetColor("#FFDB60"); + scoreTable.Get(pid + 1, 3).SetContent(to_string(s.exit_score)); + auto [c1, c2] = s.ExploreCount(); + scoreTable.Get(pid + 1, 4).SetContent(to_string(c1 + c2)); + scoreTable.Get(pid + 1, 5).SetContent(to_string(c2)); + scoreTable.Get(pid + 1, 6).SetContent((s.quit_score < 0 ? HTML_COLOR_FONT_HEADER("red") : HTML_COLOR_FONT_HEADER("black")) + to_string(s.FinalScore()) + HTML_FONT_TAIL); + } + return Score::ScoreInfo() + scoreTable.ToString(); + } + + // 玩家移动 + bool MakeMove(const PlayerID pid, const Direct direction, const bool hide) + { + static const char* arrow[4] = {"↑", "↓", "←", "→"}; + static const char* hit[4] = {"(↑撞)", "(↓撞)", "(←撞)", "(→撞)"}; + + int d = static_cast(direction); + int cx = players[pid].x; + int cy = players[pid].y; + int nx = (cx + k_DX_Direct[d] + size) % size; + int ny = (cy + k_DY_Direct[d] + size) % size; + + bool wall = false; + switch (direction) { + case Direct::UP: wall = grid_map[cx][cy].GetWall() != Wall::EMPTY; break; + case Direct::DOWN: wall = grid_map[cx][cy].GetWall() != Wall::EMPTY; break; + case Direct::LEFT: wall = grid_map[cx][cy].GetWall() != Wall::EMPTY; break; + case Direct::RIGHT: wall = grid_map[cx][cy].GetWall() != Wall::EMPTY; break; + default: return false; + } + // 非撞墙尝试移动箱子 + if (!wall && grid_map[nx][ny].Type() == GridType::BOX && players[pid].subspace < 0) { + wall = !BoxMove(nx, ny, k_DX_Direct[d], k_DY_Direct[d]); + } + // 轨迹记录 + if (wall && players[pid].subspace < 0) { + if (!hide) players[pid].move_record += hit[d]; + return false; + } + if (!hide) players[pid].move_record += arrow[d]; + + if (players[pid].subspace > 0) { + players[pid].subspace--; + return true; + } + + RemovePlayerFromMap(pid); + + players[pid].x = nx; + players[pid].y = ny; + player_map[nx][ny].push_back(pid); + + // 探索分 + auto& explore = players[pid].score.explore_map[nx][ny]; + if (explore == 0) { + bool first = true; + for (auto& player: players) { + if (player.score.explore_map[nx][ny] > 0) { + first = false; + } + } + explore = first ? 2 : 1; + } + + return true; + } + + // 箱子移动 + bool BoxMove(const int b_cx, const int b_cy, const int dx, const int dy) + { + int b_nx = (b_cx + dx + size) % size; + int b_ny = (b_cy + dy + size) % size; + if (!player_map[b_nx][b_ny].empty()) return false; + if (grid_map[b_nx][b_ny].Type() != GridType::EMPTY) return false; + + bool wall = false; + if (dx == -1 && dy == 0) wall = grid_map[b_cx][b_cy].GetWall() != Wall::EMPTY; + else if (dx == 1 && dy == 0) wall = grid_map[b_cx][b_cy].GetWall() != Wall::EMPTY; + else if (dx == 0 && dy == -1) wall = grid_map[b_cx][b_cy].GetWall() != Wall::EMPTY; + else if (dx == 0 && dy == 1) wall = grid_map[b_cx][b_cy].GetWall() != Wall::EMPTY; + if (wall) return false; + + bool sameBlock = false; + for (auto pos : unitMaps.pos) { + int bx = pos.first, by = pos.second; + if (b_cx >= bx && b_cx < bx + 3 && b_cy >= by && b_cy < by + 3 && + b_nx >= bx && b_nx < bx + 3 && b_ny >= by && b_ny < by + 3) { + sameBlock = true; + break; + } + } + if (!sameBlock) return false; + + grid_map[b_cx][b_cy].SetType(GridType::EMPTY); + grid_map[b_nx][b_ny].SetType(GridType::BOX); + return true; + } + + // 玩家从地图中移除(辅助功能) + void RemovePlayerFromMap(const PlayerID pid) + { + int cx = players[pid].x; + int cy = players[pid].y; + auto &oldCell = player_map[cx][cy]; + auto it = std::find(oldCell.begin(), oldCell.end(), pid); + if (it != oldCell.end()) oldCell.erase(it); + } + + // 地区声响(0无 1沙沙 2啪啪) + Sound GetSound(const Grid& grid, const bool water_mode) const + { + switch(grid.Type()) { + case GridType::GRASS: return Sound::SHASHA; + case GridType::WATER: return Sound::PAPA; + case GridType::ONEWAYPORTAL: + case GridType::PORTAL: return Sound::PAPA; + case GridType::TRAP: return grid.TrapStatus() ? Sound::NONE : (water_mode ? Sound::PAPA : Sound::SHASHA); + default: return Sound::NONE; + } + } + + // 获取声响方向(用于发送声响) + string GetSoundDirection(const int fromX, const int fromY, const Player& to) const + { + int toX = to.x; + int toY = to.y; + + int dx = toX - fromX; + int dy = toY - fromY; + + if (abs(dx) > size / 2) { + if (dx > 0) + dx -= size; + else + dx += size; + } + if (abs(dy) > size / 2) { + if (dy > 0) + dy -= size; + else + dy += size; + } + + if (size % 2 == 0 && abs(dx) == size / 2) { + dx = (rand() % 2 == 0) ? size / 2 : -size / 2; + } + if (size % 2 == 0 && abs(dy) == size / 2) { + dy = (rand() % 2 == 0) ? size / 2 : -size / 2; + } + + // 两玩家在同一位置 + if (dx == 0 && dy == 0) { + return ""; + } + + if (dx == 0) { + return (dy < 0) ? "正右" : "正左"; + } + if (dy == 0) { + return (dx < 0) ? "正下" : "正上"; + } + string vertical = (dx < 0) ? "下" : "上"; + string horizontal = (dy < 0) ? "右" : "左"; + return horizontal + vertical; + } + + // 热浪提示 + bool HeatNotice(const PlayerID pid) + { + int cx = players[pid].x; + int cy = players[pid].y; + if (grid_map[cx][cy].Type() == GridType::HEAT) { + return false; + } + bool inZone = false; + for (int dx = -1; dx <= 1 && !inZone; dx++) { + for (int dy = -1; dy <= 1 && !inZone; dy++) { + int nx = (cx + dx + size) % size; + int ny = (cy + dy + size) % size; + if (grid_map[nx][ny].Type() == GridType::HEAT) { + inZone = true; + } + } + } + if (inZone && !players[pid].inHeatZone) { + players[pid].inHeatZone = true; + return true; + } + if (!inZone) { + players[pid].inHeatZone = false; + } + return false; + } + + // 获取四周墙壁信息(仅显示墙壁,不展示详细颜色) + pair GetSurroundingWalls(const PlayerID pid) const + { + Grid grid = grid_map[players[pid].x][players[pid].y]; + if (players[pid].subspace > 0) { + grid.SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + } else { + grid.HideSpecialWalls(); + } + string info; + if (grid.GetWall() == Wall::EMPTY) info += "空"; else info += "墙"; + if (grid.GetWall() == Wall::EMPTY) info += "空"; else info += "墙"; + if (grid.GetWall() == Wall::EMPTY) info += "空"; else info += "墙"; + if (grid.GetWall() == Wall::EMPTY) info += "空"; else info += "墙"; + return make_pair(info, GetBoard({{grid}}, GetBoardOptions{.with_player = false})); + } + + // 玩家随机传送 + void TeleportPlayer(const PlayerID pid) + { + players[pid].subspace = -1; + players[pid].inHeatZone = false; + vector> candidates = FindLargestConnectedArea(); + + // 禁止坐标 + vector> forbiddenSources; + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + if (grid_map[i][j].Type() == GridType::EXIT || grid_map[i][j].Type() == GridType::HEAT) { + forbiddenSources.push_back({i, j}); + } + } + } + if (boss.steps >= 0) { + forbiddenSources.push_back({boss.x, boss.y}); + } + for (const auto& player: players) { + if (player.pid == pid || player.out > 0) continue; + forbiddenSources.push_back({player.x, player.y}); + } + // 禁止区域(3*3) + vector> forbidden(size, vector(size, false)); + for (const auto& src : forbiddenSources) { + int sx = src.first, sy = src.second; + for (int di = -1; di <= 1; di++) { + for (int dj = -1; dj <= 1; dj++) { + int ni = (sx + di + size) % size; + int nj = (sy + dj + size) % size; + forbidden[ni][nj] = true; + } + } + } + // 过滤候选区域(同时去除BOSS可能到达的区域) + vector> finalCandidates; + std::copy_if( + candidates.begin(), candidates.end(), + std::back_inserter(finalCandidates), + [&](auto const& pos) { + auto [x, y] = pos; + return !forbidden[x][y] && (boss.steps <= 0 || ManhattanDistance(x, y, boss.x, boss.y) > boss.steps); + } + ); + + // 无有效区域,不传送 + if (finalCandidates.empty()) { + cout << "[debug warning] 传送失败,无有效候选位置" << endl; + return; + } + + std::shuffle(finalCandidates.begin(), finalCandidates.end(), g); + pair newPos = finalCandidates.front(); + + RemovePlayerFromMap(pid); + + players[pid].x = newPos.first; + players[pid].y = newPos.second; + player_map[newPos.first][newPos.second].push_back(pid); + } + + // 捕捉顺位变更 + void UpdatePlayerTarget(const bool next) + { + for (PlayerID pid = 0; pid < playerNum; ++pid) { + if (players[pid].out > 0) continue; + PlayerID target = next ? (pid + 1) % playerNum : (playerNum + pid - 1) % playerNum; + while (players[target].out > 0 && target != pid) { + players[target].target = -1; + target = next ? (target + 1) % playerNum : (playerNum + target - 1) % playerNum; + } + players[pid].target = target; + } + } + + string MapGenerate(const vector& map_str) const + { + vector> grid_map; + grid_map.resize(size); + for (int i = 0; i < size; i++) { + grid_map[i].resize(size); + } + for (int i = 0; i < unitMaps.origin_pos.size(); i++) { + string map_id; + bool is_exit = false; + string str = map_str[i]; + std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) { return std::toupper(c); }); + if (!str.empty() && str[0] == 'E') { + is_exit = true; + map_id = str.substr(1); + } else { + map_id = str; + } + if (is_exit) { + unitMaps.SetExitBlock(unitMaps.origin_pos[i].first, unitMaps.origin_pos[i].second, grid_map, map_id, false); + } else { + unitMaps.SetMapBlock(unitMaps.origin_pos[i].first, unitMaps.origin_pos[i].second, grid_map, map_id, false); + } + } + FixAdjacentWalls(grid_map); + FixInvalidPortals(grid_map); + return GetBoard(grid_map, GetBoardOptions{.with_content = true}); + } + + int ExitCount() const + { + int count = 0; + for (int x = 0; x < size; x++) { + for (int y = 0; y < size; y++) { + if (grid_map[x][y].Type() == GridType::EXIT) { + count++; + } + } + } + return count; + } + + // BOSS生成和锁定目标 + void BossSpawn() + { + for (int attempt = 0; attempt < 500; attempt++) { + int x = rand() % size; + int y = rand() % size; + bool nearPlayer = false; + for (int i = 0; i < playerNum && !nearPlayer; i++) { + for (int dx = -1; dx <= 1 && !nearPlayer; dx++) { + for (int dy = -1; dy <= 1 && !nearPlayer; dy++) { + int nx = (players[i].x + dx + size) % size; + int ny = (players[i].y + dy + size) % size; + if (nx == x && ny == y) + nearPlayer = true; + } + } + } + boss.x = x; + boss.y = y; + boss.steps = 0; + if (!nearPlayer) break; + } + // 初始锁定最近的玩家作为目标 + int bestDist = INT_MAX; + vector candidates; + for (int i = 0; i < playerNum; i++) { + int d = ManhattanDistance(boss.x, boss.y, players[i].x, players[i].y); + if (d < bestDist) { + bestDist = d; + candidates.clear(); + candidates.push_back(i); + } else if (d == bestDist) { + candidates.push_back(i); + } + } + boss.target = candidates[rand() % candidates.size()]; + } + + // BOSS更换目标(未更换返回true) + bool BossChangeTarget(const bool reset) + { + int curTarget = boss.target; + int curDist = ManhattanDistance(boss.x, boss.y, players[boss.target].x, players[boss.target].y); + if (reset || players[boss.target].out > 0) curDist = INT_MAX; + for (auto& player : players) { + if (player.out > 0) continue; + int d = ManhattanDistance(boss.x, boss.y, player.x, player.y); + if (d < curDist) { + boss.target = player.pid; + boss.steps = 0; + curDist = d; + } + } + return curTarget == boss.target; + } + + // BOSS移动和更换目标(抓到人返回true) + bool BossMove() + { + int targetDist = ManhattanDistance(boss.x, boss.y, players[boss.target].x, players[boss.target].y); + boss.steps++; + // 步数足够直接走到目标位置 + if (boss.steps >= targetDist) { + boss.x = players[boss.target].x; + boss.y = players[boss.target].y; + boss.steps = 0; + return true; + } + // 执行移动 + int stepsRemaining = boss.steps; + while (stepsRemaining > 0) { + targetDist = ManhattanDistance(boss.x, boss.y, players[boss.target].x, players[boss.target].y); + int tx = players[boss.target].x, ty = players[boss.target].y; + int dx = tx - boss.x, dy = ty - boss.y; + if (abs(dx) > size / 2) { dx = (dx > 0) ? dx - size : dx + size; } + if (abs(dy) > size / 2) { dy = (dy > 0) ? dy - size : dy + size; } + + // 如果|dx|≠|dy|则沿较大差值轴走一步 + if (abs(dx) != abs(dy)) { + if (abs(dx) > abs(dy)) { + boss.x = (boss.x + ((dx > 0) ? 1 : -1) + size) % size; + } else { + boss.y = (boss.y + ((dy > 0) ? 1 : -1) + size) % size; + } + } else { + // 如果已经正方形,随机选择在 x 或 y 方向上移动一步 + if (rand() % 2 == 0) + boss.x = (boss.x + ((dx > 0) ? 1 : -1) + size) % size; + else + boss.y = (boss.y + ((dy > 0) ? 1 : -1) + size) % size; + } + stepsRemaining--; + } + return false; + } + + bool IsBossNearby(const Player& player) const + { + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + int nx = (boss.x + dx + size) % size; + int ny = (boss.y + dy + size) % size; + if (player.x == nx && player.y == ny) { + return true; + } + } + } + return false; + } + + private: + // 初始化区块 + void InitializeBlock() + { + vector maps = unitMaps.maps; + vector exits = unitMaps.exits; + std::shuffle(maps.begin(), maps.end(), g); + std::shuffle(exits.begin(), exits.end(), g); + + vector selected; + for (int i = 0; i < unitMaps.pos.size() - exit_num; i++) { + selected.push_back(maps[i]); + } + for (int i = 0; i < exit_num; i++) { + selected.push_back(exits[i]); + } + std::shuffle(selected.begin(), selected.end(), g); + + for (int i = 0; i < unitMaps.pos.size(); i++) { + if (selected[i].type == GridType::EXIT) { + unitMaps.SetExitBlock(unitMaps.pos[i].first, unitMaps.pos[i].second, grid_map, selected[i].id, true); + } else { + unitMaps.SetMapBlock(unitMaps.pos[i].first, unitMaps.pos[i].second, grid_map, selected[i].id, true); + } + // 记录特殊地图坐标(暂不使用) + // if (selected[i].type == GridType::SPECIAL) { + // special_pos = unitMaps.pos[i]; + // } + } + } + + // 相邻墙面修复(优先级更高的墙壁会覆盖低优先级) + void FixAdjacentWalls(vector>& grid_map) const + { + for (int x = 0; x < size; x++) { + for (int y = 0; y < size; y++) { + for (int d = 0; d < 4; d++) { + Direct dir = static_cast(d); + Direct rev = opposite(dir); + int nx = (x + k_DX_Direct[d] + size) % size; + int ny = (y + k_DY_Direct[d] + size) % size; + + Wall w1 = grid_map[x][y].GetWallByEnum(dir); + Wall w2 = grid_map[nx][ny].GetWallByEnum(rev); + Wall w = static_cast(w1) > static_cast(w2) ? w1 : w2; + if (w != w1) grid_map[x][y].SetWallByEnum(dir, w); + if (w != w2) grid_map[nx][ny].SetWallByEnum(rev, w); + } + } + } + } + + // 封闭传送门替换为水洼 + void FixInvalidPortals(vector>& grid_map) const + { + for (int x = 0; x < size; x++) { + for (int y = 0; y < size; y++) { + if (grid_map[x][y].Type() == GridType::PORTAL && grid_map[x][y].IsFullyEnclosed()) { + pair pRelPos = grid_map[x][y].PortalPos(); + grid_map[x + pRelPos.first][y + pRelPos.second].SetType(GridType::ONEWAYPORTAL); + } + } + } + } + + // 判断(x, y)格子沿(dx, dy)方向是否连通(考虑环绕) + bool IsConnected(int x, int y, int dx, int dy) const + { + int nx = (x + dx + size) % size; + int ny = (y + dy + size) % size; + if (grid_map[x][y].Type() == GridType::HEAT || grid_map[x][y].Type() == GridType::BOX) { + return false; + } + if (grid_map[nx][ny].Type() == GridType::HEAT || grid_map[nx][ny].Type() == GridType::BOX) { + return false; + } + if (dx == -1 && dy == 0) + return grid_map[x][y].GetWall() == Wall::EMPTY && grid_map[nx][ny].GetWall() == Wall::EMPTY; + else if (dx == 1 && dy == 0) + return grid_map[x][y].GetWall() == Wall::EMPTY && grid_map[nx][ny].GetWall() == Wall::EMPTY; + else if (dx == 0 && dy == -1) + return grid_map[x][y].GetWall() == Wall::EMPTY && grid_map[nx][ny].GetWall() == Wall::EMPTY; + else if (dx == 0 && dy == 1) + return grid_map[x][y].GetWall() == Wall::EMPTY && grid_map[nx][ny].GetWall() == Wall::EMPTY; + return false; + } + + int BFSRouteDistance(int sx, int sy, int ex, int ey) const + { + vector> visited(size, vector(size, false)); + queue> q; // (x, y, distance) + q.push({sx, sy, 0}); + visited[sx][sy] = true; + while (!q.empty()) { + auto [x, y, d] = q.front(); + q.pop(); + if (x == ex && y == ey) + return d; + int dx[4] = {-1, 1, 0, 0}; + int dy[4] = {0, 0, -1, 1}; + for (int i = 0; i < 4; i++) { + int nx = (x + dx[i] + size) % size; + int ny = (y + dy[i] + size) % size; + if (!visited[nx][ny] && IsConnected(x, y, dx[i], dy[i])) { + visited[nx][ny] = true; + q.push({nx, ny, d + 1}); + } + } + } + return INT_MAX; + } + + // * 随机生成所有玩家的位置,所有位置均在最大联通区域内 * + // 按照顺序依次尝试: + // 方案1:使用非逃生舱区块,相邻玩家之间的 BFS 路径距离≥5;每个玩家落点到所有逃生舱的 BFS 路径距离≥5 + // 方案2:使用非逃生舱区块,仅要求相邻玩家之间的 BFS 路径距离≥5(不检查逃生舱距离) + // 方案3:直接使用非逃生舱区块随机分配(不检查距离) + // 方案4:允许使用逃生舱区块 + // 保险方案:直接从最大连通区域中随机选取位置 + void RandomizePlayers() + { + // 1. 获取最大连通区域 + vector> largest = FindLargestConnectedArea(); + vector> inLargest(size, vector(size, false)); + for (auto coord : largest) + inLargest[coord.first][coord.second] = true; + + // 1.1 收集所有逃生舱位置 + vector> exitPositions; + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + if (grid_map[i][j].Type() == GridType::EXIT) + exitPositions.push_back({i, j}); + } + } + + // Lambda:计算 (x,y) 的连通度(即周围可连通方向数量) + auto connectivityDegree = [this](int x, int y) -> int { + int degree = 0; + int dx[4] = {-1, 1, 0, 0}; + int dy[4] = {0, 0, -1, 1}; + for (int k = 0; k < 4; k++) { + if (IsConnected(x, y, dx[k], dy[k])) + degree++; + } + return degree; + }; + + // 2. 构造候选区块:遍历 UnitMaps::pos 中所有区块 + // - blockPos:区块左上角坐标 + // - validCells:区块内所有满足在最大联通区域且连通度 ≥ 2 的候选落点 + // - isExit:若区块内含有逃生舱,则为 true + struct BlockCandidate { + pair blockPos; + vector> validCells; + bool isExit; + }; + vector candidateBlocks; + for (auto pos : unitMaps.pos) { + int bx = pos.first, by = pos.second; + bool containsExit = false; + vector> validCells; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + if (grid_map[bx + i][by + j].Type() == GridType::EXIT) { + containsExit = true; + break; + } + if (!inLargest[bx + i][by + j]) + continue; + if (connectivityDegree(bx + i, by + j) >= 2) + validCells.push_back({bx + i, by + j}); + } + if (containsExit) + break; + } + candidateBlocks.push_back({{bx, by}, validCells, containsExit}); + } + + // 3. 筛选非逃生舱区块(用于方案1、2、3) + vector candidateNonExit; + for (auto &b : candidateBlocks) { + if (!b.isExit && !b.validCells.empty()) + candidateNonExit.push_back(b); + } + // 为使得候选区块分布随机,先随机打乱候选非逃生舱区块顺序 + std::shuffle(candidateNonExit.begin(), candidateNonExit.end(), g); + + // 4. 方案1:使用非逃生舱区块,相邻玩家之间的 BFS 路径距离≥5;每个玩家落点到所有逃生舱的 BFS 路径距离≥5 + vector> assigned(playerNum, {-1, -1}); + vector used(candidateNonExit.size(), false); + // 回溯函数,checkExit 表示是否需要额外检查落点到逃生舱的距离 + function bt = [&](int idx, bool checkExit) -> bool { + if (idx == playerNum) { + int d = BFSRouteDistance(assigned[playerNum - 1].first, assigned[playerNum - 1].second, + assigned[0].first, assigned[0].second); + return d >= 5 || playerNum == 1; + } + for (int i = 0; i < candidateNonExit.size(); i++) { + if (used[i]) + continue; + // 对当前区块随机遍历其候选落点 + vector> cellCandidates = candidateNonExit[i].validCells; + std::shuffle(cellCandidates.begin(), cellCandidates.end(), g); + for (auto candidate : cellCandidates) { + if (idx > 0) { + int d = BFSRouteDistance(assigned[idx - 1].first, assigned[idx - 1].second, + candidate.first, candidate.second); + if (d < 5) + continue; + } + if (checkExit) { + bool ok = true; + for (auto &ex : exitPositions) { + int d = BFSRouteDistance(candidate.first, candidate.second, ex.first, ex.second); + if (d < 5) { ok = false; break; } + } + if (!ok) + continue; + } + assigned[idx] = candidate; + used[i] = true; + if (bt(idx + 1, checkExit)) + return true; + used[i] = false; + } + } + return false; + }; + + if (candidateNonExit.size() >= (unsigned)playerNum && bt(0, true)) { + // 方案1成功 + for (int i = 0; i < playerNum; i++) { + int x = assigned[i].first, y = assigned[i].second; + players[i].x = x; + players[i].y = y; + player_map[x][y].push_back(i); + } + return; + } + + // 5. 方案2:使用非逃生舱区块,仅要求相邻玩家之间的 BFS 路径距离≥5(不检查逃生舱距离) + std::fill(used.begin(), used.end(), false); + if (candidateNonExit.size() >= (unsigned)playerNum && bt(0, false)) { + for (int i = 0; i < playerNum; i++) { + int x = assigned[i].first, y = assigned[i].second; + players[i].x = x; + players[i].y = y; + player_map[x][y].push_back(i); + } + return; + } + + // 6. 方案3:直接使用非逃生舱区块随机分配(不检查距离) + if (candidateNonExit.size() >= (unsigned)playerNum) { + vector indices(candidateNonExit.size()); + for (int i = 0; i < candidateNonExit.size(); i++) + indices[i] = i; + std::shuffle(indices.begin(), indices.end(), g); + for (int i = 0; i < playerNum; i++) { + auto &block = candidateNonExit[indices[i]]; + vector> cells = block.validCells; + std::shuffle(cells.begin(), cells.end(), g); + pair chosen = cells.front(); + players[i].x = chosen.first; + players[i].y = chosen.second; + player_map[chosen.first][chosen.second].push_back(i); + } + return; + } + + // 7. 方案4:允许使用逃生舱区块 + vector candidateAll; + for (auto &b : candidateBlocks) { + if (!b.validCells.empty()) + candidateAll.push_back(b); + } + if (candidateAll.size() >= (unsigned)playerNum) { + vector indices(candidateAll.size()); + for (int i = 0; i < candidateAll.size(); i++) + indices[i] = i; + std::shuffle(indices.begin(), indices.end(), g); + for (int i = 0; i < playerNum; i++) { + auto &block = candidateAll[indices[i]]; + vector> cells = block.validCells; + std::shuffle(cells.begin(), cells.end(), g); + pair chosen = cells.front(); + players[i].x = chosen.first; + players[i].y = chosen.second; + player_map[chosen.first][chosen.second].push_back(i); + } + return; + } + + // 8. 保险方案:直接从最大连通区域随机选取位置 + std::shuffle(largest.begin(), largest.end(), g); + for (int i = 0; i < playerNum; i++) { + players[i].x = largest[i].first; + players[i].y = largest[i].second; + player_map[largest[i].first][largest[i].second].push_back(i); + } + } + + // 查找最大联通区域 + vector> FindLargestConnectedArea() + { + vector> visited(size, vector(size, false)); + vector>> components; + + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + if (visited[i][j]) continue; + + vector> comp; + queue> q; + q.push({i, j}); + visited[i][j] = true; + while (!q.empty()) { + auto [cx, cy] = q.front(); + q.pop(); + comp.push_back({cx, cy}); + int dx[4] = {-1, 1, 0, 0}; + int dy[4] = {0, 0, -1, 1}; + for (int k = 0; k < 4; k++) { + int nx = (cx + dx[k] + size) % size; + int ny = (cy + dy[k] + size) % size; + if (!visited[nx][ny] && IsConnected(cx, cy, dx[k], dy[k])) { + visited[nx][ny] = true; + q.push({nx, ny}); + } + } + } + components.push_back(comp); + } + } + + vector> largest; + for (auto &comp : components) { + if (comp.size() > largest.size()) + largest = comp; + } + if (largest.empty()) assert(false); + return largest; + } + + // 计算两点间曼哈顿距离 + int ManhattanDistance(int x1, int y1, int x2, int y2) const + { + int dx = abs(x1 - x2); + int dy = abs(y1 - y2); + dx = std::min(dx, size - dx); + dy = std::min(dy, size - dy); + return dx + dy; + } + + string GetGridStyle(const GridType type, const bool is_bg) const + { + if (is_bg) { + return "style=\"background-image: url('file:///" + image_path_ + GetGridImage(type) + "');\""; + } else { + return ""; + } + } + + string GetGridImage(const GridType type) const + { + switch (type) { + case GridType::EMPTY: return "empty.png"; + case GridType::GRASS: return "grass.png"; + case GridType::WATER: return "water.png"; + case GridType::ONEWAYPORTAL: return "oneway_portal.png"; + case GridType::PORTAL: return "portal.png"; + case GridType::EXIT: return "exit.png"; + case GridType::TRAP: return "trap.png"; + case GridType::HEAT: return "heat.png"; + case GridType::BOX: return "box.png"; + case GridType::BUTTON: return "button.png"; + default: return "unknown.png"; + } + } + + string GetWallColor(const Wall wall) const + { + switch (wall) { + case Wall::EMPTY: return "#FFFFFF"; + case Wall::NORMAL: return "#000000"; + case Wall::DOOR: return "#EA68A2"; + default: return "#D9D9D9"; + } + } + + std::mt19937 g; + inline static constexpr const char* style = R"( + +)"; +}; diff --git a/games/long_night/grid.h b/games/long_night/grid.h new file mode 100644 index 00000000..1b0640f0 --- /dev/null +++ b/games/long_night/grid.h @@ -0,0 +1,256 @@ + +enum class Direct { + UP, + DOWN, + LEFT, + RIGHT, +}; + +enum class Sound { + NONE, + SHASHA, + PAPA, + BOSS, +}; + +enum class Wall { + EMPTY, + NORMAL, + DOOR, +}; + +enum class GridType { + SPECIAL, + EMPTY, + GRASS, + WATER, + PORTAL, + EXIT, + TRAP, + HEAT, + BOX, + BUTTON, + ONEWAYPORTAL, +}; + +const map direction_map = { + {"上", Direct::UP}, {"U", Direct::UP}, {"s", Direct::UP}, + {"下", Direct::DOWN}, {"D", Direct::DOWN}, {"x", Direct::DOWN}, + {"左", Direct::LEFT}, {"L", Direct::LEFT}, {"z", Direct::LEFT}, + {"右", Direct::RIGHT}, {"R", Direct::RIGHT}, {"y", Direct::RIGHT}, +}; + +static constexpr int k_DX_Direct[4] = {-1, 1, 0, 0}; +static constexpr int k_DY_Direct[4] = {0, 0, -1, 1}; + +const string num[10] = {"⓪", "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨"}; + +const int hide_limit = 4; + +const char* score_rule = R"( + 【抓人分】
+抓人+100,被抓-100
+ 【逃生分】
+第1/2/3/4个逃生+150/200/250/250
+ 【探索分】
+每探索一个自己未探索的格子+1
+每探索一个所有玩家未探索的格子额外+1
+【退出惩罚】强制退出-300分
)"; + +Direct opposite(Direct dir) +{ + switch (dir) { + case Direct::UP: return Direct::DOWN; + case Direct::DOWN: return Direct::UP; + case Direct::LEFT: return Direct::RIGHT; + case Direct::RIGHT: return Direct::LEFT; + } + throw std::invalid_argument("Invalid direction"); +} + + +class Score +{ + public: + Score(const int size) + { + explore_map.resize(size); + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + explore_map[i].push_back(0); + } + } + } + // 抓人分 + int catch_score = 0; + // 逃生分 + static constexpr const int exit_order[4] = {150, 200, 250, 250}; + int exit_score = 0; + // 探索分 + vector> explore_map; + // 退出惩罚 + int quit_score = 0; + + pair ExploreCount() const + { + int count1 = 0, count2 = 0; + for (const auto &row : explore_map) { + count1 += count(row.begin(), row.end(), 1); + count2 += count(row.begin(), row.end(), 2); + } + return make_pair(count1, count2); + } + + static string ScoreInfo() { return score_rule; } + + int FinalScore() const { return catch_score + exit_score + ExploreScore() + quit_score; } + + private: + int ExploreScore() const + { + auto [c1, c2] = ExploreCount(); + return c2 * 2 + c1 * 1; + } +}; + + +class Player +{ + public: + Player(const PlayerID pid, const string &name, const string &avatar, const int size) + : pid(pid), name(name), avatar(avatar), score(size) {} + + // 玩家信息 + const PlayerID pid; // 玩家ID + const string name; // 玩家名字 + const string avatar; // 玩家头像 + // 出局(1被抓 2出口) + int out = 0; + // 当前坐标 + int x, y; + // 抓捕目标 + PlayerID target; + // 移动相关 + int subspace = -1; // 亚空间剩余步数 + string move_record; // 当前回合行动轨迹 + string all_record; // 历史回合行动轨迹 + string private_record; // 当前回合私信记录 + int hide_remaining = 0; // 隐匿剩余次数 + bool inHeatZone = false;// 在热源区块内 + bool heated = false; // 两次烫伤出局 + // 挂机状态(等待时间缩减) + bool hook_status = false; + // 玩家分数 + Score score; +}; + + +class Grid +{ + public: + void PortalTeleport(Player& player) const + { + player.x += portalRelPos.first; + player.y += portalRelPos.second; + player.subspace = -1; + } + + void TrapTrigger() { trap = !trap; } + + void switchDoor(const Direct dir) + { + Wall& target = wall[static_cast(dir)]; + switch (target) { + case Wall::DOOR: target = Wall::EMPTY; break; + case Wall::EMPTY: target = Wall::DOOR; break; + default:; + } + } + + void HideSpecialWalls() + { + for (int i = 0; i < 4; i++) + if (wall[i] != Wall::EMPTY) wall[i] = Wall::NORMAL; + } + + + bool IsFullyEnclosed() const + { + return wall[0] == Wall::NORMAL && wall[1] == Wall::NORMAL && wall[2] == Wall::NORMAL && wall[3] == Wall::NORMAL; + } + bool ContainWallType(const Wall w) const + { + return wall[0] == w || wall[1] == w || wall[2] == w || wall[3] == w; + } + + template + void SetWall(const Wall new_wall) { wall[static_cast(direct)] = new_wall; } + void SetWallByEnum(Direct direct, const Wall new_wall) { wall[static_cast(direct)] = new_wall; } + Grid& SetWall(const Wall up, const Wall down, const Wall left, const Wall right) + { + wall[0] = up; + wall[1] = down; + wall[2] = left; + wall[3] = right; + return *this; + } + Grid& SetType(const GridType type) + { + this->type = type; + return *this; + } + Grid& SetPortal(const int relPosX, const int relPosY) { + this->portalRelPos = {relPosX, relPosY}; + return *this; + } + Grid& SetButton(const int relPosX, const int relPosY, const optional dir = nullopt) + { + this->buttonRelPos = {relPosX, relPosY, dir}; + return *this; + } + void SetGrowable(const bool growable) { this->growable = growable; } + void SetContent(const string& content, const string& color = "") + { + if (color.empty()) { + this->content.first = content; + } else { + this->content.first = "" + content + ""; + } + } + void SetWallContent(const vector& wall_content) { this->content.second = wall_content; } + + template + Wall GetWall() const { return wall[static_cast(direct)]; } + Wall GetWallByEnum(Direct direct) const { return wall[static_cast(direct)]; } + GridType Type() const { return type; } + pair PortalPos() const { return portalRelPos; } + bool TrapStatus() const { return trap; } + bool CanGrow() const { return growable; } + pair> GetContent() const { return content; } + // 按钮触发位置关联 + struct ButtonTarget { + int dx; + int dy; + std::optional dir; + }; + ButtonTarget ButtonTargetPos() const { return buttonRelPos; } + + private: + // 区块类型 + GridType type = GridType::EMPTY; + // 四周墙面(上/下/左/右) + Wall wall[4] = { Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY }; + + // 预览用提示文本 + pair> content = {"", {}}; + // 特殊规则2草丛是否可生长 + bool growable = false; + + // 传送门相对位置(PORTAL) + pair portalRelPos = {0, 0}; + // 按钮触发位置(BUTTON) + ButtonTarget buttonRelPos = {0, 0, nullopt}; + + // 陷阱状态(TRAP) + bool trap = true; +}; diff --git a/games/long_night/icon.png b/games/long_night/icon.png new file mode 100644 index 00000000..7c3491c1 Binary files /dev/null and b/games/long_night/icon.png differ diff --git a/games/long_night/map.h b/games/long_night/map.h new file mode 100644 index 00000000..e8b8dfd5 --- /dev/null +++ b/games/long_night/map.h @@ -0,0 +1,1556 @@ +// 感谢 BC 提供的区块初始化思路 +class UnitMaps { + public: + // 撞墙提示 + static constexpr const array wall_hints = { + "砰!你狠狠地撞在了一堵墙上!", // 铁蛋 + "一阵剧痛传来,你撞上了一堵墙,看来这里走不通。", + "黑暗中,你的身体撞击了一面粗糙的墙壁。", + "你听到一声沉闷的回响——是墙壁挡住了你的去路。", + "墙壁在你面前横亘,仿佛无情地阻挠着前行的路。", + "墙壁冷漠地矗立在你面前,拒绝让你通过。", + "前方一堵高墙挡住了你的去路。", + "你试图继续前进,但一堵墙挡住了你的去路。", + "砰!你撞上了一堵墙,幸好没人看到。", + "你决定挑战墙壁,结果墙壁胜利了。", + "看来你的“穿墙术”还没练成。", + "你试图与墙壁讲道理,但它完全不想搭理你。", + "你向前冲去,然后优雅地和墙壁来了个亲密接触。", + "你试图用意念穿过墙壁,然而它比你的意念还坚定。", + "黑暗中,你的手掌摸到一堵砖墙上,你期待的邂逅没有发生。", // 大萝卜姬 + "噢是墙壁,你情不自禁地把耳朵贴了上去,你很失望。", + "是一面光滑湿软的墙!快趁机误导一下对手!信我的邪,你发出了只能自己听见的啪啪声。", + "你的手掌按在冰冷的墙面上,它仿佛正在吞噬你的温度。你是南方人啊,试试舌头吧!", + "彭!靠北啦,你不看路的吗?拜托~这么大一面墙你就这样撞啊。", + "砰!你撞到了墙上。看来你数错了,9又3/4并不是那么好找的。", + "你似乎走到了墙面上?快醒醒,这里并不是匹诺康尼,别做白日梦了。", // 三月七 + "这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。", + "你申请对墙壁过一个说服,dm拒绝了你。", + "你尝试使用闪现过墙,但可惜墙体的厚度超出了你的预期。", + "墙壁温柔的注视着你,不再言语。", + "仿生铁蛋bot会梦到电子萝卜吗?至少墙壁不会给你答案。", + "你在平原上走着,突然迎面遇到一堵墙,这墙向上无限高,向下无限深,向左无限远,向右无限远,这墙是什么?\n当然不可能有这样的墙,无论材质是什么,都会因为无限大的重力坍缩的。这只是那些神经兮兮的糟老头子的臆想罢了。\n不过,你确实撞在了一堵墙上。", // H3PO4 + "黑暗中突然出现的墙壁,像是命运在说:换个方向试试?", // 月影 + "你试图用脸测量墙壁的硬度,恭喜获得物理系荣誉学位!", + "砰!你与墙壁进行了深入交流,结论是它比你想象的更固执。", + "砰!脑门和墙壁的亲密接触,证明了你对探索的执着!", // 小葵 + }; + // 第一步撞墙提示 + static constexpr const array firststep_wall_hints = { + "这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。", // 三月七 + "你知道吗,撞击同一面墙114514次即可将其撞倒!", + "看来有人认为自己在玩多层迷宫……别想了,这是永久墙壁。", + "俗话说不撞南墙不回头,但有人撞了南墙也不回头。", + }; + // 树丛提示 + static constexpr const array grass_hints = { + "你踏入一片树丛,枯叶和树枝在脚下沙沙作响。", // 铁蛋 + "你一脚踏入了一片树丛,树叶发出了沙沙声,仿佛某种回应。", + "树叶微微颤动,沙沙声仿佛在轻声低语。", + "脚下传出“沙沙”的声音……你希望这只是树叶,而不是别的东西。", + "你踏入一片树丛,沙沙声在寂静的黑暗里显得格外刺耳。", + "枝叶在你身上扫过,沙沙声中,它刮破了你的蚕丝薄衫和渔网袜。", // 大萝卜姬 + "你突然跌进了一片黑暗的树丛,诡异的沙沙声勾起了你不好的回忆。", + "随着一阵沙沙声,密集的枝杈无情地划开了你的衣物和皮肤。", + "你路过一片树丛,出现了野生的妙蛙种子!快使用大师球!", // 三月七 + "你踩到了一根萝卜……等等,树林里怎么会有萝卜?", + "沙沙,沙沙,这片丛林的背后,会不会住着小红帽?", + "沙沙,你踏入了危险的树丛。这里要是藏着一个老六,可就遭了……", // Hyacinth + }; + // 啪啪提示 + static constexpr const array papa_hints = { + "你向前动了动,下半身传来了啪啪声。", // 大萝卜姬 + "啪啪!常在迷宫走,哪有不湿鞋?是的,你踩到了某些液体。", + "啪啪!你问踩的是什么液体?也许是我为那情人留下的泪。", + "啪啪!冰冷的液体渗入了你的鞋里。", + "啪!尽管你动作已经很轻,但还是发出了很大的声音。啪!你决定不管了。", + "啪啪!是谁那么不讲公德!随地……", + "啪啪!冰冷的涟漪在你的鞋边回荡,你想起了那个陪着铁蛋看冰块的下午。", + "啪啪!突如其来的声音让你的动作瞬间冻结;被打破的静谧仿佛被劈开的水,迅速地恢复了无声。你的敏锐好像没有得到回应。", + "啪嗒!你好像踩到了地雷?低头看一看,还好,只是一些液体。", // 三月七 + "啪!你似乎有什么东西掉了进去,可惜这里并没有河神。", + }; + // 陷阱触发提示 + static constexpr const array trap_hints = { + "深阱垂空百尺方,足悬铁索断人肠。", // 齐齐 + "犹似魂断垓下道,恨满胸中万古刀。", + "你想起守株待兔的故事,只是此刻,你成为了那只兔子。", // 纤光 + "为坠落的人类命名:_________", + "恭喜🎉被特斯拉捕获,电击调教一回合", // 特斯拉 + }; + // 热浪提示 + static constexpr const array heat_wave_hints = { + "你感受到了迎面扑来的热浪,炽热的空气仿佛要将你吞没", // 铁蛋 + "你感受到周围弥漫着炽热的气息!", + "!!请注意!!局部出现厄尔尼诺现象,气温异常升高,请注意做好防中暑措施。", // 纤光 + "你感到难以忍受的炎热,“要是周围有个水池就好了…”", + "在这座冰冷的迷宫里,你感到一阵久违的温暖,周围似乎有明亮的光源,吸引你一探究竟…?", + }; + // 热源进入提示 + static constexpr const array heat_core_hints = { + "你的脚被高温烫伤了,刺痛让你不由得倒吸一口凉气,然而周围的空气同样炙热无比", // 铁蛋 + "然而,光源并不总象征着安全,烈焰利用人对光明的向往,试图再次吞噬一个失落的灵魂。", // 纤光 + "oops!检测到核心温度急剧上升,即将超过阈值…准备启动自毁程序…", + "你相信自己的铜头铁臂可以击败一切,却不知眼前的岩浆能轻易融化所有金属。", + }; + // 热源公开提示 + static constexpr const array heat_active_hints = { + "你被滚滚热浪淹没了...周围的一切都在高温中扭曲变形,只剩下火焰的深渊。", // 铁蛋 + "哦不!你落入了巨人萝卜的火锅池里,这下你只好成为萝卜的夜宵了。", // 纤光 + "你失败了!\nSteve试图在岩浆中游泳。", + }; + // 逃生舱提示 + static constexpr const array exit_hints = { + "你坐进了逃生舱,在启动的轰鸣声中,你想起了那句话:“不要忘了,这个世界穿透一切高墙的东西,它就在我们的内心深处,他们无法达到,也接触不到,那就是希望。”", // 大萝卜姬 + "躺在逃生舱内,平日并不虔诚的你颤巍巍地画着十字,双手合十,嘴里念念有词。诸如什么真主阿拉耶稣基督释迦牟利急急如律令之类。前窗仿佛响应了你的号召,一阵白色闪光迅速笼罩了你。正当你诧异得到了哪位神仙的庇佑时,眼前浮现出两个大字。一个振奋人心的声音在你耳边响起:“原神,启动!”", + "你忘记躺了多久,你只记得这里很温暖、舒适、令人安心。直到一股力量把你从舱内抽离;强光穿透了你稚嫩的眼皮;你哭了,你向世界宣告着你的降临。也许你只是此刻降生的其中之一,但在她眼里,你就是她的唯一。", + "是的。夜色再浓,也挡不住黎明的到来,就像再大的困难也挡不住我们的前进。黑暗即将过去,曙光就在前头!", + "你出生后 时间就已所剩无几\n在妈妈离世之后 我不知对你倾注了多少的爱呢\n但是你的微笑让爸爸备受鼓舞呀(^_^)\n其实要是能一起走就好了 但没能做到\n希望你能忘记一切继续前行 你一定可以做到的", // 纤光 + "当逃生舱门缓缓关闭,伴随着沉闷的启动声,黑暗迷宫逐渐远去。依靠在逃生舱内冷冽的仪表光芒中,你仿佛听见遥远星辰的低语:“未来,总为勇者留下一缕希望。”", // 铁蛋 + "舱门缓缓关闭,逃生舱的指示灯一一亮起,冰冷的金属包裹着你,但比起外面的黑暗,这里却意外地令人安心。你知道,一切都已经结束,或许,也是一切的开始。", + "你说的对,但是《漫漫长夜》是由大萝卜姬自主研发的一款全新大逃杀游戏。游戏发生在一个被称作“黑暗迷宫”的地图,在这里,被铁蛋选中的人将被授予“树丛”,导引“沙沙”之力。你将扮演一位名为“狩猎者”的神秘角色,在自由的旅行中邂逅性格各异、能力独特的墙壁,和它们一起阻拦对手,找到隐匿的“逃生者”——同时,逐步发掘“逃生舱”的真相。", // 三月七 + "进入逃生舱后,随着几下逐渐变弱的震动,周围的环境随之稳定下来。也就在这时,你眼前闪过一道白光,似乎是这使得你进入了一个全新的环境,伴随着的还有来自外部的一阵欢呼声:“太好了!成功抓住宝可梦了!”", // faust + "一阵失重后,舱门终于打开。随着刺眼的白光,在指缝间你看见几个面目可憎的巨人在围观你。很快地你被巨大的餐叉粗暴地刺穿;顾不及对痛觉反应,你便殒命在血盘大口之中。健硕、坚定、智慧、乐观,这些优秀的品质在他们嘴里同样珍贵。", // 大萝卜姬 + "当逃生舱的舱门关闭时,你才发现手中的钥匙根本不属于这里。系统提示音冰冷地重复着:身份验证失败。原来从一开始,你就只是这个迷宫的装饰品而已。", // 月影 + "逃生舱启动的瞬间,你突然想起那个古老的预言:'逃出迷宫的人将获得永生,但代价是永远孤独'。舱体剧烈震动起来,不知是故障还是某种警告...", + "舱内显示屏突然亮起:'恭喜您成为第1024位逃生者!作为奖励,系统将向您展示迷宫的真相...'画面切换的瞬间,你看到了无数个一模一样的逃生舱,里面坐着无数个一模一样的你。", + "当逃生舱启动时,你突然明白:黑暗不是终点,而是黎明前的温柔。舱内温度逐渐升高,那不是故障,而是新生的心跳。", + "逃生舱启动的轰鸣声渐渐平息,取而代之的是轻柔的摇篮曲。透过舷窗,你看见繁星组成的银河缓缓流动——原来迷宫的出口,一直连接着整片宇宙。", + "当舱门完全关闭的瞬间,你听见系统轻声说:'恭喜,这是第1024次模拟。根据数据,你这次终于选择相信自己了。' 周围突然亮起温暖的阳光,原来真正的逃生舱,一直都在你心里。", + "逃生舱的显示屏突然亮起一行字:'记住,黑暗只是光明的候车室。' 随着这句话,整个舱体开始散发出柔和的金色光芒,照亮了通往新世界的道路。", + }; + // 捕捉提示 + static constexpr const array catch_hints = { + "星光黯淡,你们的相遇,是命中注定,亦是命终注定。", // 纤光 + "你化作一道黑影,在血月之下,无情地终结了又一条生命。", + "你想触碰一切的真相,但在对方空洞无神的双眼中,你没能找到答案。", + "你叹了口气,在黑暗森林里,你不得不这样做。", + "我说我杀人不眨眼,你问我眼睛干不干?永别了", // 克里斯丁 + "感谢你为了我自愿放弃逃生资格", + }; + // 同格树丛声响提示 + static constexpr const array grass_sound_hints = { + "你听见有人进入了你的小树丛,沙沙声很近很近;一股接一股热气扑向了你的耳朵;呼…哈……呼…哈……他好像很累的样子。积极地想,他也许没有察觉到你", // 大萝卜姬 + "你听见有人进入了你所在的树丛,他从旁边匆匆走过,没有发现你。阴暗的想法在你心里成长起来,是让他帮你探路,还是直接干掉。甚至运气好的话,抢在前面牛了他的逃生舱……(额外探索分只有1分并逃生一事在漫漫长夜中亦有记载)", // Hyacinth + }; + // 同格啪啪声响提示 + static constexpr const array papa_sound_hints = { + "“啪!”你汗毛直立,有人来了。。幸好,你并没有站在中间,多疑多虑的性格给了你久违的回报。你小心翼翼地蹲了下来,尽力减少接触概率。只是黑暗中你低估了脚下液体的深度。“等他走远,再把内裤脱了吧。。”你暗暗地想。", // 大萝卜姬 + "啪!啪!看来是有人来了。两个人,狭小的隔间,不间断地啪啪声……'淫秽的人!'你的脑海回想起了她的声音。是啊。我承认,我确实有点想她了。", + }; + // 无逃生舱最后生还 + static constexpr const array withoutE_win_hints = { + "我睁开了双眼,眼前的一切既熟悉又陌生。看来这次终于是我赢了。我用力地端详着周遭的一切,试图捕捉错过的几日时光的任何蛛丝马迹。“我真希望他们彻底离开了 ......”说完,我在床脚拿起了本该在枕边的剃须刀;“看来上次赢的是萝卜。”我下意识地抹了抹嘴唇。在指尖晕开的口红证实了我的猜测。我笑了。", // 大萝卜姬 + "你醒啦?现在已经是第二天了哦。\n明媚的阳光照进迷宫,耳旁传来小鸟的叫声,一切美好的不太真实,唯有眼前冰冷的血迹,无声的诉说着昨晚的那场噩梦,而有些人,永远留在了那场梦中。\n可你,真的从中逃出来了吗?\n“地形参数设置完毕,新的循环正在重启……”", // 纤光 + }; + // 有逃生舱但死斗取胜 + static constexpr const array withE_win_hints = { + "“已经没事啦~”她温柔地从背后把你抱住,轻轻抚摸着头。“你很勇敢,这一步太不容易了。”你转过身,紧紧埋进她柔软的身体里放肆哭泣。“一切都结束了。不用再害怕了。”她低下头凑近你的耳边,“咱们回家叭…”", // 大萝卜姬 + "多年以后,在家族背景下你在事业上取得了巨大成就。大家把你的性情大变归功于当年失踪逃生的经历。“之前那个玩世不恭的我已经死了。”你每次都这样认真回答大家。至于细节的提问嘛,失忆这个理由你很喜欢。", + }; + + static string RandomHint(std::span hints) + { + return string(hints[rand() % hints.size()]); + } + + vector> pos = { + {0, 0}, {0, 3}, {0, 6}, + {3, 0}, {3, 3}, {3, 6}, + {6, 0}, {6, 3}, {6, 6}, + }; + vector> origin_pos; + + struct Map { + vector> block; + GridType type; + string id; + }; + const int k_map_num = 12; + const int k_exit_num = 4; + std::mt19937 g; + + vector all_maps = { + {Map1(), GridType::WATER, "1"}, + {Map2(), GridType::PORTAL, "2"}, + {Map3(), GridType::GRASS, "3"}, + {Map4(), GridType::GRASS, "4"}, + {Map5(), GridType::WATER, "5"}, + {Map6(), GridType::PORTAL, "6"}, + {Map7(), GridType::GRASS, "7"}, + {Map8(), GridType::GRASS, "8"}, + {Map9(), GridType::EMPTY, "9"}, + {Map10(), GridType::EMPTY, "10"}, + {Map11(), GridType::GRASS, "11"}, + {Map12(), GridType::GRASS, "12"}, + {Map13(), GridType::PORTAL, "13"}, + {Map14(), GridType::PORTAL, "14"}, + {Map15(), GridType::PORTAL, "15"}, + {Map16(), GridType::PORTAL, "16"}, + {Map17(), GridType::WATER, "17"}, + {Map18(), GridType::WATER, "18"}, + {Map19(), GridType::EMPTY, "19"}, + {Map20(), GridType::EMPTY, "20"}, + {Map21(), GridType::EMPTY, "21"}, + {Map22(), GridType::EMPTY, "22"}, + {Map23(), GridType::TRAP, "23"}, + {Map24(), GridType::TRAP, "24"}, + {Map25(), GridType::HEAT, "25"}, + {Map26(), GridType::BOX, "26"}, + {Map27(), GridType::BOX, "27"}, + {Map28(), GridType::PORTAL, "28"}, + {Map29(), GridType::WATER, "29"}, + {Map30(), GridType::WATER, "30"}, + // {Map31(), GridType::PORTAL, "31"}, + // {Map32(), GridType::PORTAL, "32"}, + {Map33(), GridType::TRAP, "33"}, + {Map34(), GridType::BUTTON, "34"}, + }; + vector all_exits = { + {Exit1(), GridType::EXIT, "1"}, + {Exit2(), GridType::EXIT, "2"}, + {Exit3(), GridType::EXIT, "3"}, + {Exit4(), GridType::EXIT, "4"}, + {Exit5(), GridType::EXIT, "5"}, + {Exit6(), GridType::EXIT, "6"}, + {Exit7(), GridType::EXIT, "7"}, + {Exit8(), GridType::EXIT, "8"}, + {Exit9(), GridType::EXIT, "9"}, + {Exit10(), GridType::EXIT, "10"}, + }; + vector special_maps = { + {SMap1(), GridType::SPECIAL, "S1"}, + {SMap2(), GridType::SPECIAL, "S2"}, + {SMap3(), GridType::SPECIAL, "S3"}, + {SMap4(), GridType::SPECIAL, "S4"}, + }; + + const vector rotation_maps_id = { + "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", + "21", "22", "23", "24", "26", + "29", "30", "33", "34" + }; + const vector rotation_exits_id = {"1", "2", "3", "4"}; + vector rotation_maps; + vector rotation_exits; + + vector maps; + vector exits; + + UnitMaps(const int32_t mode) + { + std::random_device rd; + g = std::mt19937(rd()); + if (mode == 0) { + maps.insert(maps.end(), all_maps.begin(), all_maps.begin() + k_map_num); + exits.insert(exits.end(), all_exits.begin(), all_exits.begin() + k_exit_num); + } else if (mode == 1) { + std::sample(all_maps.begin(), all_maps.end(), std::back_inserter(maps), k_map_num, g); + SampleExits(all_exits, k_exit_num / 2); + } else if (mode == 2) { + for (const auto &m : all_maps) { + if (std::find(rotation_maps_id.begin(), rotation_maps_id.end(), m.id) != rotation_maps_id.end()) { + rotation_maps.push_back(m); + } + } + for (const auto &m : all_exits) { + if (std::find(rotation_exits_id.begin(), rotation_exits_id.end(), m.id) != rotation_exits_id.end()) { + rotation_exits.push_back(m); + } + } + std::sample(rotation_maps.begin(), rotation_maps.end(), std::back_inserter(maps), k_map_num, g); + SampleExits(rotation_exits, k_exit_num / 2); + } else { + std::sample(special_maps.begin(), special_maps.end(), std::back_inserter(maps), 2, g); + std::sample(all_maps.begin(), all_maps.end(), std::back_inserter(maps), k_map_num - 2, g); + SampleExits(all_exits, k_exit_num / 2); + } + origin_pos = pos; + } + + void SampleExits(const vector& exits_pool, const int k_exit_pair) + { + if (exits_pool.size() % 2 != 0) return; + + int pair_num = exits_pool.size() / 2; + vector pairs(pair_num); + std::iota(pairs.begin(), pairs.end(), 0); + std::shuffle(pairs.begin(), pairs.end(), g); + pairs.resize(k_exit_pair); + std::sort(pairs.begin(), pairs.end()); + for (int p : pairs) { + exits.push_back(exits_pool[p * 2]); + exits.push_back(exits_pool[p * 2 + 1]); + } + } + + vector> FindBlockById(const string id, const bool is_exit, const bool special = false) const + { + const vector& search_list = is_exit ? (special ? exits : all_exits) : (special ? maps : all_maps); + auto it = std::find_if(search_list.begin(), search_list.end(), [id](const Map& map) { return map.id == id; }); + if (it != search_list.end()) return it->block; + auto special_it = std::find_if(special_maps.begin(), special_maps.end(), [id](const Map& map) { return map.id == id; }); + if (special_it != special_maps.end()) return special_it->block; + return InitializeMapTemplate(); + } + + static bool MapContainGridType(const vector& maps, const GridType& type) + { + for (const auto& map: maps) { + for (int k = 0; k < 9; ++k) { + if (map.block[k / 3][k % 3].Type() == type) { + return true; + } + } + } + return false; + } + + static bool MapContainWallType(const vector& maps, const Wall& wall) + { + for (const auto& map: maps) { + for (int k = 0; k < 9; ++k) { + if (map.block[k / 3][k % 3].ContainWallType(wall)) { + return true; + } + } + } + return false; + } + + // 特殊事件详情 + static string ShowSpecialEvent(const int type) + { + if (type == 1) { + return "[特殊事件]【怠惰的园丁】树丛将在其区块内随机位置生成(有可能生成在中间)"; + } else if (type == 2) { + return "[特殊事件]【营养过剩】树丛和陷阱将额外向随机1个方向再次生成1个树丛(不可隔墙生长)"; + } else if (type == 3) { + return "[特殊事件]【雨天小故事】地图中所有树丛变成水洼,陷阱会发出啪啪声"; + } else { + return "无"; + } + } + + // 特殊事件1——怠惰的园丁:草丛将在其区块内随机位置生成 + void SpecialEvent1() + { + auto MarkMaps = [](auto& maps) { + for (auto& map : maps) { + for (int k = 0; k < 9; ++k) { + int i = k / 3, j = k % 3; + if (map.block[i][j].Type() == GridType::GRASS) + map.block[i][j].SetContent("?"); + } + } + }; + auto ProcessMaps = [](auto& maps) { + for (auto& map: maps) { + for (int k = 0; k < 9; ++k) { + int i = k / 3, j = k % 3; + if (map.block[i][j].Type() == GridType::GRASS) { + map.block[i][j].SetType(GridType::EMPTY); + int m; + do { + m = rand() % 9; + } while (map.block[m / 3][m % 3].Type() != GridType::EMPTY); + map.block[m / 3][m % 3].SetType(GridType::GRASS); + break; + } + } + } + }; + + MarkMaps(all_maps); + MarkMaps(all_exits); + MarkMaps(special_maps); + ProcessMaps(maps); + ProcessMaps(exits); + } + + // 特殊事件2——营养过剩:树丛和陷阱将额外向随机1个方向再次生成1个树丛 + void SpecialEvent2() + { + auto MarkMaps = [](auto& maps) { + for (auto& map : maps) { + for (int k = 0; k < 9; ++k) { + int i = k / 3, j = k % 3; + if (map.block[i][j].CanGrow()) + map.block[i][j].SetContent("?"); + } + } + }; + auto ProcessMaps = [this](auto& maps) { + for (auto& map : maps) { + vector> growablePositions; + int grassCount = 0; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + if (map.block[i][j].Type() == GridType::GRASS || map.block[i][j].Type() == GridType::TRAP) { + grassCount++; + } + if (map.block[i][j].CanGrow()) { + growablePositions.emplace_back(i, j); + } + } + } + int targetGrowth = min(grassCount, static_cast(growablePositions.size())); + if (targetGrowth == 0) continue; // 无法生长则跳过 + + std::shuffle(growablePositions.begin(), growablePositions.end(), g); + for (int i = 0; i < targetGrowth; i++) { + int x = growablePositions[i].first; + int y = growablePositions[i].second; + map.block[x][y].SetType(GridType::GRASS); + } + } + }; + + MarkMaps(all_maps); + MarkMaps(all_exits); + MarkMaps(special_maps); + ProcessMaps(maps); + ProcessMaps(exits); + } + + // 特殊事件3——雨天小故事:地图中所有树丛变成水洼 + void SpecialEvent3() + { + auto ProcessMaps = [](auto& maps) { + for (auto& map: maps) { + for (int k = 0; k < 9; ++k) { + int i = k / 3, j = k % 3; + if (map.block[i][j].Type() == GridType::GRASS) { + map.block[i][j].SetType(GridType::WATER); + } + } + } + }; + + ProcessMaps(maps); + ProcessMaps(exits); + ProcessMaps(special_maps); + } + + // 大地图区块位置随机 + bool RandomizeBlockPosition(const int size) + { + vector> candidates; + for (int i = 0; i < size - 2; i++) { + for (int j = 0; j < size - 2; j++) { + candidates.push_back({i, j}); + } + } + std::shuffle(candidates.begin(), candidates.end(), g); + vector> chosen; + bool found = backtrack(0, chosen, candidates); + if (found) { + for (int i = 0; i < 9; i++) { + pos[i] = chosen[i]; + } + } + return found; + } + + // 边长12:16个区块 + void SetMapPosition12() + { + pos.insert(pos.begin() + 3, {0, 9}); + pos.insert(pos.begin() + 6, {3, 9}); + pos.push_back({6, 9}); + pos.push_back({9, 0}); + pos.push_back({9, 3}); + pos.push_back({9, 6}); + pos.push_back({9, 9}); + origin_pos = pos; + } + + void SetMapBlock(const int x, const int y, vector>& grid_map, const string& map_id, const bool special) const + { + SetBlock(x, y, grid_map, FindBlockById(map_id, false, special)); + } + + void SetExitBlock(const int x, const int y, vector>& grid_map, const string& exit_id, const bool special) const + { + SetBlock(x, y, grid_map, FindBlockById(exit_id, true, special)); + } + + private: + void SetBlock(const int x, const int y, vector>& grid_map, const vector> block) const + { + for (int i = 0; i < block.size(); i++) { + for (int j = 0; j < block[i].size(); j++) { + grid_map[x + i][y + j] = block[i][j]; + } + } + } + + static bool overlaps(const pair& a, const pair& b) + { + if (a.first + 2 < b.first || b.first + 2 < a.first || a.second + 2 < b.second || b.second + 2 < a.second) + return false; + return true; + } + + // 回溯搜索 + static bool backtrack(int index, vector>& chosen, const vector>& candidates) + { + if (chosen.size() == 9) { + return true; + } + for (int i = index; i < candidates.size(); i++) { + bool valid = true; + for (const auto& p : chosen) { + if (overlaps(p, candidates[i])) { + valid = false; + break; + } + } + if (!valid) + continue; + chosen.push_back(candidates[i]); + if (backtrack(i + 1, chosen, candidates)) + return true; + chosen.pop_back(); + } + return false; + } + + static vector> InitializeMapTemplate() + { + vector> map; + map.resize(3); + for (auto& row : map) { + row.resize(3); + } + return map; + } + + static vector> Map1() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::WATER); + map[0][2].SetType(GridType::WATER); + map[2][0].SetType(GridType::WATER); + map[2][2].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Map2() + { + auto map = InitializeMapTemplate(); + map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2); + map[1][1].SetType(GridType::GRASS); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[0][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + return map; + } + + static vector> Map3() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::GRASS); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[0][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + + return map; + } + + static vector> Map4() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::GRASS); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Map5() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::WATER); + map[0][2].SetType(GridType::WATER); + map[2][0].SetType(GridType::WATER); + map[2][2].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Map6() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2); + map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2); + map[1][1].SetType(GridType::GRASS); + + map[0][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[1][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[2][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Map7() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::GRASS); + + map[0][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + + map[1][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[2][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + + return map; + } + + static vector> Map8() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::GRASS); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + + return map; + } + + static vector> Map9() + { + auto map = InitializeMapTemplate(); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Map10() + { + auto map = InitializeMapTemplate(); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + + return map; + } + + static vector> Map11() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::GRASS); + + map[0][0].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][2].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[1][0].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + return map; + } + + static vector> Map12() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::GRASS); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL).SetGrowable(true); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL).SetGrowable(true); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL).SetGrowable(true); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL).SetGrowable(true); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL).SetGrowable(true); + + return map; + } + + static vector> Map13() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::WATER); + map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2); + map[2][2].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[0][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Map14() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2); + map[0][2].SetType(GridType::WATER); + map[2][0].SetType(GridType::WATER); + map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2); + + map[0][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[2][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Map15() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::PORTAL).SetPortal(0, 2); + map[0][2].SetType(GridType::PORTAL).SetPortal(0, -2); + map[2][0].SetType(GridType::WATER); + map[2][2].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[0][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Map16() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::PORTAL).SetPortal(1, 0); + map[2][1].SetType(GridType::PORTAL).SetPortal(-1, 0); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + + map[2][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[2][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Map17() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::WATER); + map[0][2].SetType(GridType::WATER); + map[1][1].SetType(GridType::GRASS); + map[2][0].SetType(GridType::WATER); + map[2][2].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[0][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Map18() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::WATER); + map[0][2].SetType(GridType::WATER); + map[1][1].SetType(GridType::GRASS); + map[2][0].SetType(GridType::WATER); + map[2][2].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[0][2].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[2][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Map19() + { + auto map = InitializeMapTemplate(); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + + map[1][0].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + + return map; + } + + static vector> Map20() + { + auto map = InitializeMapTemplate(); + + map[0][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[1][2].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + + map[2][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Map21() + { + auto map = InitializeMapTemplate(); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + map[0][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[2][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Map22() + { + auto map = InitializeMapTemplate(); + + map[0][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + + return map; + } + + static vector> Map23() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::TRAP); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + + return map; + } + + static vector> Map24() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::TRAP); + + map[0][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Map25() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::HEAT); + + return map; + } + + static vector> Map26() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::BOX); + + return map; + } + + static vector> Map27() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::BOX); + map[2][2].SetType(GridType::BOX); + + return map; + } + + static vector> Map28() + { + auto map = InitializeMapTemplate(); + map[1][0].SetType(GridType::PORTAL).SetPortal(0, 2); + map[1][2].SetType(GridType::PORTAL).SetPortal(0, -2); + map[1][1].SetType(GridType::TRAP); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[1][0].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + return map; + } + + static vector> Map29() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::WATER); + map[1][1].SetType(GridType::WATER); + map[2][2].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Map30() + { + auto map = InitializeMapTemplate(); + map[0][2].SetType(GridType::WATER); + map[1][1].SetType(GridType::WATER); + map[2][0].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Map31() + { + auto map = InitializeMapTemplate(); + map[0][2].SetType(GridType::ONEWAYPORTAL).SetPortal(2, -2); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2); + map[1][1].SetType(GridType::GRASS); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[0][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + return map; + } + + static vector> Map32() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::ONEWAYPORTAL).SetPortal(2, 2); + map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2); + map[1][1].SetType(GridType::GRASS); + + map[0][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[1][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[2][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Map33() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::TRAP); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + return map; + } + + static vector> Map34() + { + auto map = InitializeMapTemplate(); + map[0][2].SetType(GridType::BUTTON).SetButton(0, -2, Direct::DOWN).SetContent("A"); + map[2][0].SetType(GridType::BUTTON).SetButton(0, 2, Direct::UP).SetContent("B"); + + map[0][0].SetWall(Wall::EMPTY, Wall::DOOR, Wall::EMPTY, Wall::EMPTY).SetWallContent({"", "A", "", ""}); + map[0][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::DOOR, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::NORMAL, Wall::DOOR, Wall::EMPTY, Wall::EMPTY); + + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::DOOR, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetWallContent({"B", "", "", ""}); + + return map; + } + + // static vector> Map35() + // { + // auto map = InitializeMapTemplate(); + // map[0][0].SetType(GridType::WATER); + // map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2); + // map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2); + // map[2][2].SetType(GridType::WATER); + + // map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + // map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + // map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + // map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + // map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + // map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + // map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + // map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + // map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + // return map; + // } + + static vector> Exit1() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::EXIT); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Exit2() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::EXIT); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Exit3() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::EXIT); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Exit4() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::EXIT); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Exit5() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2); + map[0][2].SetType(GridType::WATER); + map[1][1].SetType(GridType::EXIT); + map[2][0].SetType(GridType::WATER); + map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Exit6() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::WATER); + map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2); + map[1][1].SetType(GridType::EXIT); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2); + map[2][2].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[1][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Exit7() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::WATER); + map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2); + map[1][1].SetType(GridType::EXIT); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2); + map[2][2].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Exit8() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::WATER); + map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2); + map[1][1].SetType(GridType::EXIT); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2); + map[2][2].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Exit9() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2); + map[0][1].SetType(GridType::EXIT); + map[1][1].SetType(GridType::TRAP); + map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Exit10() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2); + map[1][1].SetType(GridType::TRAP); + map[2][1].SetType(GridType::EXIT); + map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::NORMAL); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + // 特殊地图 + static vector> SMap1() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::WATER); + map[0][1].SetType(GridType::TRAP); + map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2); + map[1][0].SetType(GridType::GRASS); + map[1][1].SetType(GridType::HEAT); + map[1][2].SetType(GridType::GRASS); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2); + map[2][1].SetType(GridType::TRAP); + map[2][2].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> SMap2() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2).SetContent("A", "#FFF8E7"); + map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2).SetContent("B", "#FFF8E7"); + map[1][1].SetType(GridType::WATER); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2).SetContent("B", "#FFF8E7"); + map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2).SetContent("A", "#FFF8E7"); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> SMap3() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2).SetContent("A", "#FFF8E7"); + map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2).SetContent("B", "#FFF8E7"); + map[1][1].SetType(GridType::TRAP); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2).SetContent("B", "#FFF8E7"); + map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2).SetContent("A", "#FFF8E7"); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + map[0][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> SMap4() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2).SetContent("A", "#FFF8E7"); + map[0][1].SetType(GridType::PORTAL).SetPortal(2, 0).SetContent("B", "#FFF8E7"); + map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2).SetContent("C", "#FFF8E7"); + map[1][0].SetType(GridType::PORTAL).SetPortal(0, 2).SetContent("D", "#FFF8E7"); + map[1][1].SetType(GridType::PORTAL).SetPortal(0, 0).SetContent("E(E)", "#FFF8E7"); + map[1][2].SetType(GridType::PORTAL).SetPortal(0, -2).SetContent("D", "#FFF8E7"); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2).SetContent("C", "#FFF8E7"); + map[2][1].SetType(GridType::PORTAL).SetPortal(-2, 0).SetContent("B", "#FFF8E7"); + map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2).SetContent("A", "#FFF8E7"); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + return map; + } +}; diff --git a/games/long_night/mygame.cc b/games/long_night/mygame.cc new file mode 100644 index 00000000..6b74dc67 --- /dev/null +++ b/games/long_night/mygame.cc @@ -0,0 +1,895 @@ +// Copyright (c) 2018-present, JiaQi Yu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include +#include +#include +#include + +#include "game_framework/stage.h" +#include "game_framework/util.h" +#include "utility/html.h" + +using namespace std; + +#include "grid.h" +#include "map.h" +#include "board.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +class MainStage; +template using SubGameStage = StageFsm; +template using MainGameStage = StageFsm; +const GameProperties k_properties { + .name_ = "漫漫长夜", // the game name which should be unique among all the games + .developer_ = "铁蛋", + .description_ = "在漆黑的迷宫中探索,根据有限的线索展开追击与逃生", + .shuffled_player_id_ = true, +}; +uint64_t MaxPlayerNum(const CustomOptions& options) { return 8; } +uint32_t Multiple(const CustomOptions& options) { return 1; } +const MutableGenericOptions k_default_generic_options{ + .is_formal_{false}, +}; +const std::vector k_rule_commands = {}; + +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +{ + if (GET_OPTION_VALUE(game_options, 特殊事件) == -1) { + GET_OPTION_VALUE(game_options, 特殊事件) = rand() % 3 + 1; + } + if (generic_options_readonly.PlayerNum() > 6 && GET_OPTION_VALUE(game_options, 边长) < 12) { + GET_OPTION_VALUE(game_options, 边长) = 12; + reply() << "[警告] 玩家数 " << generic_options_readonly.PlayerNum() << " 超出普通地图限制,自动将地图边长调整为 12*12"; + } + return true; +} + +const std::vector k_init_options_commands = { + InitOptionsCommand("设定特殊事件或游戏模式", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const int32_t mode) + { + switch (mode) { + case -1: + case 1: + case 2: + case 3: + GET_OPTION_VALUE(game_options, 特殊事件) = mode; break; + case 10: + case 12: + GET_OPTION_VALUE(game_options, 边长) = mode; break; + case 20: + GET_OPTION_VALUE(game_options, 大乱斗) = true; break; + case 21: + case 22: + GET_OPTION_VALUE(game_options, 隐匿) = mode - 20; break; + case 23: + GET_OPTION_VALUE(game_options, 点杀) = true; break; + case 24: + GET_OPTION_VALUE(game_options, BOSS) = true; break; + case 30: + case 31: + case 32: + case 33: + GET_OPTION_VALUE(game_options, 模式) = mode - 30; break; + case 100: + return NewGameMode::SINGLE_USER; + default:; + } + return NewGameMode::MULTIPLE_USERS; + }, + AlterChecker({ + {"单机", 100}, {"随机", -1}, {"怠惰的园丁", 1}, {"营养过剩", 2}, {"雨天小故事", 3}, + {"10*10", 10}, {"12*12", 12}, {"大乱斗", 20}, {"回合隐匿", 21}, {"单步隐匿", 22}, {"点杀", 23}, {"BOSS", 24}, + {"标准", 30}, {"狂野", 31}, {"幻变", 32}, {"疯狂", 33} + })), +}; + +// ========== GAME STAGES ========== + +class RoundStage; + +class MainStage : public MainGameStage +{ + public: + MainStage(StageUtility&& utility) + : StageFsm(std::move(utility), + MakeStageCommand(*this, "查看所有区块信息和图例", &MainStage::BlockInfo_, VoidChecker("地图")), + MakeStageCommand(*this, "指定区块生成地图预览,逃生舱区块需要加 'E' 前缀,未知区块用 0 代替", &MainStage::MapPreview_, + VoidChecker("预览"), RepeatableChecker>("序号", "2 3 0 11 E1 0 E2 7 9"))) + , round_(0) + , player_scores_(Global().PlayerNum(), 0) + , board(Global().ResourceDir(), GAME_OPTION(模式)) + {} + + virtual int64_t PlayerScore(const PlayerID pid) const override { return player_scores_[pid]; } + + std::vector player_scores_; + + // 回合数 + int round_; + // 地图 + Board board; + // 无逃生舱最后生还胜利判定 + bool withoutE_win_ = false; + // 无逃生舱最后生还判定 + bool withE_win_ = false; + + // 剩余玩家数 + int Alive_() const { return std::count_if(board.players.begin(), board.players.end(), [](const auto& player){ return player.out == 0; }); } + + private: + CompReqErrCode BlockInfo_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + auto sender = reply(); + if (GAME_OPTION(特殊事件) > 0) { + sender << UnitMaps::ShowSpecialEvent(GAME_OPTION(特殊事件)) << "\n"; + } + if (GAME_OPTION(边长) > 9) { + sender << "本局游戏地图为 " << GAME_OPTION(边长) << "x" << GAME_OPTION(边长) << "\n"; + } + sender << Markdown(board.GetAllBlocksInfo(GAME_OPTION(特殊事件)), 1000); + return StageErrCode::OK; + } + + CompReqErrCode MapPreview_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const vector& map_string) + { + vector map_str = map_string; + map_str.resize(board.unitMaps.pos.size(), "0"); + reply() << Markdown(board.MapGenerate(map_str), 60 * (GAME_OPTION(边长) + 1)); + return StageErrCode::OK; + } + + void FirstStageFsm(SubStageFsmSetter setter) + { + // Global().SaveMarkdown(board.GetAllBlocksInfo(GAME_OPTION(特殊事件), GAME_OPTION(模式)), 1000); // 用于生成全部地图列表 + srand((unsigned int)time(NULL)); + + auto sender = Global().Boardcast(); + if (!GAME_OPTION(捉捕目标)) { + sender << "【捉捕顺位】本局玩家的捉捕顺位为相反顺序,捉捕目标变更为上家\n\n"; + } + if (GAME_OPTION(特殊事件) > 0) { // 特殊事件 + switch (GAME_OPTION(特殊事件)) { + case 1: board.unitMaps.SpecialEvent1(); break; + case 2: board.unitMaps.SpecialEvent2(); break; + case 3: board.unitMaps.SpecialEvent3(); break; + } + sender << UnitMaps::ShowSpecialEvent(GAME_OPTION(特殊事件)) << "\n\n"; + } + if (GAME_OPTION(边长) == 10) { // 边长10 + if (board.unitMaps.RandomizeBlockPosition(GAME_OPTION(边长))) { + sender << "【10*10】本局游戏地图将更改为 " << GAME_OPTION(边长) << "x" << GAME_OPTION(边长) << " 大地图。9个区块在大地图随机排列,区块不会重叠。没有区块覆盖的地图空隙将变成普通道路。\n"; + } else { + sender << "[错误] 生成10*10地图时发生错误:未能成功随机布局,游戏无法正常开始!"; + return; + } + } + if (GAME_OPTION(边长) == 12) { // 边长12 + board.unitMaps.SetMapPosition12(); + board.exit_num = 4; + sender << "【12*12】本局游戏地图将更改为 " << GAME_OPTION(边长) << "x" << GAME_OPTION(边长) << " 大地图。使用 16 个区块铺满地图,逃生舱固定为 4 个。\n"; + } + if (GAME_OPTION(点杀)) { // 点杀 + sender << "【点杀模式】捕捉改为仅在回合结束时触发,路过不会触发捕捉\n"; + } + if (GAME_OPTION(隐匿) == 1) { // 隐匿 + sender << "【隐匿模式】回合隐匿:隐匿效果持续1回合,**仅可使用1次**,隐匿后当回合的行动转为私聊进行,不会发出声响,不会触发捕捉。\n"; + } else if (GAME_OPTION(隐匿) == 2) { + sender << "【隐匿模式】单步隐匿:隐匿效果仅作用于下一步,**可使用" << hide_limit << "次**,隐匿后在私聊行动一步,不会发出声响,不会触发捕捉。\n"; + } + if (GAME_OPTION(大乱斗)) { + sender << "【大乱斗模式】所有的逃生舱改为随机传送!但仍会统计逃生分\n"; + } + + board.size = GAME_OPTION(边长); + // 初始化玩家 + board.playerNum = Global().PlayerNum(); + board.players.reserve(board.playerNum); + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + board.players.emplace_back(pid, Global().PlayerName(pid), Global().PlayerAvatar(pid, 40), board.size); + if (GAME_OPTION(隐匿) == 1) { // 隐匿 + board.players[pid].hide_remaining = 1; + } else if (GAME_OPTION(隐匿) == 2) { + board.players[pid].hide_remaining = hide_limit; + } + } + board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); + // 出口数 + if (GAME_OPTION(边长) != 12) { + if (Global().PlayerNum() > 1) { + board.exit_num = Global().PlayerNum() / 2; + } else { + board.exit_num = 1; + } + } + // 单机模式 + if (Global().PlayerNum() == 1) board.players[0].target = 100; + // 初始化地图 + board.Initialize(GAME_OPTION(BOSS)); + + if (GAME_OPTION(BOSS)) { + board.boss.all_record = "
【开局】初始锁定玩家为 [" + to_string(board.boss.target) + "号](巨响)"; + sender << "【BOSS】米诺陶斯现身于地图中,会在回合结束时追击最近的玩家。BOSS发出震耳欲聋的巨响!请所有玩家留意BOSS开局所在的方位!\n"; + sender << "当前BOSS锁定的玩家为 " << At(board.boss.target) << "\n"; + } + + sender << "[提示] 本局游戏人数为 " << board.playerNum << " 人,逃生舱数量为 " << board.exit_num << " 个。请留意私信发送的开局墙壁信息"; + + // 开局私信墙壁信息 + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + auto [info, md] = board.GetSurroundingWalls(pid); + board.players[pid].private_record = "【开局】\n您所在位置的四周墙壁信息,按照 上下左右 顺序分别是:\n" + info; + Global().Tell(pid) << board.players[pid].private_record << "\n" << Markdown(md, 110); + } + + string mode_str[4] = {"标准", "狂野", "幻变", "疯狂"}; + sender << "\n\n【区块模式】" + mode_str[GAME_OPTION(模式)]; + if (GAME_OPTION(模式) > 0) { + sender << ":本局可能出现的区块种类详见下图所示:\n"; + sender << Markdown(board.GetAllBlocksInfo(GAME_OPTION(特殊事件)), 1000); + } + + setter.Emplace(*this, ++round_); + } + + void NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) + { + bool game_end = (Alive_() <= 1 && Global().PlayerNum() > 1) || ((Alive_() <= 0 || board.ExitCount() == 0) && Global().PlayerNum() == 1); + if (!game_end && round_ < 20) { + setter.Emplace(*this, ++round_); + return; + } + + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + player_scores_[pid] = board.players[pid].score.FinalScore(); + } + + if (game_end) { + if (Global().PlayerNum() > 1) { + Global().Boardcast() << "玩家剩余 " + to_string(Alive_()) + " 人,游戏结束!"; + } else if (board.players[0].out == 2) { + Global().Boardcast() << "经过 " + to_string(round_) + " 回合,您成功抵达了逃生舱!"; + } + } else { + Global().Boardcast() << "回合数到达上限,游戏结束"; + } + Global().Boardcast() << "完整行动轨迹:\n" << Markdown(board.GetAllRecord(), 500); + Global().Boardcast() << "玩家分数细则:\n" << Markdown(board.GetAllScore(), 800); + Global().Boardcast() << Markdown(board.GetFinalBoard(), 120 * (GAME_OPTION(边长) + 1)); +/* *********************************************************** */ + // 积分奖励发放 + std::regex pattern(R"(机器人\d+号)"); + bool hasBots = std::ranges::any_of( + std::views::iota(0u, Global().PlayerNum()), + [&](unsigned int pid) { + return std::regex_match(Global().PlayerName(pid), pattern); + } + ); + if (hasBots) return; + if (Global().PlayerNum() == 1) { + if (board.players[0].out == 1) { + Global().Boardcast() << "很遗憾,您被淘汰了,未能获得积分奖励"; + return; + } + if (round_ > 10) { + Global().Boardcast() << "未能获得积分奖励,努力在10回合内抵达逃生舱吧!"; + return; + } + } + string pt_message = "新游戏积分已记录"; + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + auto name = Global().PlayerName(pid); + auto start = name.find_last_of('('), end = name.find(')', start); + std::string id = name.substr(start + 1, end - start - 1); + int32_t point = (Global().PlayerNum() > 1) ? ((player_scores_[pid] + min(round_ - 1, 7) * 40) * 2) : (round_ <= 10 ? (11 - round_) * 60 : 0); + point = GAME_OPTION(模式) == 0 ? point : (GAME_OPTION(模式) == 2 ? point * 1.2 : point * 1.3); + if (point > 0) pt_message += "\n" + id + " " + to_string(point); + } + pt_message += "\n「#pt help」查看游戏积分帮助"; + Global().Boardcast() << pt_message; +/* *********************************************************** */ + } +}; + +class RoundStage : public SubGameStage<> +{ + public: + RoundStage(MainStage& main_stage, const uint64_t round) + : StageFsm(main_stage, "第 " + std::to_string(round) + " 回合" , + MakeStageCommand(*this, "选择方向,在迷宫中探路", &RoundStage::MakeMove_, AlterChecker(direction_map)), + MakeStageCommand(*this, "主动停止行动,并结束回合", &RoundStage::Stop_, VoidChecker("停止")), + MakeStageCommand(*this, "使用“隐匿”技能(仅限隐匿模式)", &RoundStage::Hide_, VoidChecker("隐匿")), + MakeStageCommand(*this, "查看当前回合进展情况", &RoundStage::Status_, VoidChecker("赛况")), + MakeStageCommand(*this, "查看所有玩家完整行动轨迹", &RoundStage::AllStatus_, VoidChecker("完整赛况"))) + { + step = 0; + currentPlayer = 0; + if (Main().Alive_() == 0) { + Global().Boardcast() << "[错误] 发生了意料之外的错误:无可行动玩家但游戏仍未判定结束,请联系管理员或中断游戏!"; + return; + } + while (Main().board.players[currentPlayer].out > 0) { + currentPlayer = currentPlayer + 1; + } + hide = false; + active_stop = false; + } + + // 当前行动玩家 + PlayerID currentPlayer; + // 步数 + int step; + // 隐匿状态 + bool hide; + // 主动停止或超时 + bool active_stop; + + // 记录门的状态发生过改变 + bool door_modified = false; + + virtual void OnStageBegin() override + { + Global().SaveMarkdown(Main().board.GetBoard(Main().board.grid_map), 60 * (GAME_OPTION(边长) + 1)); + + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + Main().board.players[pid].move_record = ""; + if (pid != currentPlayer) { + Global().SetReady(pid); + } + } + Global().Boardcast() << Markdown(Main().board.GetPlayerTable(Main().round_)); + uint32_t think_time = Main().board.players[currentPlayer].hook_status ? 30 : GAME_OPTION(思考时限); + Global().Boardcast() << "请 " << At(currentPlayer) << " 在公屏选择方向移动(第一次行动前有 " << think_time << " 秒思考时间,行动开始后总时限为 " << GAME_OPTION(行动时限) << " 秒)"; + Global().StartTimer(think_time); + if (Main().round_ == 1) { + if (GAME_OPTION(BOSS)) SendSoundMessage(Main().board.boss.x, Main().board.boss.y, Sound::BOSS, true); + Global().Boardcast() << "可尝试使用「预览」指令生成自定义地图来记录草稿,格式例如:预览 2 3 0 11 E1 0 E2(其中E前缀表示逃生舱)" + << "\n\n!!!【重要提醒】!!! 请注意:当前版本主动停止或超时将无法得知四周墙壁信息!"; + } + } + + private: + AtomReqErrCode MakeMove_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, Direct direction) + { + Player& player = Main().board.players[pid]; + if (player.out == 2) { + reply() << "您已乘坐逃生舱撤离,无需继续行动"; + return StageErrCode::FAILED; + } + if (pid != currentPlayer) { + reply() << "[错误] 不是您的回合,当前正在行动玩家是:" << Global().PlayerName(currentPlayer); + return StageErrCode::FAILED; + } + if (!is_public && Global().PlayerNum() > 1) { + if (GAME_OPTION(隐匿) == 0) { + reply() << "[错误] 请全程在公屏进行行动"; + return StageErrCode::FAILED; + } + if (!hide) { + reply() << "[错误] 您未使用隐匿技能,请在公屏进行行动"; + return StageErrCode::FAILED; + } + } + + if (player.hook_status) player.hook_status = false; + + bool success = Main().board.MakeMove(pid, direction, hide); + step++; + + // 撞墙直接切换下一个玩家 + if (!success) { + if (step == 1) { + reply() << UnitMaps::RandomHint(UnitMaps::firststep_wall_hints) << "\n移动时碰撞【墙壁】,本回合结束!**请留意机器人私信发送的四周墙壁信息**"; + } else { + reply() << UnitMaps::RandomHint(UnitMaps::wall_hints) << "\n移动时碰撞【墙壁】,本回合结束!请留意机器人私信发送的四周墙壁信息"; + } + return StageErrCode::READY; + } + + auto sender = reply(); + + bool ready_status = HandleGridInteraction(player, sender); + if (ready_status) return StageErrCode::READY; + + // 继续行动 + if (step == 1) { + sender << "请继续行动(行动总时限为 " << GAME_OPTION(行动时限) << " 秒)"; + Global().StartTimer(GAME_OPTION(行动时限)); + } else { + int time = std::chrono::duration_cast(*Global().TimerFinishTime() - std::chrono::steady_clock::now()).count(); + sender << "请继续行动(剩余时间 " << time << " 秒)"; + } + // 单步隐匿:消除隐匿状态 + if (GAME_OPTION(隐匿) == 2 && hide) { hide = false; } + return StageErrCode::OK; + } + + // 处理区块效果(返回玩家回合是否结束) + bool HandleGridInteraction(Player& player, MsgSenderBase::MsgSenderGuard& sender) + { + if (player.subspace > 0) return false; // 亚空间内不影响地图 + + Grid& grid = Main().board.grid_map[player.x][player.y]; + // [逃生舱] + if (grid.Type() == GridType::EXIT) { + player.move_record += "(逃生)"; + int exited = Main().board.exit_num - Main().board.ExitCount(); + player.score.exit_score += Score::exit_order[exited]; // 逃生分 + grid.SetType(GridType::EMPTY); + + if (GAME_OPTION(大乱斗)) { + player.move_record += "【传送】"; + Main().board.TeleportPlayer(player.pid); // 大乱斗随机传送 + sender << UnitMaps::RandomHint(UnitMaps::exit_hints) << "\n\n您已抵达【逃生舱】!此逃生舱已失效," << At(player.pid) << " 被随机传送至地图其他地方!"; + } else { + player.out = 2; + sender << UnitMaps::RandomHint(UnitMaps::exit_hints) << "\n\n您已抵达【逃生舱】!不再参与后续游戏,此逃生舱已失效"; + if (Main().Alive_() > 1) { + Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 + sender << ",剩余玩家捕捉目标顺位发生变更!\n"; + sender << Markdown(Main().board.GetPlayerTable(Main().round_)); + } + } + // 隐匿状态公屏提示 + if (hide) { + Global().Boardcast() << At(currentPlayer) << " 已抵达【逃生舱】!" << (GAME_OPTION(大乱斗) ? "被随机传送至地图其他地方" : "不再参与后续游戏") << ",此逃生舱已失效\n" + << Markdown(Main().board.GetPlayerTable(Main().round_)); + } + return true; + } + // [传送门] + if (player.subspace == 0) { + // 离开亚空间,传送门传送 + Main().board.RemovePlayerFromMap(player.pid); + grid.PortalTeleport(player); + Main().board.player_map[player.x][player.y].push_back(player.pid); + } else if (grid.Type() == GridType::PORTAL) { + if (player.subspace == -1) { + // 进入亚空间 + player.subspace = 2; + // 出口是[单向传送门]则交换 + pair pRelPos = Main().board.grid_map[player.x][player.y].PortalPos(); + Grid& target_grid = Main().board.grid_map[player.x + pRelPos.first][player.y + pRelPos.second]; + if (target_grid.Type() == GridType::ONEWAYPORTAL) { + grid.SetType(GridType::ONEWAYPORTAL); + target_grid.SetType(GridType::PORTAL); + } + } + } + // [陷阱] + if (grid.Type() == GridType::TRAP) { + grid.TrapTrigger(); + if (grid.TrapStatus()) { + player.move_record += "(陷阱)"; + sender << UnitMaps::RandomHint(UnitMaps::trap_hints) << "\n\n移动触发【陷阱】,本回合被强制停止行动!"; + return true; + } + } + // [热源] + string step_info, heat_message; + if (Main().board.HeatNotice(player.pid)) { + step_info = "[热浪(第" + to_string(step) + "步)]"; + heat_message = UnitMaps::RandomHint(UnitMaps::heat_wave_hints) + "\n移动进入【热浪范围】,当前位置附近存在热源"; + } + if (grid.Type() == GridType::HEAT) { + if (player.heated) { + player.move_record += "(热源)"; + sender << UnitMaps::RandomHint(UnitMaps::heat_active_hints) << "\n\n您本局游戏已进入过【热源】,高温难耐,本回合无法继续前进!"; + return true; + } else { + player.heated = true; + step_info = "[热源(第" + to_string(step) + "步)]"; + heat_message = UnitMaps::RandomHint(UnitMaps::heat_core_hints) + "\n移动进入【热源】!请注意,在下一次进入热源时,将公开热源并强制停止行动"; + } + } + if (heat_message != "") { + Global().Tell(player.pid) << step_info << heat_message; + player.private_record += "\n" + step_info; + } + // [按钮] + if (grid.Type() == GridType::BUTTON) { + Grid::ButtonTarget target = grid.ButtonTargetPos(); + const int s = Main().board.size; + // 切换[门]状态 + if (target.dir.has_value()) { + int tx = player.x + target.dx; + int ty = player.y + target.dy; + const Direct dir = target.dir.value(); + Main().board.grid_map[tx][ty].switchDoor(dir); + int d = static_cast(dir); + int nx = (tx + k_DX_Direct[d] + s) % s; + int ny = (ty + k_DY_Direct[d] + s) % s; + Main().board.grid_map[nx][ny].switchDoor(opposite(dir)); + door_modified = true; + } + } + // 声响 Sound + Sound sound = Main().board.GetSound(grid, GAME_OPTION(特殊事件) == 3); + if (sound == Sound::SHASHA) { + if (hide) { + sender << "移动进入【树丛】(隐匿中,不会向其他人发出声响)\n\n"; + } else { + player.move_record += "[沙沙]"; + sender << UnitMaps::RandomHint(UnitMaps::grass_hints) << "\n移动进入【树丛】,请其他玩家留意私信声响信息!\n\n"; + SendSoundMessage(player.x, player.y, sound, false); + } + } else if (sound == Sound::PAPA) { + if (hide) { + sender << "移动发出【啪啪声】(隐匿中,不会向其他人发出声响)\n\n"; + } else { + player.move_record += "[啪啪]"; + sender << UnitMaps::RandomHint(UnitMaps::papa_hints) << "\n移动发出【啪啪声】,请其他玩家留意私信声响信息!\n\n"; + SendSoundMessage(player.x, player.y, sound, false); + } + } + // 非点杀模式检测玩家捕捉(隐匿状态不能捕捉) + if (!GAME_OPTION(点杀)) { + if (PlayerCatch(player, sender)) { + return true; + } + } + // 玩家可继续行动 + return false; + } + + AtomReqErrCode Stop_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + Player& player = Main().board.players[pid]; + if (player.out == 2) { + reply() << "您已乘坐逃生舱撤离,无需继续行动"; + return StageErrCode::FAILED; + } + if (pid != currentPlayer) { + reply() << "[错误] 不是您的回合,当前正在行动玩家是:" << Global().PlayerName(currentPlayer); + return StageErrCode::FAILED; + } + if (!is_public && Global().PlayerNum() > 1) { + if (GAME_OPTION(隐匿) == 0) { + reply() << "[错误] 请全程在公屏进行行动"; + } else { + reply() << "[错误] 您未使用隐匿技能,请在公屏进行行动"; + } + return StageErrCode::FAILED; + } + if (!hide) player.move_record += "(停止)"; + active_stop = true; + reply() << "您选择主动停止行动,本回合结束!主动停止无法获得四周墙壁信息"; + return StageErrCode::READY; + } + + AtomReqErrCode Hide_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + if (GAME_OPTION(隐匿) == 0) { + reply() << "[错误] 本局游戏未开启隐匿技能"; + return StageErrCode::FAILED; + } + Player& player = Main().board.players[pid]; + if (player.out == 2) { + reply() << "您已乘坐逃生舱撤离,无需继续行动"; + return StageErrCode::FAILED; + } + if (pid != currentPlayer) { + reply() << "[错误] 不是您的回合,当前正在行动玩家是:" << Global().PlayerName(currentPlayer); + return StageErrCode::FAILED; + } + if (hide) { + reply() << "[错误] 您已经处于隐匿状态,请在私信选择行动"; + return StageErrCode::FAILED; + } + if (player.hide_remaining == 0) { + reply() << "[错误] 隐匿技能次数已耗尽"; + return StageErrCode::FAILED; + } + if (!is_public && Global().PlayerNum() > 1) { + reply() << "[错误] 请在公屏使用隐匿技能"; + return StageErrCode::FAILED; + } + + hide = true; + player.hide_remaining--; + if (GAME_OPTION(隐匿) == 1) { + player.move_record += "【隐匿行动】"; + reply() << "使用隐匿技能,本回合剩余时间转为私聊行动,不会发出声响,不会触发捕捉。剩余次数:" << player.hide_remaining; + } else if (GAME_OPTION(隐匿) == 2) { + player.move_record += "�"; + reply() << "使用隐匿技能,下一步请在私聊行动,不会发出声响,不会触发捕捉。剩余次数:" << player.hide_remaining; + } + return StageErrCode::OK; + } + + AtomReqErrCode Status_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + auto sender = reply(); + if (is_public) { + if (GAME_OPTION(特殊事件) > 0) { + sender << UnitMaps::ShowSpecialEvent(GAME_OPTION(特殊事件)) << "\n"; + } + if (GAME_OPTION(边长) > 9) { + sender << "本局游戏地图为 " << GAME_OPTION(边长) << "x" << GAME_OPTION(边长) << "\n"; + } + sender << Markdown(Main().board.GetPlayerTable(Main().round_)); + for (PlayerID pid = 0; pid < currentPlayer.Get(); ++pid) { + if (Main().board.players[pid].out == 0) { + sender << "\n[" << pid.Get() << "号]本回合行动轨迹:\n" << Main().board.players[pid].move_record; + } + } + sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].move_record; + } else { + sender << Main().board.players[pid].private_record; + } + return StageErrCode::OK; + } + + AtomReqErrCode AllStatus_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + auto sender = reply(); + if (GAME_OPTION(特殊事件) > 0) { + sender << UnitMaps::ShowSpecialEvent(GAME_OPTION(特殊事件)) << "\n"; + } + if (GAME_OPTION(边长) > 9) { + sender << "本局游戏地图为 " << GAME_OPTION(边长) << "x" << GAME_OPTION(边长) << "\n"; + } + sender << Markdown(Main().board.GetAllRecord(), 500); + sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].move_record; + if (!is_public) { + sender << "\n\n" << Main().board.players[pid].private_record; + } + return StageErrCode::OK; + } + + bool PlayerCatch(Player& player, MsgSenderBase::MsgSenderGuard& sender) + { + vector list = Main().board.player_map[player.x][player.y]; + PlayerID t = player.target; + if (find(list.begin(), list.end(), t) != list.end() && !hide) { + active_stop = false; + sender << UnitMaps::RandomHint(UnitMaps::catch_hints) << "\n\n"; + sender << At(t) << " 被捕捉!"; + + if (Main().round_ == 1) { + player.move_record += "(首轮捕捉)【传送】"; + Main().board.players[t].all_record += (Main().board.players[t].all_record == "" ? "
" : "") + string("【首轮被抓传送】"); + Main().board.TeleportPlayer(t); // 随机传送被捉方 + Main().board.TeleportPlayer(player.pid); // 随机传送捕捉方 + sender << "\n【首轮玩家保护】\n首轮捕捉不生效:双方均被随机传送至地图其他地方!"; + return true; + } + + player.move_record += "(捕捉)"; + Main().board.players[t].out = 1; + Global().Eliminate(t); + player.score.catch_score += 100; // 抓人分 + Main().board.players[t].score.catch_score -= 100; + + if (Main().Alive_() > 1) { + player.move_record += "【传送】"; + Main().board.TeleportPlayer(player.pid); // 随机传送捕捉方 + Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 + sender << "\n" << At(player.pid) << " 被随机传送至地图其他地方,捕捉目标顺位发生变更!\n"; + sender << Markdown(Main().board.GetPlayerTable(Main().round_)); + } + + if (Main().Alive_() == 1) { + // 无逃生舱最后生还判定 + if (Main().board.ExitCount() == 0) Main().withoutE_win_ = true; + // 有逃生舱但死斗取胜判定 + if (Main().board.ExitCount() > 0) Main().withE_win_ = true; + } + return true; + } + return false; + } + + // 私信其他玩家发送声响信息 + void SendSoundMessage(const int fromX, const int fromY, const Sound sound, const bool to_all) + { + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if ((pid != currentPlayer || to_all) && Main().board.players[pid].out == 0) { + string direction = Main().board.GetSoundDirection(fromX, fromY, Main().board.players[pid]); + string step_info = "[" + to_string(currentPlayer) + "号(第" + to_string(step) + "步)]"; + string sound_message; + if (direction == "") { + if (Main().board.players[currentPlayer].target != pid || GAME_OPTION(点杀)) { + switch (sound) { + case Sound::SHASHA: sound_message = step_info + "\n" + UnitMaps::RandomHint(UnitMaps::grass_sound_hints); break; + case Sound::PAPA: sound_message = step_info + "\n" + UnitMaps::RandomHint(UnitMaps::papa_sound_hints); break; + default: sound_message = "[错误] 未知声音类型:相同格子的未知声音"; + } + } + } else { + switch (sound) { + case Sound::SHASHA: sound_message = step_info + "你听见了来自【" + direction + "方】的沙沙声!"; break; + case Sound::PAPA: sound_message = step_info + "你听见了来自【" + direction + "方】的啪啪声!"; break; + case Sound::BOSS: sound_message = "[BOSS] 你听见了来自【" + direction + "方】的巨大响声!"; break; + default: sound_message = "[错误] 未知声音类型:不同格子来自【" + direction + "方】的未知声音"; + } + } + Main().board.players[pid].private_record += "\n" + sound_message; + Global().Tell(pid) << sound_message; + } + } + } + + virtual CheckoutErrCode OnStageTimeout() override + { + Main().board.players[currentPlayer].move_record += "(超时)"; + active_stop = true; + if (step == 0) { + Main().board.players[currentPlayer].hook_status = true; + Global().Boardcast() << "玩家 " << At(PlayerID(currentPlayer)) << " 超时未行动,已进入挂机状态,再次行动前仅有30秒等待时间"; + } else { + Global().Boardcast() << "玩家 " << At(PlayerID(currentPlayer)) << " 行动超时,切换下一个玩家"; + } + Global().SetReady(currentPlayer); + return HandleStageOver(); + } + + virtual CheckoutErrCode OnPlayerLeave(const PlayerID pid) override + { + if (Main().board.players[pid].out > 0) { + return StageErrCode::CONTINUE; + } + Main().board.players[pid].out = 1; + Main().board.players[pid].score.quit_score -= 300; // 退出分 + + auto sender = Global().Boardcast(); + sender << "玩家 " << At(pid) << " 退出游戏"; + if (Main().Alive_() > 1) { + Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 + sender << ",捕捉目标顺位发生变更!\n"; + sender << Markdown(Main().board.GetPlayerTable(Main().round_)); + return StageErrCode::CONTINUE; + } + return StageErrCode::CHECKOUT; + } + + virtual CheckoutErrCode OnStageOver() override + { + return HandleStageOver(); + } + + CheckoutErrCode HandleStageOver() + { + Player& player = Main().board.players[currentPlayer]; + // 点杀模式:回合结束才触发捉捕 + if (GAME_OPTION(点杀)) { + vector list = Main().board.player_map[player.x][player.y]; + if (find(list.begin(), list.end(), player.target) != list.end() && !hide) { + auto sender = Global().Boardcast(); + PlayerCatch(player, sender); + } + } + Global().Boardcast() << "[" << currentPlayer.Get() << "号]玩家本回合的完整行动轨迹:\n" << player.move_record; + // 记录历史行动轨迹 + player.all_record += "
【第 " + to_string(Main().round_) + " 回合】
" + player.move_record; + // 仅剩1玩家,游戏结束 + if ((Main().Alive_() == 1 && Global().PlayerNum() > 1) || (Main().Alive_() == 0 && Global().PlayerNum() == 1)) { + if (Main().withoutE_win_) { // 无逃生舱最后生还胜利 + Global().Boardcast() << At(currentPlayer) << "\n" << UnitMaps::RandomHint(UnitMaps::withoutE_win_hints); + } + if (Main().withE_win_) { // 有逃生舱但死斗取胜 + Global().Boardcast() << At(currentPlayer) << "\n" << UnitMaps::RandomHint(UnitMaps::withE_win_hints); + } + return StageErrCode::CHECKOUT; + } + // 私信发送四周墙壁信息(主动停止或超时不发送) + if (player.out == 0 && !active_stop) { + auto [info, md] = Main().board.GetSurroundingWalls(currentPlayer); + player.private_record = "【第 " + to_string(Main().round_) + " 回合】\n您所在位置的四周墙壁信息,按照 上下左右 顺序分别是:\n" + info; + Global().Tell(currentPlayer) << player.private_record << "\n" << Markdown(md, 110); + } + step = 0; + hide = false; + active_stop = false; + // 下一个玩家行动 + do { + currentPlayer = (currentPlayer + 1) % Global().PlayerNum(); + if (currentPlayer == 0) break; + } while (Main().board.players[currentPlayer].out > 0); + if (currentPlayer != 0) { + uint32_t think_time = Main().board.players[currentPlayer].hook_status ? 30 : GAME_OPTION(思考时限); + Global().Boardcast() << "请 " << At(currentPlayer) << " 在公屏选择方向移动(第一次行动前有 " << think_time << " 秒思考时间,行动开始后总时限为 " << GAME_OPTION(行动时限) << " 秒)"; + Global().ClearReady(currentPlayer); + Global().StartTimer(think_time); + return StageErrCode::CONTINUE; + } + + // [回合结束] 所有玩家都行动后结束本回合 + // BOSS相关结算 + if (GAME_OPTION(BOSS)) { + string boss_record = "
【第 " + to_string(Main().round_) + " 回合】"; + auto& boss = Main().board.boss; + auto sender = Global().Boardcast(); + sender << "【回合结束[BOSS行动]】"; + + if (Main().board.BossChangeTarget(false)) { + // 未更换目标,执行移动 + bool is_catch = Main().board.BossMove(); + // 抓住玩家 + if (is_catch) { + for (auto pid: Main().board.player_map[boss.x][boss.y]) { + if (Main().board.players[pid].out > 0) continue; + Main().board.players[pid].all_record += "【BOSS捕捉】"; + Main().board.players[pid].out = 1; + if (Global().PlayerNum() > 1) Global().Eliminate(pid); + Main().board.players[pid].score.catch_score -= 100; // 抓人分 + boss_record += "[" + to_string(pid) + "号] "; + sender << "\n" << At(pid); + } + boss_record += "被BOSS捕捉出局!"; + sender << "\n被BOSS捕捉出局!"; + if (Main().Alive_() > 1) { + Main().board.BossChangeTarget(true); // 重置锁定目标 + Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 + boss_record += "变更目标至 [" + to_string(boss.target) + "号]"; + sender << "\n\nBOSS更换锁定目标至 " << At(boss.target) << ",同时玩家捕捉目标顺位发生变更!\n"; + sender << Markdown(Main().board.GetPlayerTable(Main().round_)); + } + } else { + boss_record += "向 [" + to_string(boss.target) + "号] 移动了 " + to_string(boss.steps) + " 步"; + sender << "【回合结束】\nBOSS向 " << At(boss.target) << " 移动了 " << boss.steps << " 步"; + } + // BOSS移动后发出巨响 + if (Main().Alive_() > 1 || (Global().PlayerNum() == 1 && Main().board.players[0].out == 0)) { + boss_record += "(巨响)"; + sender << "\n\nBOSS发出震耳欲聋的巨响!请所有玩家留意私信声响信息!"; + SendSoundMessage(boss.x, boss.y, Sound::BOSS, true); + } + } else { + // 更换目标,重置步数 + boss_record += "发现更近的目标,变更目标至 [" + to_string(boss.target) + "号]"; + sender << "\nBOSS发现了距离更近的玩家,变更锁定目标至 " << At(boss.target); + } + + // BOSS周围8格内获得喘息提示 + for (auto& player : Main().board.players) { + if (Main().board.IsBossNearby(player) && player.out == 0) { + player.private_record += "\n[BOSS] 你听到来自BOSS沉重的喘息声!"; + Global().Tell(player.pid) << "「呼……呼……」你听到来自BOSS沉重的喘息声!"; + } + } + boss.all_record += boss_record; + } + // 门变更过进行提示 + if (door_modified) { + Global().Boardcast() << "【注意】在本回合内,有门的状态发生过变化,但存在恢复原状的可能"; + door_modified = false; + } + + return StageErrCode::CHECKOUT; + } + + virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) override + { + if (Global().IsReady(pid)) { + return StageErrCode::OK; + } + Main().board.players[pid].out = 1; + Main().board.players[pid].score.quit_score -= 300; // 退出分 + + auto sender = Global().Boardcast(); + sender << "笨笨的机器人退出了游戏"; + if (Main().Alive_() > 1) { + Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 + sender << ",捕捉目标顺位发生变更!\n"; + sender << Markdown(Main().board.GetPlayerTable(Main().round_)); + } + return StageErrCode::READY; + } +}; + +auto* MakeMainStage(MainStageFactory factory) { return factory.Create(); } + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + diff --git a/games/long_night/option.cmake b/games/long_night/option.cmake new file mode 100644 index 00000000..e69de29b diff --git a/games/long_night/options.h b/games/long_night/options.h new file mode 100644 index 00000000..ffbd8901 --- /dev/null +++ b/games/long_night/options.h @@ -0,0 +1,15 @@ +EXTEND_OPTION("每局游戏先根据模式从区块池抽取12+4组成随机池,再抽取地图区块
" + "【标准】仅从**经典区块**中抽取区块   【幻变】将从**轮换区块**中随机抽取
" + "【狂野】将从**所有区块**中随机抽取   【疯狂】随机池包含2个**特殊区块**", 模式, + (AlterChecker({{"标准", 0}, {"狂野", 1}, {"幻变", 2}, {"疯狂", 3}})), 2) +EXTEND_OPTION("捉捕目标:设置游戏中玩家的捉捕顺序", 捉捕目标, (BoolChecker("下家", "上家")), true) +EXTEND_OPTION("设置游戏地图边长", 边长, (AlterChecker({{"9", 9}, {"10", 10}, {"12", 12}})), 9) +EXTEND_OPTION("点杀模式:捕捉仅在回合结束时触发,路过不会触发捕捉", 点杀, (BoolChecker("开启", "关闭")), false) +EXTEND_OPTION("隐匿模式:隐匿后私聊行动,可选回合和单步模式", 隐匿, (AlterChecker({{"关闭", 0}, {"回合", 1}, {"单步", 2}})), 0) +EXTEND_OPTION("大乱斗模式:逃生舱改为随机传送", 大乱斗, (BoolChecker("开启", "关闭")), false) +EXTEND_OPTION("在游戏中生成BOSS牛头怪米诺陶斯,在回合结束时追击玩家", BOSS, (BoolChecker("开启", "关闭")), false) +EXTEND_OPTION("设置游戏特殊事件", 特殊事件, (AlterChecker({ + {"随机", -1}, {"无", 0}, {"怠惰的园丁", 1}, {"营养过剩", 2}, {"雨天小故事", 3} +})), 0) +EXTEND_OPTION("行动前思考的时间限制", 思考时限, (ArithChecker(30, 3600, "超时时间(秒)")), 180) +EXTEND_OPTION("开始行动后的总时间限制", 行动时限, (ArithChecker(60, 3600, "超时时间(秒)")), 360) diff --git a/games/long_night/resource/box.png b/games/long_night/resource/box.png new file mode 100644 index 00000000..a6b77feb Binary files /dev/null and b/games/long_night/resource/box.png differ diff --git a/games/long_night/resource/button.png b/games/long_night/resource/button.png new file mode 100644 index 00000000..ae01153b Binary files /dev/null and b/games/long_night/resource/button.png differ diff --git a/games/long_night/resource/empty.png b/games/long_night/resource/empty.png new file mode 100644 index 00000000..a9625a99 Binary files /dev/null and b/games/long_night/resource/empty.png differ diff --git a/games/long_night/resource/exit.png b/games/long_night/resource/exit.png new file mode 100644 index 00000000..9241c0fd Binary files /dev/null and b/games/long_night/resource/exit.png differ diff --git a/games/long_night/resource/grass.png b/games/long_night/resource/grass.png new file mode 100644 index 00000000..c8e9a026 Binary files /dev/null and b/games/long_night/resource/grass.png differ diff --git a/games/long_night/resource/heat.png b/games/long_night/resource/heat.png new file mode 100644 index 00000000..cf7ff591 Binary files /dev/null and b/games/long_night/resource/heat.png differ diff --git a/games/long_night/resource/oneway_portal.png b/games/long_night/resource/oneway_portal.png new file mode 100644 index 00000000..ea0cec0a Binary files /dev/null and b/games/long_night/resource/oneway_portal.png differ diff --git a/games/long_night/resource/portal.png b/games/long_night/resource/portal.png new file mode 100644 index 00000000..b84856fb Binary files /dev/null and b/games/long_night/resource/portal.png differ diff --git a/games/long_night/resource/trap.png b/games/long_night/resource/trap.png new file mode 100644 index 00000000..dfa297d7 Binary files /dev/null and b/games/long_night/resource/trap.png differ diff --git a/games/long_night/resource/unknown.png b/games/long_night/resource/unknown.png new file mode 100644 index 00000000..025fe623 Binary files /dev/null and b/games/long_night/resource/unknown.png differ diff --git a/games/long_night/resource/water.png b/games/long_night/resource/water.png new file mode 100644 index 00000000..f7f65a77 Binary files /dev/null and b/games/long_night/resource/water.png differ diff --git a/games/long_night/rule.md b/games/long_night/rule.md new file mode 100644 index 00000000..dac521ec --- /dev/null +++ b/games/long_night/rule.md @@ -0,0 +1,50 @@ +## 漫漫长夜 + +- **游戏人数:** 2-6 +- **原作:** 大萝卜姬 +- **详细图片规则可查看群文件:《漫漫长夜》.pdf** + +### 游戏简介 +- 本游戏适合 4/6 名玩家游玩。你和其他玩家被传送到一座伸手不见五指的迷宫里,你将同时扮演逃生者和狩猎者的身份,在漆黑的迷宫里通过有限的信息到达逃生舱或者捕捉其他玩家,以此获得胜利! + +### 一、地图与行动 +- **1.地图:**
+ ①全局地图**不可视**,所有行动**公屏**进行。
+ ②开局地图由16个区块中随机不重复的9个按照随机顺序排列组成3x3的大地图。*而且,逃生舱区块数量恒定=玩家总人数的1半,大地图的边缘会连通至地图的另一端。*
+ ③开局所有玩家将随机进入地图某一位置,且所有玩家均不知所有玩家的方位。而且,每个区块仅出生1位玩家;玩家出生点不会在逃生舱区块;捕猎顺序的上下家和所有玩家与逃生舱的距离会尽可能大于等于5格。 +- **2.行动与目标:**
+ ①开局随机玩家行动顺序,每位玩家按顺序轮流行动。
+ ②所有玩家的游戏目标是:**按照行动顺序,捕捉自己下一位玩家,即捕猎顺序和行动顺序一致。**
+ ③玩家的每次行动均可移动无限步数 **(移动1个正方形格子为1步)**。若移动中碰到墙壁则立即停止行动,玩家将**秘密得知自己所在位置的四周墙壁信息**,然后轮到下一位玩家行动。可任意时刻自行停止移动,**但主动停止无法获得私信墙壁信息**。
+ ④玩家在移动过程中经过下家玩家则视为捕捉成功。玩家在移动过程中经过上家玩家不会被捕捉。
+ ⑤若有玩家捕捉成功或从出口逃离,游戏目标顺位更改。**捕捉成功会使行动停止,并且随机传送到地图任一位置。** 剩余玩家的捕猎目标按原顺序顺位更改。游戏将进行到仅剩 1 个玩家。 + +### 二、其他设定 +- **1.声响:**
+ ①若玩家移动过程中,经过区块中会**发出声响**的特殊地形(详见游戏图例),则裁判**给所有玩家私聊声源方向**。
+ ②关于除了 正上/正下/正左/正右 外的夹角方向,例如右上,指正上和正右的夹角范围。
+ ③因为地图边界联通,裁判私聊声源方向时会优先选择就近的方向。10x10地图时,若声源距离相等,裁判将选择任意1个告知。 +- **2.传送门与亚空间:**
+ 踩到紫色传送门后,玩家会发出啪啪声,然后视为进入亚空间。出生在传送门时不会进入亚空间,不会发出啪啪声。亚空间内没有墙壁,也没有方向的概念。亚空间内玩家任意移动2步即可到达相同区块的另一个传送门。例如:“→啪(踩到传送门,发出啪啪声,进入亚空间)←→啪(任意2次移动后,离开亚空间,踩到传送门,发出啪啪声)” +- **3.亚空间停留**
+ 玩家刚进入亚空间或在亚空间内移动一步,均视作在亚空间内。停留在亚空间内,获得的四周墙壁信息为“空空空空”,**玩家本身依然在传送门入口处,捉捕需要在传送门入口处才能触发** + +### 三、特殊事件 +- ①怠惰的园丁:树丛将在其区块内随机位置生成(有可能生成在中间) +- ②营养过剩:树丛和陷阱将额外向随机1个方向再次生成1个树丛(共8个方向,且不可隔墙生长) +- ③雨天小故事:地图中所有树丛变成水洼,陷阱会发出啪啪声 + +### 四、特殊模式 +- ①区块模式:**如果传送门被四周封闭,同区块对应的传送门将会变为水洼** + + 【标准】仅使用游戏最初的**经典区块**组成随机池 + + 【幻变】将从**轮换区块**中抽取12+4组成随机池,根据更新发生变化 + + 【狂野】将从**所有区块**中抽取12+4组成随机池 + + 【疯狂】从**所有区块**中抽取10+4,同时包含2个**特殊区块** +- ②点杀:捕捉改为仅在回合结束时触发,路过不会触发捕捉 +- ③反侦察手段:玩家获得技能“隐匿”,可选【回合】和【单步】模式 + 1. 回合隐匿:持续1回合,**仅可使用1次**,当回合的所有行动转为私聊进行,不会发出声响,不会触发捕捉。 + 2. 单步隐匿:仅作用于下一步,**可使用4次**,隐匿后在私聊行动下一步,不会发出声响,不会触发捕捉。 +- ④10x10!:本局游戏的大地图将更改为10x10大地图,9个区块随机排列塞满,区块不会重叠,没有区块的空隙将变成普通道路。 +- ⑤大乱斗:所有的逃生舱改为随机传送!但仍会统计逃生分 +- ⑥BOSS:米诺陶斯随机生成在地图中,每回合最后行动,首回合锁定最近玩家为目标(公屏显示),每回合移动步数递增,发现更近玩家则更换目标并重置步数。无视地形,移动结束时会发出巨响,如果玩家在其周围会听到喘息声。BOSS踩到玩家则玩家出局,玩家经过米诺陶斯不会出局。 + diff --git a/games/long_night/unittest.cc b/games/long_night/unittest.cc new file mode 100644 index 00000000..9d65fa15 --- /dev/null +++ b/games/long_night/unittest.cc @@ -0,0 +1,42 @@ +// Copyright (c) 2018-present, Chang Liu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include "game_framework/unittest_base.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +GAME_TEST(2, leave_test1) +{ + START_GAME(); + + ASSERT_LEAVE(CHECKOUT, 0); + + ASSERT_SCORE(-300, 0); +} + +GAME_TEST(2, leave_test2) +{ + START_GAME(); + + ASSERT_LEAVE(CHECKOUT, 1); + + ASSERT_SCORE(0, -300); +} + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + gflags::ParseCommandLineFlags(&argc, &argv, true); + return RUN_ALL_TESTS(); +} diff --git a/games/lucky_poker/mygame.cc b/games/lucky_poker/mygame.cc index 8c258330..c59694aa 100644 --- a/games/lucky_poker/mygame.cc +++ b/games/lucky_poker/mygame.cc @@ -31,15 +31,15 @@ const GameProperties k_properties { .developer_ = "森高", .description_ = "能够看到各个玩家部分手牌,两阶段下注的比拼大小的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 4; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) +uint64_t MaxPlayerNum(const CustomOptions& options) { return 4; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return GET_OPTION_VALUE(options, 种子).empty() ? GET_OPTION_VALUE(options, 轮数) / 3 : 0; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 2) { reply() << "该游戏至少 2 人参加"; @@ -62,7 +62,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 3; return NewGameMode::SINGLE_USER; diff --git a/games/mahjong_17_steps/mygame.cc b/games/mahjong_17_steps/mygame.cc index 6608d86d..52ada17b 100644 --- a/games/mahjong_17_steps/mygame.cc +++ b/games/mahjong_17_steps/mygame.cc @@ -29,15 +29,15 @@ const GameProperties k_properties { .developer_ = "森高", .description_ = "组成满贯听牌牌型,并经历 17 轮切牌的麻将游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 4; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) +uint64_t MaxPlayerNum(const CustomOptions& options) { return 4; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return GET_OPTION_VALUE(options, 种子).empty() ? 3 : 0; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() == 1) { reply() << "该游戏至少 2 名玩家参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -53,7 +53,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [](MyGameOptions& game_options, MutableGenericOptions& generic_options) + [](CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 3; return NewGameMode::SINGLE_USER; diff --git a/games/move_chess/mygame.cc b/games/move_chess/mygame.cc index b6d824c1..e8eb2f53 100644 --- a/games/move_chess/mygame.cc +++ b/games/move_chess/mygame.cc @@ -29,14 +29,14 @@ const GameProperties k_properties { .developer_ = "睦月", .description_ = "通过移动棋子形成四连的棋类游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; }// 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 2; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; }// 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 2; } const MutableGenericOptions k_default_generic_options{ .is_formal_{false}, }; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏必须 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -47,7 +47,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; diff --git a/games/naval_battle/board.h b/games/naval_battle/board.h index 664bab98..d21860a8 100644 --- a/games/naval_battle/board.h +++ b/games/naval_battle/board.h @@ -1,16 +1,16 @@ const map position_map = { - {"上", 1}, {"U", 1}, {"s", 1}, - {"下", 2}, {"D", 2}, {"x", 2}, - {"左", 3}, {"L", 3}, {"z", 3}, - {"右", 4}, {"R", 4}, {"y", 4}, + {"上", 1}, {"U", 1}, {"s", 1}, + {"下", 2}, {"D", 2}, {"x", 2}, + {"左", 3}, {"L", 3}, {"z", 3}, + {"右", 4}, {"R", 4}, {"y", 4}, }; class Board { public: - // 底色(状态 类型) + // 底色(状态 类型) const string color[3][4] = { {"ECECEC","E0FFE0","E0FFE0", "4363D8"}, // 00 01 02 03 {"B0E0FF","FFA0A0","000000", "5A5A5A"}, // 10 11 12 13 @@ -21,75 +21,142 @@ class Board {" ","+","", ""}, // 00 01 02 03 {"","","", ""}, // 10 11 12 13 }; - // 机身相对飞机头坐标位置偏差(方向 机身 XY) - const int position[5][9][2] = {{}, // 1上 2下 3左 4右 - {{-2,1}, {-1,1}, {0,1}, {1,1}, {2,1}, {0,2}, {-1,3}, {0,3}, {1,3}}, - {{-2,-1}, {-1,-1}, {0,-1}, {1,-1}, {2,-1}, {0,-2}, {-1,-3}, {0,-3}, {1,-3}}, - {{1,2}, {1,1}, {1,0}, {1,-1}, {1,-2}, {2,0}, {3,1}, {3,0}, {3,-1}}, - {{-1,2}, {-1,1}, {-1,0}, {-1,-1}, {-1,-2}, {-2,0}, {-3,1}, {-3,0}, {-3,-1}}, - }; + // 机身相对飞机头坐标位置偏差(方向 机身 XY) + vector>> positions = {{}, // 1上 2下 3左 4右 + {{-2,1}, {-1,1}, {0,1}, {1,1}, {2,1}, {0,2}, {-1,3}, {0,3}, {1,3}}, + {{-2,-1}, {-1,-1}, {0,-1}, {1,-1}, {2,-1}, {0,-2}, {-1,-3}, {0,-3}, {1,-3}}, + {{1,2}, {1,1}, {1,0}, {1,-1}, {1,-2}, {2,0}, {3,1}, {3,0}, {3,-1}}, + {{-1,2}, {-1,1}, {-1,0}, {-1,-1}, {-1,-2}, {-2,0}, {-3,1}, {-3,0}, {-3,-1}}, + }; + // 多要害模式其他飞机头相对主飞机头位置偏差 + vector>> positions2 = {{}, {}, {}, {}, {}}; + int crucial_num = 1; + pair main_crucial{-1, -1}; + // 飞机边界相对飞机头的最大偏差 + int boundary[5] = {-1, 0, 3, 2, 2}; // 地图大小 int sizeX, sizeY; /* - *** 地图区域 *** - - 状态 - | - 类型 - - 0 - 未被打击 | 0 - 空地 - 1 - 已被打击 | 1 - 机身 - 2 - 侦察点 | 2 - 飞机头 3 - 特殊要害 - */ - int map[20][20][2]; - - /* - 保存飞机的参数(移除功能用) - 如果为飞机头,则保存方向数字 - 如果为机身,则保存叠加层数(重叠功能用) - */ - int body[20][20]; - /* - 保存标记数据 - 特殊机身标记(+) - 300 - 飞机头标记(☆) - 200 - 机身标记(—) - 0-199(保存机身标记叠加层数) - */ - int mark[20][20]; + *** 地图区域 *** + - 状态 - | - 类型 - + 0 - 未被打击 | 0 - 空地 + 1 - 已被打击 | 1 - 机身 + 2 - 侦察点 | 2 - 飞机头 3 - 特殊要害 + */ + int map[20][20][2]; + + /* + 保存飞机的参数(移除功能用) + 如果为飞机头,则保存方向数字 + 如果为机身,则保存叠加层数(重叠功能用) + */ + int body[20][20]; + /* + 保存标记数据 + 特殊机身标记(+) - 300 + 飞机头标记(☆) - 200 + 机身标记(—) - 0-199(保存机身标记叠加层数) + */ + int mark[20][20]; - /* - 是否为本回合行动 - 0 - 否 1 - 是 - */ + /* + 是否为本回合行动 + 0 - 否 1 - 是 + */ int this_turn[20][20]; // 地图markdown格式 string grid[20][20]; - // 玩家昵称 - string MapName; + // 玩家昵称 + string MapName; // 飞机数量 int planeNum; - // 剩余要害数 - int alive; - - // 是否为准备回合 - int prepare; - // 保存首要害坐标 - int firstX, firstY; - - // 初始化地图 - void InitializeMap() - { - for(int j = 0; j <= sizeY; j++) { + // 剩余要害数 + int alive; + + // 是否为准备回合 + int prepare; + // 保存首要害坐标 + int firstX, firstY; + + // 计算自定义形状相关参数 + void CustomizeShape(const vector shape) { + // 清空positions,第0项保留为空 + for (int i = 1; i <= 4; i++) { + positions[i].clear(); + } + // 清空boundary + for (int i = 1; i <= 4; i++) { + boundary[i] = 0; + } + // 形状为空,返回空结果 + if (shape.size() == 0) { + return; + } + pair mainPos{-1, -1}; + int rows = shape.size(); + int cols = shape[0].size(); + // 查找靠近中间上方的 2 作为主飞机头 + for (int r = 0; r < rows; ++r) { + if (shape[r][2] == '2') { mainPos = {r, 2}; break; } + if (shape[r][1] == '2') { mainPos = {r, 1}; break; } + if (shape[r][3] == '2') { mainPos = {r, 3}; break; } + if (shape[r][0] == '2') { mainPos = {r, 0}; break; } + if (shape[r][4] == '2') { mainPos = {r, 4}; break; } + } + // 如果没有找到 2,返回空结果 + if (mainPos.first == -1) { + return; + } + // 计算相对坐标差 + crucial_num = 0; + for (int r = 0; r < rows; ++r) { + for (int c = 0; c < cols; ++c) { + int dy = r - mainPos.first; + int dx = c - mainPos.second; + if (shape[r][c] == '1') { + positions[1].push_back({dx, dy}); // 上1 + positions[2].push_back({-dx, -dy}); // 下2 + positions[3].push_back({dy, -dx}); // 左3 + positions[4].push_back({-dy, dx}); // 右4 + if (-dy > boundary[1]) boundary[1] = -dy; // 上1 + if (dy > boundary[2]) boundary[2] = dy; // 下2 + if (-dx > boundary[3]) boundary[3] = -dx; // 左3 + if (dx > boundary[4]) boundary[4] = dx; // 右4 + } + if (shape[r][c] == '2' && (dx != 0 || dy != 0)) { + positions2[1].push_back({dx, dy}); // 上1 + positions2[2].push_back({-dx, -dy}); // 下2 + positions2[3].push_back({dy, -dx}); // 左3 + positions2[4].push_back({-dy, dx}); // 右4 + if (-dy > boundary[1]) boundary[1] = -dy; // 上1 + if (dy > boundary[2]) boundary[2] = dy; // 下2 + if (-dx > boundary[3]) boundary[3] = -dx; // 左3 + if (dx > boundary[4]) boundary[4] = dx; // 右4 + } + } + crucial_num += std::count(shape[r].begin(), shape[r].end(), '2'); + } + if (crucial_num > 1) main_crucial = mainPos; + } + + // 初始化地图 + void InitializeMap(const bool corner) + { + for(int j = 0; j <= sizeY; j++) { for(int i = 0; i <= sizeX; i++) { map[i][j][0] = map[i][j][1] = body[i][j] = mark[i][j] = this_turn[i][j] = 0; - if ((i == 1) + (j == 1) + (i == sizeX) + (j == sizeY) == 2) { - map[i][j][0] = 1; - } - } - } - } + if ((i == 1) + (j == 1) + (i == sizeX) + (j == sizeY) == 2 && corner) { + map[i][j][0] = 1; + } + } + } + } // 图形界面 - string Getmap(int show_planes, int crucial_mode) + string Getmap(const int show_planes, const int crucial_mode) { // 初始化 for(int i = 0; i <= sizeX + 2; i++) @@ -144,20 +211,24 @@ class Board { for(int i = 1; i <= sizeX; i++) { - grid[i][j] += " 0) { - if (map[i][j][1] == 2 && !show_planes && (crucial_mode == 1 || (!(firstX == i && firstY == j) && crucial_mode == 2))) { - grid[i][j] += color[map[i][j][0]][1]; - } else { - grid[i][j] += color[map[i][j][0]][map[i][j][1]]; - } + if (map[i][j][1] == 2 && !show_planes && (crucial_mode == 1 || (!(firstX == i && firstY == j) && crucial_mode == 2))) { + grid[i][j] += color[map[i][j][0]][1]; + } else { + grid[i][j] += color[map[i][j][0]][map[i][j][1]]; + } } else { grid[i][j] += color[0][0]; } - grid[i][j] += "\">"; + if (map[i][j][1] == 2 && body[i][j] > 0 && positions2[1].size() > 0 && show_planes) { + grid[i][j] += "; border: 1px solid red"; + } + + grid[i][j] += ";\">"; // 地图符号 string m; @@ -168,22 +239,22 @@ class Board !(((crucial_mode == 1 || (!(firstX == i && firstY == j) && crucial_mode == 2))) && mark[i][j] == 200 && map[i][j][0] != 2 && !(map[i][j][0] == 1 && map[i][j][1] >= 3))) { - if (map[i][j][1] == 2 && !show_planes && (crucial_mode == 1 || (!(firstX == i && firstY == j) && crucial_mode == 2))) { - m = icon[this_turn[i][j]][1]; - } else { - m = icon[this_turn[i][j]][map[i][j][1]]; - } + if (map[i][j][1] == 2 && !show_planes && (crucial_mode == 1 || (!(firstX == i && firstY == j) && crucial_mode == 2))) { + m = icon[this_turn[i][j]][1]; + } else { + m = icon[this_turn[i][j]][map[i][j][1]]; + } } else { - // 空地标记 - if (mark[i][j] == 200) { - m = ""; - } else if (mark[i][j] == 300) { - m = "+"; - } else if (mark[i][j] > 0) { - m = ""; - } else { - m = fill; - } + // 空地标记 + if (mark[i][j] == 200) { + m = ""; + } else if (mark[i][j] == 300) { + m = "+"; + } else if (mark[i][j] > 0) { + m = ""; + } else { + m = fill; + } } } grid[i][j] += m; @@ -197,8 +268,8 @@ class Board // 玩家昵称 mapString += ""; - mapString += ""; - mapString += ""; + mapString += ""; + mapString += ""; mapString += "
" + fill + "
" + MapName + "
" + fill + "
" + MapName + "
"; mapString += ""; @@ -222,25 +293,25 @@ class Board // 要害数量显示 mapString += "
"; - if (prepare) { - mapString += ""; - } else { - if (crucial_mode == 0) { - mapString += ""; - } else { - mapString += ""; - } - } - mapString += ""; + if (prepare) { + mapString += ""; + } else { + if (crucial_mode == 0) { + mapString += ""; + } else { + mapString += ""; + } + } + mapString += ""; mapString += "
剩余飞机数:" + to_string(planeNum - alive) + "
命中要害:" + to_string(planeNum - alive) + "
命中要害:???
" + fill + "
剩余飞机数:" + to_string(planeNum - alive / crucial_num) + "
命中要害:" + to_string(planeNum * crucial_num - alive) + "
命中要害:???
" + fill + "
"; return mapString; } - // 检查坐标是否合法 - static string CheckCoordinate(string &s) + // 检查坐标是否合法 + static string CheckCoordinate(string &s) { - // 长度必须为2或3 + // 长度必须为2或3 if (s.length() != 2 && s.length() != 3) { return "[错误] 输入的坐标长度只能为 2 或 3,如:A1"; @@ -260,7 +331,7 @@ class Board return "[错误] 请输入合法的坐标(字母+数字),如:A1"; } return "OK"; - } + } // 将字符串转为一个位置pair。必须确保字符串是合法的再执行这个操作。 static pair TranString(string s) @@ -279,18 +350,18 @@ class Board // 检查地图边界 bool CheckMapBoundary(const int X, const int Y, const int direction) const { - if (direction == 1 && (X < 3 || X > sizeX-2 || Y < 1 || Y > sizeY-3)) return false; - if (direction == 2 && (X < 3 || X > sizeX-2 || Y < 4 || Y > sizeY )) return false; - if (direction == 3 && (X < 1 || X > sizeX-3 || Y < 3 || Y > sizeY-2)) return false; - if (direction == 4 && (X < 4 || X > sizeX || Y < 3 || Y > sizeY-2)) return false; + if (direction == 1 && (X < 1 + boundary[3] || X > sizeX - boundary[4] || Y < 1 + boundary[1] || Y > sizeY - boundary[2])) return false; + if (direction == 2 && (X < 1 + boundary[4] || X > sizeX - boundary[3] || Y < 1 + boundary[2] || Y > sizeY - boundary[1])) return false; + if (direction == 3 && (X < 1 + boundary[1] || X > sizeX - boundary[2] || Y < 1 + boundary[4] || Y > sizeY - boundary[3])) return false; + if (direction == 4 && (X < 1 + boundary[2] || X > sizeX - boundary[1] || Y < 1 + boundary[3] || Y > sizeY - boundary[4])) return false; return true; } // 玩家执行指令添加一架飞机 string PlayerAddPlane(string s, const int direction, const bool overlap) { - string result = CheckCoordinate(s); - if (result != "OK") return result; + string result = CheckCoordinate(s); + if (result != "OK") return result; auto pos = TranString(s); return AddPlane(pos.first, pos.second, direction, overlap); @@ -308,36 +379,49 @@ class Board { return "[错误] 飞机头只能放置于空地,不能与机身或其他飞机头重叠"; } - // 检查是否是侦察点 + // 检查是否是侦察点 if (map[X][Y][0] == 2) { return "[错误] 飞机头不能放置于侦察点"; } - // 检查飞机是否重叠 - for (int i = 0; i < 9; i++) { - if (map[X + position[direction][i][0]][Y + position[direction][i][1]][1] >= 2) { - return "[错误] 无法放置于此位置:机身不能与其他飞机头重叠"; - } - if (map[X + position[direction][i][0]][Y + position[direction][i][1]][1] == 1 && !overlap) { - return "[错误] 无法放置于此位置:当前规则飞机机身之间不允许重叠"; - } - } - // 放置飞机 - map[X][Y][1] = 2; - body[X][Y] = direction; - for (int i = 0; i < 9; i++) { - map[X + position[direction][i][0]][Y + position[direction][i][1]][1] = 1; - body[X + position[direction][i][0]][Y + position[direction][i][1]] += 1; - } - alive++; + // 检查飞机是否重叠 + for (auto position : positions[direction]) { + string position_str = string(1, 'A' + X + position.first - 1) + to_string(Y + position.second); + if (map[X + position.first][Y + position.second][1] >= 2) { + return "[错误] 无法放置于此位置:机身不能与其他飞机头重叠。重叠位置:" + position_str; + } + if (map[X + position.first][Y + position.second][1] == 1 && !overlap) { + return "[错误] 无法放置于此位置:当前规则飞机机身之间不允许重叠。重叠位置:" + position_str; + } + } + for (auto position2 : positions2[direction]) { + string position_str = string(1, 'A' + X + position2.first - 1) + to_string(Y + position2.second); + if (map[X + position2.first][Y + position2.second][1] != 0) { + return "[错误] 多要害模式下,附属飞机头不能与机身或其他飞机头重叠。重叠位置:" + position_str; + } + if (map[X + position2.first][Y + position2.second][0] == 2) { + return "[错误] 多要害模式下,附属飞机头不能放置于侦察点。侦察点位置:" + position_str; + } + } + // 放置飞机 + map[X][Y][1] = 2; + body[X][Y] = direction; + for (auto position : positions[direction]) { + map[X + position.first][Y + position.second][1] = 1; + body[X + position.first][Y + position.second] += 1; + } + for (auto position2 : positions2[direction]) { + map[X + position2.first][Y + position2.second][1] = 2; + } + alive += crucial_num; return "OK"; } // 玩家执行指令移除一架飞机 string PlayerRemovePlane(string s) { - string result = CheckCoordinate(s); - if (result != "OK") return result; + string result = CheckCoordinate(s); + if (result != "OK") return result; auto pos = TranString(s); return RemovePlane(pos.first, pos.second); @@ -346,39 +430,45 @@ class Board // 根据坐标移除飞机 string RemovePlane(int X, int Y) { - if (map[X][Y][1] != 2) { - return "[错误] 移除失败:此处不存在飞机头,请输入飞机头坐标"; - } - int direction = body[X][Y]; - map[X][Y][1] = body[X][Y] = 0; - for (int i = 0; i < 9; i++) { - body[X + position[direction][i][0]][Y + position[direction][i][1]] -= 1; - // 如果此位置已没有机身,则设为空地(存在重叠则不移除) - if (body[X + position[direction][i][0]][Y + position[direction][i][1]] == 0) { - map[X + position[direction][i][0]][Y + position[direction][i][1]][1] = 0; - } - } - alive--; + if (map[X][Y][1] != 2) { + return "[错误] 移除失败:此处不存在飞机头,请输入飞机头坐标"; + } + int direction = body[X][Y]; + if (direction == 0) { + return "[错误] 移除失败:此位置的飞机头并非主飞机头,请输入主飞机头坐标来移除飞机(已用红框标记)"; + } + map[X][Y][1] = body[X][Y] = 0; + for (auto position : positions[direction]) { + body[X + position.first][Y + position.second] -= 1; + // 如果此位置已没有机身,则设为空地(存在重叠则不移除) + if (body[X + position.first][Y + position.second] == 0) { + map[X + position.first][Y + position.second][1] = 0; + } + } + for (auto position2 : positions2[direction]) { + map[X + position2.first][Y + position2.second][1] = 0; + } + alive -= crucial_num; return "OK"; } // 清空全部飞机 - void RemoveAllPlanes() + void RemoveAllPlanes() { for(int i = 1; i <= sizeX; i++) { for(int j = 1; j <= sizeY; j++) { - map[i][j][1] = 0; - body[i][j] = 0; - } - } - alive = 0; + map[i][j][1] = 0; + body[i][j] = 0; + } + } + alive = 0; } // 玩家执行指令使地图被进攻(对方操作) string PlayerAttack(string s) { - string result = CheckCoordinate(s); - if (result != "OK") return result; + string result = CheckCoordinate(s); + if (result != "OK") return result; auto pos = TranString(s); return Attack(pos.first, pos.second); @@ -387,83 +477,163 @@ class Board // 根据坐标执行进攻操作 string Attack(int X, int Y) { - // 检查地图边界 + // 检查地图边界 if (X < 1 || X > sizeX || Y < 1 || Y > sizeY) { return "[错误] 攻击的坐标超出了地图的范围"; } - if (map[X][Y][0] != 0) { - return "[错误] 无法对已被打击的区域或侦察点发射导弹"; - } - map[X][Y][0] = 1; - this_turn[X][Y] = 1; - if (map[X][Y][1] == 0) { - return "0"; - } - if (map[X][Y][1] == 1){ - return "1"; - } - if (map[X][Y][1] == 2){ - alive--; - return "2"; - } + if (map[X][Y][0] != 0) { + return "[错误] 无法对已被打击的区域或侦察点发射导弹"; + } + map[X][Y][0] = 1; + this_turn[X][Y] = 1; + if (map[X][Y][1] == 0) { + return "0"; + } + if (map[X][Y][1] == 1){ + return "1"; + } + if (map[X][Y][1] == 2){ + alive--; + return "2"; + } // 特殊要害 if (map[X][Y][1] == 3){ - return "3"; - } - return "Empty Return"; - } + return "3"; + } + return "Empty Return"; + } - // 添加飞机标记 + // 添加飞机标记 string AddMark(string s, const int direction) { - string result = CheckCoordinate(s); - if (result != "OK") return result; + string result = CheckCoordinate(s); + if (result != "OK") return result; auto pos = TranString(s); int X = pos.first, Y = pos.second; // 检查地图边界 if (!CheckMapBoundary(X, Y, direction)) { return "[错误] 标记的飞机位置超出了地图范围,请检查坐标和方向是否正确"; } - // 设置标记 + // 设置标记 if (mark[X][Y] != 300) mark[X][Y] = 200; - for (int i = 0; i < 9; i++) { - if (mark[X + position[direction][i][0]][Y + position[direction][i][1]] < 200) { - mark[X + position[direction][i][0]][Y + position[direction][i][1]] += 1; + for (auto position : positions[direction]) { + if (mark[X + position.first][Y + position.second] < 200) { + mark[X + position.first][Y + position.second] += 1; } - } + } + for (auto position2 : positions2[direction]) { + if (mark[X + position2.first][Y + position2.second] != 300) { + mark[X + position2.first][Y + position2.second] = 200; + } + } return "OK"; } // 移除飞机标记 - string RemoveMark(string s, const int direction) + string RemoveMark(string s, const int direction) { - string result = CheckCoordinate(s); - if (result != "OK") return result; + string result = CheckCoordinate(s); + if (result != "OK") return result; auto pos = TranString(s); int X = pos.first, Y = pos.second; // 检查地图边界 if (!CheckMapBoundary(X, Y, direction)) { return "[错误] 移除指定的坐标位置超出了地图范围,请检查坐标和方向是否正确"; } - if (mark[X][Y] != 300) mark[X][Y] = 0; - for (int i = 0; i < 9; i++) { - if (mark[X + position[direction][i][0]][Y + position[direction][i][1]] < 200 && mark[X + position[direction][i][0]][Y + position[direction][i][1]] > 0) { - mark[X + position[direction][i][0]][Y + position[direction][i][1]] -= 1; - } - } + if (mark[X][Y] != 300) mark[X][Y] = 0; + for (auto position : positions[direction]) { + if (mark[X + position.first][Y + position.second] < 200 && mark[X + position.first][Y + position.second] > 0) { + mark[X + position.first][Y + position.second] -= 1; + } + } + for (auto position2 : positions2[direction]) { + if (mark[X + position2.first][Y + position2.second] != 300) { + mark[X + position2.first][Y + position2.second] = 0; + } + } return "OK"; } - // 清空全部标记 - void RemoveAllMark() + // 清空全部标记 + void RemoveAllMark() { for(int i = 1; i <= sizeX; i++) { for(int j = 1; j <= sizeY; j++) { if (mark[i][j] != 300) { - mark[i][j] = 0; + mark[i][j] = 0; + } + } + } + } + + static string GetPlaneTable(const vector shape, const int direction, const pair pos = {-1, -1}) { + const vector components = { + " ", + "+", + "", + "", + "①", + "②", + "③", + "④", + "�", + }; + + string table = ""; + int rows = shape.size(); + int cols = shape[0].size(); + for (int r = 0; r < rows; ++r) { + table += ""; + for (int c = 0; c < cols; ++c) { + int trans_r, trans_c; + if (direction == 1) { + trans_r = r; + trans_c = c; + } + if (direction == 2) { + trans_r = rows - r - 1; + trans_c = cols - c - 1; + } + if (direction == 3) { + trans_r = c; + trans_c = rows - r - 1; } - } - } + if (direction == 4) { + trans_r = cols - c - 1; + trans_c = r; + } + if (trans_r == pos.first && trans_c == pos.second) { + table += ""; + } else { + table += components[shape[trans_r][trans_c] - '0']; + } + } + table += ""; + } + table += "
"; + + return table; + } + + string GetAllDirectionTable(const vector shape) { + string table = ""; + table += ""; + table += ""; + table += ""; + table += ""; + table += ""; + table += ""; + + table += ""; + table += ""; + table += ""; + table += ""; + table += ""; + table += ""; + table += "
" + GetPlaneTable(shape, 1, main_crucial) + " " + GetPlaneTable(shape, 2, main_crucial) + "
"; + table += "
" + GetPlaneTable(shape, 3, main_crucial) + " " + GetPlaneTable(shape, 4, main_crucial) + "
"; + table += "
"; + return table; } }; diff --git a/games/naval_battle/boss.h b/games/naval_battle/boss.h index 54215782..825dbdd8 100644 --- a/games/naval_battle/boss.h +++ b/games/naval_battle/boss.h @@ -18,13 +18,13 @@ class Boss // BOSS0 临时类型&万能核心 int tempBossType{}; - const int UniversalCore_position[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}}; - const int UniversalCoreRandom_position[4][2] = {{-1,-1}, {-1,1}, {1,-1}, {1,1}}; + const vector> UniversalCore_position = {{-1,0}, {1,0}, {0,-1}, {0,1}}; + const vector> UniversalCoreRandom_position = {{-1,-1}, {-1,1}, {1,-1}, {1,1}}; // BOSS2 核弹研发中心 bool nuclear_hitted = false; bool RD_is_hit = false; - const int RDcenter_position[8][2] = {{-2,0}, {-1,0}, {1,0}, {2,0}, {0,-2}, {0,-1}, {0,1}, {0,2}}; + const vector> RDcenter_position = {{-2,0}, {-1,0}, {1,0}, {2,0}, {0,-2}, {0,-1}, {0,1}, {0,2}}; // BOSS3 电磁干扰 bool EMI = false; @@ -43,32 +43,18 @@ class Boss // BOSS技能介绍 string BossIntro() const { - const string empty = " "; - const string body = "+"; - const string head = ""; - const string S_head = ""; - const string unknown = "�"; - const string num1 = "①"; - const string num2 = "②"; - const string num3 = "③"; - const string num4 = "④"; - string BOSS_SkillIntro = ""; if (BossType == 0) { BOSS_SkillIntro += ""; BOSS_SkillIntro += ""; - BOSS_SkillIntro += ""; + BOSS_SkillIntro += ""; BOSS_SkillIntro += ""; } if (BossType == 1) { BOSS_SkillIntro += ""; - BOSS_SkillIntro += ""; + BOSS_SkillIntro += ""; BOSS_SkillIntro += ""; BOSS_SkillIntro += ""; BOSS_SkillIntro += ""; @@ -79,15 +65,11 @@ class Boss BOSS_SkillIntro += ""; BOSS_SkillIntro += ""; - BOSS_SkillIntro += ""; - - BOSS_SkillIntro += ""; + BOSS_SkillIntro += ""; + + BOSS_SkillIntro += ""; BOSS_SkillIntro += ""; BOSS_SkillIntro += ""; BOSS_SkillIntro += ""; @@ -107,13 +89,9 @@ class Boss BOSS_SkillIntro += ""; BOSS_SkillIntro += ""; - BOSS_SkillIntro += ""; + BOSS_SkillIntro += ""; BOSS_SkillIntro += ""; } @@ -122,7 +100,7 @@ class Boss } // BOSS放置阶段(放置阶段调用) - void BossPrepare(Board (&board)[2]) + void BossPrepare(Board (&board)[2], int (&timeout)[2]) { // 特殊飞机放置 BossSpecialPlanesPrepare(board); @@ -130,12 +108,17 @@ class Boss int X, Y, direction; RD_is_hit = false; int try_count = 0; - while (board[1].alive < board[1].planeNum) { + int clear_count = 0; + while (board[1].alive < board[1].planeNum * board[1].crucial_num) { X = rand() % board[1].sizeX + 1; Y = rand() % board[1].sizeY + 1; direction = rand() % 4 + 1; if (board[1].AddPlane(X, Y, direction, overlap) == "OK") try_count = 0; if (try_count++ > 5000) { + if (clear_count++ > 10) { + timeout[1] = 1; + return; + } board[1].RemoveAllPlanes(); try_count = 0; BossSpecialPlanesPrepare(board); @@ -155,18 +138,18 @@ class Boss // BOSS0 放置万能核心 board[1].map[X][Y][1] = 3; for (auto position : UniversalCore_position) { - board[1].map[X + position[0]][Y + position[1]][1] = board[1].body[X + position[0]][Y + position[1]] = 1; + board[1].map[X + position.first][Y + position.second][1] = board[1].body[X + position.first][Y + position.second] = 1; } const int a = rand() % 2 + 1, b = rand() % 2 + 1; for (auto position : UniversalCoreRandom_position) { - board[1].map[X + a * position[0]][Y + b * position[1]][1] = board[1].body[X + a * position[0]][Y + b * position[1]] = 1; + board[1].map[X + a * position.first][Y + b * position.second][1] = board[1].body[X + a * position.first][Y + b * position.second] = 1; } SpecialPlane_success = true; } else if (BossType == 2) { // BOSS2 放置核弹研发中心 board[1].map[X][Y][1] = 3; for (auto position : RDcenter_position) { - board[1].map[X + position[0]][Y + position[1]][1] = board[1].body[X + position[0]][Y + position[1]] = 1; + board[1].map[X + position.first][Y + position.second][1] = board[1].body[X + position.first][Y + position.second] = 1; } SpecialPlane_success = true; } else { @@ -182,7 +165,7 @@ class Boss { if (BossType == 0) tempBossType = rand() % 3 + 1; string normalInfo = (BossType == 0 ? "[BOSS " + to_string(tempBossType) + " 形态]\n" : ""); - if (BossType == 1 || tempBossType == 1) normalInfo += BossNormalAttack(board, round, attack_count, 3, 6, 20, 2); + if (BossType == 1 || tempBossType == 1) normalInfo += BossNormalAttack(board, round, attack_count, 1, 4, 20, 2); if (BossType == 2 || tempBossType == 2) normalInfo += BossNormalAttack(board, round, attack_count, 3, 6, 18, 2); if (BossType == 3 || tempBossType == 3) normalInfo += BossNormalAttack(board, round, attack_count, 3, 5, 99, 0); @@ -226,7 +209,7 @@ class Boss int X, Y; int skill = rand() % 100 + 1; - if (board[1].alive <= board[1].planeNum - 3) add_p = 5; + if (board[1].alive <= (board[1].planeNum - 3) * board[1].crucial_num) add_p = 5; if (skill > 100 - skill_probability[0] - add_p * 1) { @@ -234,7 +217,7 @@ class Boss int alive_count = 0; for(int j = 1; j <= board[1].sizeY; j++) { for(int i = 1; i <= board[1].sizeX; i++) { - if (board[1].map[i][j][1] == 2 && board[1].map[i][j][0] == 0) { + if (board[1].map[i][j][1] == 2 && board[1].map[i][j][0] == 0 && board[1].body[i][j] > 0) { board[1].RemovePlane(i, j); alive_count++; } @@ -242,18 +225,18 @@ class Boss } int try_count = 0; int direction; - while (board[1].alive < alive_count) { + while (board[1].alive < alive_count * board[1].crucial_num) { X = rand() % board[1].sizeX + 1; Y = rand() % board[1].sizeY + 1; direction = rand() % 4 + 1; int found_count = 0; - for (int i = 0; i < 9; i++) { - if (board[1].map[X + board[1].position[direction][i][0]][Y + board[1].position[direction][i][1]][0] > 0) { + for (auto position : board[1].positions[direction]) { + if (board[1].map[X + position.first][Y + position.second][0] > 0) { found_count++; } } bool hide = false; - for (int i = 0; i <= 9; i++) { + for (int i = 0; i <= board[1].positions[1].size(); i++) { if ((found_count <= i && try_count >= i * 300) || (found_count <= 1 && rand() % 50 == 0)) { hide = true; break; } @@ -263,8 +246,7 @@ class Boss } if (try_count > 3000) { timeout[1] = 1; - board[1].alive = 0; - return "[系统错误] BOSS技能阶段——空军指挥:移动 " + to_string(alive_count) + " 架飞机时发生错误,随机放置飞机次数到达上限,已强制中断游戏进程!"; + return "[漏洞监测] BOSS技能阶段——空军指挥:移动 " + to_string(alive_count) + " 架飞机时发生错误,随机放置飞机次数到达上限,已强制中断游戏进程!"; } } return "【WARNING】BOSS发动技能 [空军指挥]!已指挥 " + to_string(alive_count) + " 架飞机飞行至地图的其他位置"; @@ -400,11 +382,13 @@ class Boss { // 5%概率发动空军支援,复活或支援新飞机 bool found = false; - for(int j = 1; j <= board[1].sizeY; j++) { - for(int i = 1; i <= board[1].sizeX; i++) { - if (board[1].map[i][j][1] == 2 && board[1].map[i][j][0] == 1 && !found) { - board[1].RemovePlane(i, j); - found = true; + if (board[1].crucial_num == 1) { // 仅单要害模式移除飞机 + for(int j = 1; j <= board[1].sizeY; j++) { + for(int i = 1; i <= board[1].sizeX; i++) { + if (board[1].map[i][j][1] == 2 && board[1].map[i][j][0] == 1 && !found) { + board[1].RemovePlane(i, j); + found = true; + } } } } @@ -419,13 +403,13 @@ class Boss Y = rand() % board[1].sizeY + 1; direction = rand() % 4 + 1; int found_count = 0; - for (int i = 0; i < 9; i++) { - if (board[1].map[X + board[1].position[direction][i][0]][Y + board[1].position[direction][i][1]][0] > 0) { + for (auto position : board[1].positions[direction]) { + if (board[1].map[X + position.first][Y + position.second][0] > 0) { found_count++; } } bool hide = false; - for (int i = 0; i <= 9; i++) { + for (int i = 0; i <= board[1].positions[1].size(); i++) { if (found_count <= i && try_count >= i * 300) { hide = true; } @@ -433,14 +417,12 @@ class Boss if (board[1].map[X][Y][0] == 0 && hide) { if (board[1].AddPlane(X, Y, direction, overlap) == "OK") success = true; } - if (try_count++ > 3000) { - timeout[1] = 1; - board[1].alive = 0; - return "[系统错误] BOSS技能阶段——空军支援:随机放置飞机次数到达上限,已强制中断游戏进程!"; + if (try_count++ > 3000 && !success) { + return "【WARNING】BOSS发动技能 [空军支援]!随机放置飞机次数到达上限,技能发动失败!"; } } if (found) { - board[1].alive++; + board[1].alive += board[1].crucial_num; return "【WARNING】BOSS发动技能 [空军支援]!已成功将一架已被击落的飞机更换为新飞机,并转移位置" + N_warning; } else { board[1].planeNum++; @@ -466,7 +448,7 @@ class Boss EMI = false; int skill = rand() % 100 + 1; - if (board[1].alive <= board[1].planeNum - 3) add_p = 5; + if (board[1].alive <= board[1].planeNum * board[1].crucial_num - 3 * board[1].crucial_num) add_p = 5; if (skill > 100 - skill_probability[0] - add_p * 1) { @@ -556,7 +538,7 @@ class Boss { // 15%概率发动自爆无人机,随机打击3个十字区域 int success = 0; - const int num = board[1].alive <= board[1].planeNum - 3 ? 2 : 3; + const int num = board[1].alive <= board[1].planeNum * board[1].crucial_num - 3 * board[1].crucial_num ? 2 : 3; string str[3], areas; while (success < num) { X = rand() % (board[0].sizeX - 4) + 3; @@ -567,7 +549,7 @@ class Boss // 十字形同BOSS2研发中心 board[0].Attack(X, Y); for (auto position : RDcenter_position) { - board[0].Attack(X + position[0], Y + position[1]); + board[0].Attack(X + position.first, Y + position.second); } success++; areas += (string(1, 'A' + X - 1) + to_string(Y)) + " "; @@ -603,7 +585,7 @@ class Boss } return "【WARNING】BOSS发动技能 [分导弹头]!在导弹落点 " + areas + "附近的5*5区域内进行分散打击,总计额外打击了 " + to_string(count) + " 个位置"; } - else if (skill > 100 - skill_probability[4] && board[1].alive > board[1].planeNum - 3) + else if (skill > 100 - skill_probability[4] && board[1].alive > board[1].planeNum * board[1].crucial_num - 3 * board[1].crucial_num) { // 15%概率发动电磁干扰,玩家导弹在5*5区域偏移 EMI = true; @@ -616,36 +598,34 @@ class Boss // BOSS 被动技能 pair BOSS_PassiveSkills(Board (&board)[2], string str) const { - if (is_boss) { - // BOSS2 [导弹拦截] - if (BossType == 2 || tempBossType == 2) { - if (rand() % 100 < 8) { - string ret = board[0].PlayerAttack(str); - if (ret == "0" || ret == "1" || ret == "2") { - return make_pair(ret, "【WARNING】BOSS触发技能 [导弹拦截],当前导弹被拦截并打击到了玩家的地图上"); - } + // BOSS2 [导弹拦截] + if (BossType == 2 || tempBossType == 2) { + if (rand() % 100 < 8) { + string ret = board[0].PlayerAttack(str); + if (ret == "0" || ret == "1" || ret == "2") { + return make_pair(ret, "【WARNING】BOSS触发技能 [导弹拦截],当前导弹被拦截并打击到了玩家的地图上"); } } - // BOSS3 [电磁干扰] - if (BossType == 3 || tempBossType == 3) { - if (EMI) { - if (Board::CheckCoordinate(str) != "OK") return make_pair("Failed", "Empty"); - int X = str[0] - 'A' + 1, Y = str[1] - '0'; - if (str.length() == 3) Y = (str[1] - '0') * 10 + str[2] - '0'; - if (board[1].map[X][Y][0] > 0) return make_pair("Failed", "Empty"); - - int try_count = 0; - while (try_count++ < 1000) { - int actual_X = X + rand() % 5 - 2; - int actual_Y = Y + rand() % 5 - 2; - string actual_str = string(1, 'A' + actual_X - 1) + to_string(actual_Y); - string ret = board[1].PlayerAttack(actual_str); - if (ret == "0" || ret == "1" || ret == "2") { - if (actual_str == str) { - return make_pair(ret, "虽然受到了强烈的电磁干扰,但导弹还是成功击中了 " + actual_str + "。\n令人振奋的消息...也许?"); - } else { - return make_pair(ret, "受强烈电磁干扰的影响,导弹无法准确锁定位置,导弹击中了 " + actual_str); - } + } + // BOSS3 [电磁干扰] + if (BossType == 3 || tempBossType == 3) { + if (EMI) { + if (Board::CheckCoordinate(str) != "OK") return make_pair("Failed", "Empty"); + int X = str[0] - 'A' + 1, Y = str[1] - '0'; + if (str.length() == 3) Y = (str[1] - '0') * 10 + str[2] - '0'; + if (board[1].map[X][Y][0] > 0) return make_pair("Failed", "Empty"); + + int try_count = 0; + while (try_count++ < 1000) { + int actual_X = X + rand() % 5 - 2; + int actual_Y = Y + rand() % 5 - 2; + string actual_str = string(1, 'A' + actual_X - 1) + to_string(actual_Y); + string ret = board[1].PlayerAttack(actual_str); + if (ret == "0" || ret == "1" || ret == "2" || ret == "3") { + if (actual_str == str) { + return make_pair(ret, "虽然受到了强烈的电磁干扰,但导弹还是成功击中了 " + actual_str + "。\n令人振奋的消息...也许?"); + } else { + return make_pair(ret, "受强烈电磁干扰的影响,导弹无法准确锁定位置,导弹击中了 " + actual_str); } } } @@ -678,7 +658,7 @@ class Boss Y = (str[1] - '0') * 10 + str[2] - '0'; } for (auto position : RDcenter_position) { - board[1].mark[X + position[0]][Y + position[1]] = 300; + board[1].mark[X + position.first][Y + position.second] = 300; } return "核弹研发中心被击中!研发成功概率已大幅下降。\n" + info; } diff --git a/games/naval_battle/icon.png b/games/naval_battle/icon.png index 33f0747c..902a82cf 100644 Binary files a/games/naval_battle/icon.png and b/games/naval_battle/icon.png differ diff --git a/games/naval_battle/mygame.cc b/games/naval_battle/mygame.cc index b820e19d..b643f3c3 100644 --- a/games/naval_battle/mygame.cc +++ b/games/naval_battle/mygame.cc @@ -25,23 +25,55 @@ const GameProperties k_properties { .developer_ = "铁蛋", .description_ = "在地图上布置飞机,并击落对手的飞机", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { - return GET_OPTION_VALUE(options, 飞机) >= 3 ? std::min(3U, GET_OPTION_VALUE(options, 飞机) - 2) : 1; + if (GET_OPTION_VALUE(options, 形状).size() != 1) { + return 0; + } else { + return GET_OPTION_VALUE(options, 飞机) >= 3 ? std::min(3U, GET_OPTION_VALUE(options, 飞机) - 2) : 1; + } } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为" << generic_options_readonly.PlayerNum(); return false; } + auto shape = GET_OPTION_VALUE(game_options, 形状); + if (shape.empty()) { + reply() << "[错误] 形状参数不能为空:必须包含5个长度为5的数字串,且只能包含数字012,数字2(机头)有且仅有一个。形如:00000 00200 11111 00100 01110"; + return false; + } + if (shape[0] != "默认") { + if (shape.size() != 5) { + reply() << "[错误] 形状参数数量必须为5个:必须包含5个长度为5的数字串,且只能包含数字012,数字2(机头)有且仅有一个。形如:00000 00200 11111 00100 01110"; + return false; + } + int countOf2 = 0; + for (const auto& row : shape) { + if (row.size() != 5) { + reply() << "[错误] 形状参数每一行都必须为5个数字:必须包含5个长度为5的数字串,且只能包含数字012,数字2(机头)有且仅有一个。形如:00000 00200 11111 00100 01110"; + return false; + } + if (!std::all_of(row.begin(), row.end(), [](char c) { return c == '0' || c == '1' || c == '2'; })) { + reply() << "[错误] 形状参数中只能包含数字012:必须包含5个长度为5的数字串。形如:00000 00200 11111 00100 01110"; + return false; + } + countOf2 += std::count(row.begin(), row.end(), '2'); + } + if (countOf2 == 0) { + reply() << "[错误] 形状参数中必须包含数字2(机头)"; + return false; + } + } + const int planeLimit[8] = {3, 3, 4, 5, 6, 6, 7, 8}; - if (GET_OPTION_VALUE(game_options, 飞机) > planeLimit[GET_OPTION_VALUE(game_options, 边长) - 8] && !GET_OPTION_VALUE(game_options, 重叠)) { + if (GET_OPTION_VALUE(game_options, 飞机) > planeLimit[GET_OPTION_VALUE(game_options, 边长) - 8] && !GET_OPTION_VALUE(game_options, 重叠) && shape[0] == "默认") { GET_OPTION_VALUE(game_options, 飞机) = planeLimit[GET_OPTION_VALUE(game_options, 边长) - 8]; reply() << "[警告] 边长为 " + to_string(GET_OPTION_VALUE(game_options, 边长)) + " 的地图最多可设置 " + to_string(planeLimit[GET_OPTION_VALUE(game_options, 边长) - 8]) + " 架不重叠的飞机,飞机数已自动调整为 " + to_string(planeLimit[GET_OPTION_VALUE(game_options, 边长) - 8]) + "!"; } @@ -67,12 +99,13 @@ const std::vector k_init_options_commands = { "[第4项] 连发次数:范围 [1-10],其他输入均为默认3
" "[第5项] 侦察区域大小:范围 [0-30],其他输入均为默认随机
" "[第6项] 进攻时限:范围 [30-3600],其他输入均为默认120
" + "[第7项] 自定义形状:包含5个长度5的字符串,具体见配置帮助
" "【多人游戏快捷配置】
" "[第1项] 地图边长:仅支持 [8-15],其他输入均为默认10
" "[第2项] 飞机数:仅支持 [1-8],其他输入均为默认3
" "[第3项] 重叠:(同上)    [第4项] 要害:(同上)
" "[第5项] 连发次数:(同上)  [第6项] 侦察区域大小:(同上)
", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options, const bool& is_single, const vector& options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const bool& is_single, const vector& options) { if (is_single) { if (options.size() >= 1) GET_OPTION_VALUE(game_options, BOSS挑战) = options[0] >= 0 && options[0] <= 3 ? options[0] : 100; @@ -81,6 +114,18 @@ const std::vector k_init_options_commands = { if (options.size() >= 4) GET_OPTION_VALUE(game_options, 连发) = options[3] >= 1 && options[3] <= 10 ? options[3] : GET_OPTION_VALUE(game_options, 连发); if (options.size() >= 5) GET_OPTION_VALUE(game_options, 侦察) = options[4] >= 0 && options[4] <= 30 ? options[4] : GET_OPTION_VALUE(game_options, 侦察); if (options.size() >= 6) GET_OPTION_VALUE(game_options, 进攻时限) = options[5] >= 30 && options[5] <= 3600 ? options[5] : GET_OPTION_VALUE(game_options, 进攻时限); + if (options.size() == 11) { + vector sub_vector(options.begin() + 6, options.begin() + 11); + vector shape; + transform(sub_vector.begin(), sub_vector.end(), + back_inserter(shape), + [](int32_t value) { + ostringstream oss; + oss << setw(5) << setfill('0') << value; + return oss.str(); + }); + GET_OPTION_VALUE(game_options, 形状) = shape; + } generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; } else { @@ -105,7 +150,8 @@ class MainStage : public MainGameStage { public: MainStage(StageUtility&& utility) - : StageFsm(std::move(utility)) + : StageFsm(std::move(utility), + MakeStageCommand(*this, "查看本局对战的飞机形状", &MainStage::ShapeInfo_, VoidChecker("形状"))) , round_(0) , player_scores_(Global().PlayerNum(), 0) { @@ -126,6 +172,12 @@ class MainStage : public MainGameStage // BOSS挑战 Boss boss; + CompReqErrCode ShapeInfo_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + ShowPlaneShape(reply); + return StageErrCode::OK; + } + string GetAllMap(const int show_0, const int show_1, const int crucial_mode) { string allmap = "
???BOSS战
【特殊飞机】万能核心:形状不固定,要害在其中心,随机在某个编号对应的位置上安置4个机身。被打击要害时视为所有BOSS形态的特殊要害被击中(此要害不计算在总要害数中)
"; - BOSS_SkillIntro += "" + num1 + num2 + empty + num2 + num1 + ""; - BOSS_SkillIntro += "" + num3 + num4 + body + num4 + num3 + ""; - BOSS_SkillIntro += "" + empty + body + S_head + body + empty + ""; - BOSS_SkillIntro += "" + num3 + num4 + body + num4 + num3 + ""; - BOSS_SkillIntro += "" + num1 + num2 + empty + num2 + num1 + ""; - BOSS_SkillIntro += "
"; + BOSS_SkillIntro += Board::GetPlaneTable({"45054", "67176", "01310", "67176", "45054"}, 1); + BOSS_SkillIntro += "
【变换技能】每回合会从所有BOSS中随机一个并改变形态,根据变换对应的BOSS发动普通打击和主动技能。被动技能仅在切换至对应的BOSS时才有可能触发。
无限火力BOSS战
BOSS每回合最多发动一个技能,且都会进行普通打击:随机向地图上发射 3-6 枚导弹。当达到 20 回合时,BOSS将增强普攻至最多 8 枚导弹
BOSS每回合最多发动一个技能,且都会进行普通打击:随机向地图上发射 1-4 枚导弹。当达到 20 回合时,BOSS将增强普攻至 4-6 枚导弹
【主动技能1】15% 概率发动 [空军指挥]——随机移动所有未被击落的飞机至其他位置,并尽可能避开已侦察区域
【主动技能3】15% 概率发动 [连环轰炸]——随机打击地图上某个坐标的整个十字区域
【主动技能2】15% 概率发动 [雷达扫描]——随机扫描地图上 5*5 的区域,其中的所有飞机(飞机头+机身)会被直接击落
核平铀好BOSS战
【特殊飞机】核弹研发中心:呈十字形,要害在其中心,形状见下图。被打击要害时使核弹发射基础概率减少 10%(此要害不计算在总要害数中)
"; - BOSS_SkillIntro += "" + empty + empty + body + empty + empty + ""; - BOSS_SkillIntro += "" + empty + empty + body + empty + empty + ""; - BOSS_SkillIntro += "" + body + body + S_head + body + body + ""; - BOSS_SkillIntro += "" + empty + empty + body + empty + empty + ""; - BOSS_SkillIntro += "" + empty + empty + body + empty + empty + ""; - BOSS_SkillIntro += "
BOSS每回合最多发动一个主动技能,且都会进行普通打击:随机向地图上发射 3-6 枚导弹。当达到 18 回合时,BOSS将增强普攻至最多 8 枚导弹
"; + BOSS_SkillIntro += Board::GetPlaneTable({"00100", "00100", "11311", "00100", "00100"}, 1); + BOSS_SkillIntro += "
BOSS每回合最多发动一个主动技能,且都会进行普通打击:随机向地图上发射 3-6 枚导弹。当达到 18 回合时,BOSS将增强普攻至 6-8 枚导弹
【主动技能1】[核弹]——摧毁整个地图,但如果概率低于 6% 时引爆威力会下降。基础概率为0,每回合概率提升 0.5%;BOSS每有一个机身被打击,概率提升 0.1%;每有一个要害被打击,概率提升 0.5%;可通过打击研发中心来延缓进展
【主动技能2】5% 概率发动 [空军支援]——将一架已被击落的飞机更换为新飞机,并转移位置。如果BOSS没有被击落的飞机,会额外起飞一架(最多不超过 6 架)
【主动技能3】15% 概率发动 [石墨炸弹]——发动技能的回合,玩家仅有 1 枚导弹
【?】BOSS战
【特殊飞机】???:(此要害不计算在总要害数中)
"; - BOSS_SkillIntro += "" + empty + body + empty + body + empty + ""; - BOSS_SkillIntro += "" + body + head + empty + head + body + ""; - BOSS_SkillIntro += "" + empty + empty + S_head + empty + empty + ""; - BOSS_SkillIntro += "" + body + head + empty + head + body + ""; - BOSS_SkillIntro += "" + empty + body + empty + body + empty + ""; - BOSS_SkillIntro += "
"; + BOSS_SkillIntro += Board::GetPlaneTable({"01010", "12021", "00300", "12021", "01010"}, 1); + BOSS_SkillIntro += "
【】
"; @@ -135,8 +187,28 @@ class MainStage : public MainGameStage return allmap; } + void ShowPlaneShape(MsgSenderBase& msgSender) + { + auto sender = msgSender(); + if (GAME_OPTION(形状).size() != 1) { + if (board[0].crucial_num == 1) { + sender << "本局游戏使用的飞机形状如下图所示\n"; + } else { + sender << "本局为多要害模式,打击全部飞机头才能获胜。\n在放置、移除、标记时,需输入主飞机头的坐标(已在图中用红框标记)\n"; + } + sender << Markdown(board[0].GetAllDirectionTable(GAME_OPTION(形状)), 360); + } else { + sender << "本局游戏使用的飞机为默认形状\n" << Markdown(board[0].GetAllDirectionTable({"00000", "00200", "11111", "00100", "01110"}), 360); + } + } + virtual void FirstStageFsm(SubStageFsmSetter setter) override { + // 自定义形状初始化 + if (GAME_OPTION(形状).size() != 1) { + board[0].CustomizeShape(GAME_OPTION(形状)); + board[1].CustomizeShape(GAME_OPTION(形状)); + } // 初始化玩家配置参数 for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { board[pid].sizeX = board[pid].sizeY = GAME_OPTION(边长); @@ -194,8 +266,8 @@ class MainStage : public MainGameStage board[1].planeNum = 6; } } - board[0].InitializeMap(); - board[1].InitializeMap(); + board[0].InitializeMap(GAME_OPTION(形状).size() == 1); + board[1].InitializeMap(GAME_OPTION(形状).size() == 1); // 随机生成侦察点 srand((unsigned int)time(NULL)); @@ -319,6 +391,11 @@ class PrepareStage : public SubGameStage<> Global().Boardcast() << "[特殊规则] 本局仅首“要害”公开:每个玩家命中过1次机头以后,之后再次命中其他机头时,仅告知命中,不提示命中要害,且不具有额外一回合。"; } + // 游戏开始时展示飞机形状 + if (GAME_OPTION(形状).size() != 1) { + Main().ShowPlaneShape(Global().BoardcastMsgSender()); + } + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { Global().Tell(pid) << Markdown(Main().board[pid].Getmap(1, GAME_OPTION(要害))); Global().Tell(pid) << "请放置飞机,指令为「坐标 方向」,如:C5 上\n可通过「帮助」查看全部命令格式"; @@ -334,10 +411,10 @@ class PrepareStage : public SubGameStage<> return StageErrCode::FAILED; } if (Global().IsReady(pid)) { - reply() << "[错误] 放置失败:您已经确认过了,请等待对手行动"; + reply() << "[错误] 您已经确认过了,请等待对手行动"; return StageErrCode::FAILED; } - if (Main().board[pid].alive == Main().board[pid].planeNum) { + if (Main().board[pid].alive / Main().board[pid].crucial_num == Main().board[pid].planeNum) { reply() << "[错误] 放置失败:飞机数量已达本局上限,请先使用「移除 坐标」命令移除飞机,或使用「清空」命令清除全部飞机"; return StageErrCode::FAILED; } @@ -349,8 +426,8 @@ class PrepareStage : public SubGameStage<> } reply() << Markdown(Main().board[pid].Getmap(1, GAME_OPTION(要害))); - if (Main().board[pid].alive < Main().board[pid].planeNum) { - reply() << "放置成功!您还有 " + to_string(Main().board[pid].planeNum - Main().board[pid].alive) + " 架飞机等待放置,请继续行动"; + if (Main().board[pid].alive / Main().board[pid].crucial_num < Main().board[pid].planeNum) { + reply() << "放置成功!您还有 " + to_string(Main().board[pid].planeNum - Main().board[pid].alive / Main().board[pid].crucial_num) + " 架飞机等待放置,请继续行动"; } else { reply() << "放置成功!若您已准备完成,请使用「确认」命令结束行动"; } @@ -403,8 +480,8 @@ class PrepareStage : public SubGameStage<> reply() << "[错误] 您已经确认过了,请等待对手确认行动"; return StageErrCode::FAILED; } - if (Main().board[pid].alive < Main().board[pid].planeNum) { - reply() << "[错误] 您还有 " + to_string(Main().board[pid].planeNum - Main().board[pid].alive) + " 架飞机等待放置"; + if (Main().board[pid].alive / Main().board[pid].crucial_num < Main().board[pid].planeNum) { + reply() << "[错误] 您还有 " + to_string(Main().board[pid].planeNum - Main().board[pid].alive / Main().board[pid].crucial_num) + " 架飞机等待放置"; return StageErrCode::FAILED; } reply() << "确认成功!"; @@ -449,7 +526,13 @@ class PrepareStage : public SubGameStage<> return StageErrCode::OK; } - Main().boss.BossPrepare(Main().board); + Main().boss.BossPrepare(Main().board, Main().timeout); + + if (Main().timeout[1] == 1) { + Global().SetReady(0); + Global().Boardcast() << "[漏洞监测] BOSS准备阶段——普通飞机放置:随机放置飞机和清空次数到达上限,已强制中断游戏进程!"; + return StageErrCode::READY; + } Global().Boardcast() << "BOSS已抵达战场,请根据BOSS技能选择合适的部署和战术!\n" << Markdown(Main().boss.BossIntro(), 680); return StageErrCode::READY; @@ -509,14 +592,19 @@ class AttackStage : public SubGameStage<> return StageErrCode::FAILED; } - // BOSS被动技能 - pair pair_paf = Main().boss.BOSS_PassiveSkills(Main().board, str); - string result = pair_paf.first; - if (pair_paf.second != "Empty") { - reply() << pair_paf.second; - } - // 正常攻击 - if (result == "Failed") { + string result = ""; + if (Main().boss.is_boss) { + // BOSS被动技能 + pair pair_paf = Main().boss.BOSS_PassiveSkills(Main().board, str); + result = pair_paf.first; + if (pair_paf.second != "Empty") { + reply() << pair_paf.second; + } + if (result == "Failed") { + result = Main().board[!pid].PlayerAttack(str); + } + } else { + // 正常攻击 result = Main().board[!pid].PlayerAttack(str); } diff --git a/games/naval_battle/options.h b/games/naval_battle/options.h index 73fe30e2..de7edd48 100644 --- a/games/naval_battle/options.h +++ b/games/naval_battle/options.h @@ -8,3 +8,5 @@ EXTEND_OPTION("连发次数", 连发, (ArithChecker(1, 10, "次数")), EXTEND_OPTION("初始随机侦察区域大小(默认为随机)", 侦察, (ArithChecker(0, 30, "面积")), 100) EXTEND_OPTION("BOSS挑战类型:【快捷配置已不再此处支持】
" "如需使用快捷配置请使用「#规则 大海战」查看帮助", BOSS挑战, (ArithChecker(0, 3, "类型")), 100) +EXTEND_OPTION("自定义飞机形状:需包含5*5图形内的所有格子信息,参数分为5段(视为5行),一行5个格子。其中2为机头(需被全部打击),1为机身,0为空地", 形状, + (RepeatableChecker>("形状参数", "00000 00200 11111 00100 01110")), (std::vector{"默认"})) diff --git a/games/nerduel/mygame.cc b/games/nerduel/mygame.cc index 6487517e..0ebacddc 100644 --- a/games/nerduel/mygame.cc +++ b/games/nerduel/mygame.cc @@ -24,12 +24,12 @@ const GameProperties k_properties { .developer_ = "dva", .description_ = "猜测对方所设置的算式的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) { return 1; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return 1; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 2) { reply() << "人数不足。"; return false; @@ -39,14 +39,14 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; }, VoidChecker("单机")), InitOptionsCommand("设置游戏模式", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options, const bool is_normal) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const bool is_normal) { GET_OPTION_VALUE(game_options, 游戏模式) = is_normal; return NewGameMode::MULTIPLE_USERS; @@ -57,7 +57,7 @@ const std::vector k_init_options_commands = { // ========== UI ============== struct MyTable { - MyTable(const MyGameOptions& option, const std::string_view resource_dir) + MyTable(const CustomOptions& option, const std::string_view resource_dir) : resource_dir_(resource_dir), len(GET_OPTION_VALUE(option, 等式长度)), table_(1, GET_OPTION_VALUE(option, 等式长度) * 2 + 9) { diff --git a/games/normal_renju/mygame.cc b/games/normal_renju/mygame.cc index 7432e1a3..a2f94579 100644 --- a/games/normal_renju/mygame.cc +++ b/games/normal_renju/mygame.cc @@ -24,12 +24,12 @@ const GameProperties k_properties { .developer_ = "森高", .description_ = "采用 swap2 规则的无禁手五子棋游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 2; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 2; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为" << generic_options_readonly.PlayerNum(); @@ -40,7 +40,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; diff --git a/games/numcomb/mygame.cc b/games/numcomb/mygame.cc index c26fa354..03b62f99 100644 --- a/games/numcomb/mygame.cc +++ b/games/numcomb/mygame.cc @@ -31,16 +31,16 @@ const GameProperties k_properties { .developer_ = "森高", .description_ = "通过放置卡牌,让同数字连成直线获得积分,比拼分数高低的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) { return GET_OPTION_VALUE(options, 种子).empty() ? 2 : 0; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return GET_OPTION_VALUE(options, 种子).empty() ? 2 : 0; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { return true; } +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { return true; } const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 1; return NewGameMode::SINGLE_USER; diff --git a/games/opencomb/achievements.h b/games/opencomb/achievements.h new file mode 100644 index 00000000..1e695308 --- /dev/null +++ b/games/opencomb/achievements.h @@ -0,0 +1,14 @@ +EXTEND_ACHIEVEMENT(蜂巢开拓者, "在不指定种子的前提下,地图上未联通的线路仅剩 2 条") +EXTEND_ACHIEVEMENT(蜂巢编织者, "在不指定种子的前提下,地图上未联通的线路仅剩 1 条") +EXTEND_ACHIEVEMENT(蜂巢完美者, "在不指定种子的前提下,成功联通地图上的每一条线路") +EXTEND_ACHIEVEMENT(经典开篇, "无种子且使用传统模式[经典]卡池,基础分达到 250 分及以上") +EXTEND_ACHIEVEMENT(不凡经典, "无种子且使用传统模式[经典]卡池,基础分达到 300 分及以上") +EXTEND_ACHIEVEMENT(传世经典, "无种子且使用传统模式[经典]卡池,基础分达到 350 分及以上") +EXTEND_ACHIEVEMENT(万象归一, "无种子且使用传统模式[癞子]卡池,基础分达到 350 分及以上") +EXTEND_ACHIEVEMENT(空灵之境, "无种子且使用传统模式[空气]卡池,基础分达到 250 分及以上") +EXTEND_ACHIEVEMENT(乱象初现, "无种子且使用传统模式[混乱]卡池,基础分达到 250 分及以上") +EXTEND_ACHIEVEMENT(破碎旅程, "无种子且使用传统模式[混乱]卡池,基础分达到 300 分及以上") +EXTEND_ACHIEVEMENT(混乱主宰, "无种子且使用传统模式[混乱]卡池,基础分达到 350 分及以上") +EXTEND_ACHIEVEMENT(空中楼阁, "无种子且使用[空气]或[混乱]卡池,联通所有的空气线路") +EXTEND_ACHIEVEMENT(十全十美, "无种子且使用[癞子]或[混乱]卡池,获得第一名且所有癞子线均为10分") +EXTEND_ACHIEVEMENT(贯穿星河, "在不指定种子的前提下,成功联通一条长度为 9 的线路") \ No newline at end of file diff --git a/games/opencomb/icon.png b/games/opencomb/icon.png new file mode 100644 index 00000000..8e55df0a Binary files /dev/null and b/games/opencomb/icon.png differ diff --git a/games/opencomb/mygame.cc b/games/opencomb/mygame.cc new file mode 100644 index 00000000..2ebd1954 --- /dev/null +++ b/games/opencomb/mygame.cc @@ -0,0 +1,1004 @@ +// Copyright (c) 2018-present, JiaQi Yu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "game_framework/stage.h" +#include "game_framework/util.h" +#include "utility/html.h" + +#include "opencomb.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +class MainStage; +template using SubGameStage = StageFsm; +template using MainGameStage = StageFsm; +const GameProperties k_properties { + .name_ = "开放蜂巢", + .developer_ = "铁蛋", + .description_ = "使用特殊道具改变经典地图,体验不一样的数字蜂巢玩法", +}; +uint64_t MaxPlayerNum(const CustomOptions& options) { return 10; } +uint32_t Multiple(const CustomOptions& options) { return GET_OPTION_VALUE(options, 种子).empty() ? 2 : 0; } +const MutableGenericOptions k_default_generic_options; +const std::vector k_rule_commands = {}; + +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { + if (GET_OPTION_VALUE(game_options, 模式) == 1 && generic_options_readonly.PlayerNum() < 2) { + reply() << "「云顶」对战模式至少需要 2 人参加游戏"; + return false; + } + return true; +} + +const std::vector k_init_options_commands = { + InitOptionsCommand("独自一人开始游戏", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) + { + generic_options.bench_computers_to_player_num_ = 1; + return NewGameMode::SINGLE_USER; + }, + VoidChecker("单机")), + InitOptionsCommand("修改游戏配置:卡池、地图、游戏模式、道具、连线奖励", + [] (CustomOptions& game_options, + MutableGenericOptions& generic_options, + const int32_t& card, + const std::string map, + const std::string mode, + const std::string special, + const std::string line_rewards + ) { + GET_OPTION_VALUE(game_options, 卡池) = card; + for (size_t i = 0; i < map_string.size(); ++i) { + if (map_string[i].first == map) { + GET_OPTION_VALUE(game_options, 地图) = i; + break; + } + } + if (mode == "云顶") GET_OPTION_VALUE(game_options, 模式) = 1; + if (special == "关闭") GET_OPTION_VALUE(game_options, 道具) = false; + if (line_rewards == "关闭") GET_OPTION_VALUE(game_options, 连线奖励) = false; + return NewGameMode::MULTIPLE_USERS; + }, + AlterChecker({{"经典", 0}, {"癞子", 1}, {"空气", 2}, {"混乱", 3}}), + OptionalDefaultChecker>("经典", "地图", "随机"), + OptionalDefaultChecker>("传统", "模式", "云顶"), + OptionalDefaultChecker>("开启", "道具", "[开启/关闭]"), + OptionalDefaultChecker>("开启", "连线奖励", "[开启/关闭]")), +}; + +// ========== GAME STAGES ========== + +static const std::array, k_direct_max> all_points{ + std::vector{3, 4, 8, 10, 0}, + std::vector{1, 5, 9, 10, 0}, + std::vector{2, 6, 7, 10, 0} +}; + +struct Player +{ + Player(std::string resource_path, const uint32_t map) : comb_(new OpenComb(std::move(resource_path), map)) {} + Player(Player&&) = default; + int hp = 200; + std::unique_ptr comb_; +}; + +class RoundStage; +class SelectStage; + +class MainStage : public MainGameStage +{ + public: + MainStage(StageUtility&& utility) + : StageFsm(std::move(utility), + MakeStageCommand(*this, "查看蜂巢初始状态,用于预览序号", &MainStage::InitInfo_, AlterChecker({{"地图", 0}, {"盘面", 0}}))) + , round_(-1) + , alive_(Global().PlayerNum()) + , player_out_(Global().PlayerNum(), 0) + , player_leave_(Global().PlayerNum(), false) + { + srand((unsigned int)time(NULL)); + const uint32_t map = GAME_OPTION(地图) == 0 ? rand() % k_map_size : GAME_OPTION(地图) - 1; + for (uint64_t i = 0; i < Global().PlayerNum(); ++i) { + players_.emplace_back(Global().ResourceDir(), map); + } + + // seed + seed_str_ = GAME_OPTION(种子); + if (seed_str_.empty()) { + std::random_device rd; + std::uniform_int_distribution dis; + seed_str_ = std::to_string(dis(rd)); + } + std::seed_seq seed(seed_str_.begin(), seed_str_.end()); + g = std::mt19937(seed); + + // 卡池模板生成 + std::vector classical_cards_; + std::vector wild_cards_; + std::vector air_cards_; + std::vector chaos_cards_; + for (const int32_t point_0 : all_points[0]) { + for (const int32_t point_1 : all_points[1]) { + for (const int32_t point_2 : all_points[2]) { + int32_t count0 = (point_0 == 0) + (point_1 == 0) + (point_2 == 0); + int32_t count10 = (point_0 == 10) + (point_1 == 10) + (point_2 == 10); + if (count0 == 0 && count10 == 0) { + classical_cards_.emplace_back(point_0, point_1, point_2); + } + if (count0 == 0 && count10 == 1) { + wild_cards_.emplace_back(point_0, point_1, point_2); + } + if (count0 == 1 && count10 == 0) { + air_cards_.emplace_back(point_0, point_1, point_2); + } + if (count0 > 0 && count10 > 0) { + chaos_cards_.emplace_back(point_0, point_1, point_2); + } + } + } + } + + if (GAME_OPTION(卡池) == 0) { + cards_.insert(cards_.end(), classical_cards_.begin(), classical_cards_.end()); + cards_.insert(cards_.end(), classical_cards_.begin(), classical_cards_.end()); + } else if (GAME_OPTION(卡池) == 1) { + cards_.insert(cards_.end(), classical_cards_.begin(), classical_cards_.end()); + cards_.insert(cards_.end(), wild_cards_.begin(), wild_cards_.end()); + } else if (GAME_OPTION(卡池) == 2) { + cards_.insert(cards_.end(), classical_cards_.begin(), classical_cards_.end()); + cards_.insert(cards_.end(), air_cards_.begin(), air_cards_.end()); + } else if (GAME_OPTION(卡池) == 3) { + cards_.insert(cards_.end(), chaos_cards_.begin(), chaos_cards_.end()); + std::shuffle(classical_cards_.begin(), classical_cards_.end(), g); + std::shuffle(wild_cards_.begin(), wild_cards_.end(), g); + std::shuffle(air_cards_.begin(), air_cards_.end(), g); + cards_.insert(cards_.end(), classical_cards_.begin(), classical_cards_.begin() + 10); + cards_.insert(cards_.end(), wild_cards_.begin(), wild_cards_.begin() + 10); + cards_.insert(cards_.end(), air_cards_.begin(), air_cards_.begin() + 10); + cards_.emplace_back(0, 0, 0); + cards_.emplace_back(0, 0, 0); + } + + std::vector tmp_cards = cards_; + + for (uint32_t i = 0; i < 2; ++i) cards_.emplace_back(10, 10, 10); + + if (GAME_OPTION(道具)) { + for (uint32_t i = 0; i < 2; ++i) cards_.emplace_back("wall"); + for (uint32_t i = 0; i < 2; ++i) cards_.emplace_back("wall_broken"); + for (uint32_t i = 0; i < 4; ++i) cards_.emplace_back("erase"); + for (uint32_t i = 0; i < 3; ++i) cards_.emplace_back("move"); + for (uint32_t i = 0; i < 2; ++i) cards_.emplace_back("reshape"); + } + + std::shuffle(cards_.begin(), cards_.end(), g); + + if (GAME_OPTION(道具)) { + // first round + const std::string special_cards[5] = {"wall", "wall_broken", "erase", "move", "reshape"}; + std::uniform_int_distribution dist(0, 4); + int32_t start = dist(g); + cards_.insert(cards_.begin(), special_cards[start]); + } else { + round_ = 0; + } + + // 云顶模式 + if (GAME_OPTION(模式) == 1) { + if (!GAME_OPTION(连线奖励)) { + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + players_[pid].hp = 150; + } + } + dRate_ = std::pow(M_E, Global().PlayerNum() / 6.0) / M_E; + iRate_ = dRate_; + std::shuffle(tmp_cards.begin(), tmp_cards.end(), g); + cards2_.insert(cards2_.end(), tmp_cards.begin(), tmp_cards.end()); + std::shuffle(tmp_cards.begin(), tmp_cards.end(), g); + cards2_.insert(cards2_.end(), tmp_cards.begin(), tmp_cards.end()); + } + + it_ = cards_.begin(); + it2_ = cards2_.begin(); + } + + virtual void FirstStageFsm(SubStageFsmSetter setter) override; + + virtual void NextStageFsm(SelectStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) override; + + virtual void NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) override; + + bool hasBots(); + + int64_t PlayerScore(const PlayerID pid) const + { + if (GAME_OPTION(连线奖励)) { + return players_[pid].comb_->Score() + players_[pid].comb_->ExtraScore(); + } else { + return players_[pid].comb_->Score(); + } + } + + void SetPlayerBoard(html::Table& table, const int pos, const PlayerID pid, const bool isEliminated) { + std::string board = "### " + Global().PlayerAvatar(pid, 40) + "   " + Global().PlayerName(pid) + + "\n\n### " HTML_COLOR_FONT_HEADER(green) "当前积分:" + std::to_string(PlayerScore(pid)) + HTML_FONT_TAIL + + (GAME_OPTION(连线奖励) ? (HTML_COLOR_FONT_HEADER(blue) "(" + std::to_string(players_[pid].comb_->Score()) + "+" + + std::to_string(players_[pid].comb_->ExtraScore()) + ")" + HTML_FONT_TAIL) : "") + + (GAME_OPTION(模式) == 1 ? (HTML_COLOR_FONT_HEADER(#8B0000) " 血量:" + std::to_string(players_[pid].hp) + HTML_FONT_TAIL) : "") + + "\n\n" + + players_[pid].comb_->ToHtml(); + if (isEliminated) { + table.Get(pos / 2, pos % 2).SetColor("#C0C0C0").SetContent(board); + } else { + table.Get(pos / 2, pos % 2).SetContent(board); + } + } + + std::string CombHtml(const std::string& str) + { + html::Table table(players_.size() / 2 + 1, 2); + table.SetTableStyle("align=\"center\" cellpadding=\"20\" cellspacing=\"0\""); + int pos = 0; + // 传统模式或未淘汰玩家 + for (PlayerID pid = 0; pid.Get() < players_.size(); ++pid) { + if (player_out_[pid] == 0) { + SetPlayerBoard(table, pos++, pid, false); + } + } + if (players_.size() % 2 && GAME_OPTION(模式) == 0) { + table.MergeRight(table.Row() - 1, 0, 2); + } + if (GAME_OPTION(模式) == 1) { + // 云顶模式已淘汰玩家 + for (PlayerID pid = 0; pid.Get() < players_.size(); ++pid) { + if (player_out_[pid] > 0) { + SetPlayerBoard(table, pos++, pid, true); + } + } + std::stringstream ss; + ss << std::fixed << std::setprecision(2) << dRate_; + std::string rate = ss.str(); + return str + "(伤害倍率:" + rate + ")" + GetStyle(Global().ResourceDir()) + table.ToString(); + } + return str + GetStyle(Global().ResourceDir()) + table.ToString(); + } + + std::string GetName(std::string x) { + std::string ret = ""; + int n = x.length(); + if (n == 0) return ret; + + int l = 0; + int r = n - 1; + + if (x[0] == '<') l++; + if (x[r] == '>') { + while (r >= 0 && x[r] != '(') r--; + r--; + } + + for (int i = l; i <= r; i++) { + ret += x[i]; + } + return ret; + } + + bool CheckGameOver_(const std::vector& players_) const + { + if (GAME_OPTION(模式) == 0) { + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (players_[pid].comb_->IsAllFilled()) { + return true; + } + } + } else { + if (alive_ <= 1) { + return true; + } + } + return false; + } + + int32_t round_; + std::vector players_; + std::string seed_str_; + std::mt19937 g; + + int32_t alive_; + std::vector player_out_; + std::vector player_leave_; + std::vector> fought_list_; + double dRate_, iRate_; + PlayerID last_mirror_; + std::vector cards2_; + decltype(cards2_)::iterator it2_; + + private: + CompReqErrCode InitInfo_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const uint32_t null) + { + html::Table table(2, 2); + table.SetTableStyle("align=\"center\" cellpadding=\"10\" cellspacing=\"0\""); + table.Get(0, 0).SetContent(" \n### " + Global().PlayerAvatar(pid, 40) + "   " + Global().PlayerName(pid)); + table.Get(0, 1).SetContent(" \n### 初始地图"); + table.Get(1, 0).SetContent(players_[pid].comb_->ToHtml()); + table.Get(1, 1).SetContent(players_[pid].comb_->GetInitTable_()); + reply() << Markdown("" + GetStyle(Global().ResourceDir()) + table.ToString()); + return StageErrCode::OK; + } + + void NewStage_(SubStageFsmSetter& setter); + + std::vector cards_; + decltype(cards_)::iterator it_; +}; + +class RoundStage : public SubGameStage<> +{ + public: + RoundStage(MainStage& main_stage, const uint64_t round, const AreaCard& card) + : StageFsm(main_stage, (round ? "第" + std::to_string(round) + "回合" : "道具回合"), + MakeStageCommand(*this, "放置砖块(或使用道具)", &RoundStage::Set_, ArithChecker(1, 73, "位置")), + MakeStageCommand(*this, "使用移动道具", &RoundStage::Move_, ArithChecker(1, 73, "起始"), ArithChecker(1, 73, "结束")), + MakeStageCommand(*this, "跳过本回合(仅限特殊道具)", &RoundStage::Pass_, VoidChecker("pass")), + MakeStageCommand(*this, "查看本回合开始时蜂巢情况,可用于图片重发", &RoundStage::Info_, VoidChecker("赛况"))) + , card_(card) + , comb_html_(main_stage.CombHtml("## 第 " + std::to_string(round) + " 回合")) + {} + + virtual void OnStageBegin() override + { + if (card_.Type() == "") { + Global().Boardcast() << "本回合砖块为 " << card_.CardName() << ",请公屏或私信裁判设置数字:"; + } else { + Global().Boardcast() << "本回合为特殊道具 [" << card_.CardName() << "],请公屏或私信裁判使用道具,或「pass」跳过本回合:"; + } + SendInfo(Global().BoardcastMsgSender()); + Global().StartTimer(GAME_OPTION(局时)); + } + + private: + void HandleUnreadyPlayers_() + { + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (!Global().IsReady(pid) && Main().player_out_[pid] == 0) { + auto& player = Main().players_[pid]; + auto sender = Global().Boardcast(); + + if (card_.Type() == "") { + if (player.comb_->IsAllFilled()) { + // 云顶模式盘面已满,放入位置1 + const auto [point, extra_point] = player.comb_->Fill(1, card_); + sender << At(pid) << "因为超时未做选择,盘面已满自动填入位置 1"; + if (point > 0) sender << ",意外收获 " + std::to_string(point) + " 点积分"; + else if (point < 0) sender << ",损失 " + std::to_string(-point) + " 点积分"; + if (GAME_OPTION(连线奖励)) { + if (extra_point > 0) sender << "和连线额外奖励 " + std::to_string(extra_point) + " 点积分"; + else if (extra_point < 0) sender << ",损失奖励得分 " + std::to_string(-extra_point) + " 点积分"; + } + } else { + // 超时自动放入第一个空位 + const auto [num, result] = player.comb_->SeqFill(card_); + sender << At(pid) << "因为超时未做选择,自动填入空位置 " << num; + if (result.score_ > 0) { + sender << ",意外收获 " << result.score_ << " 点积分"; + } + if (GAME_OPTION(连线奖励) && result.extra_score_ > 0) { + sender << "和连线奖励分数 " << result.extra_score_ << " 点积分"; + } + } + } else { + sender << At(pid) << "因为超时未做选择,自动跳过特殊道具"; + } + } + } + Global().HookUnreadyPlayers(); + } + + virtual CheckoutErrCode OnPlayerLeave(const PlayerID pid) override + { + Main().player_leave_[pid] = true; + return StageErrCode::CONTINUE; + } + + virtual CheckoutErrCode OnStageTimeout() override + { + HandleUnreadyPlayers_(); + if (GAME_OPTION(模式) == 0) { + return StageErrCode::CHECKOUT; + } else { + return Battle(); + } + } + + virtual CheckoutErrCode OnStageOver() override + { + HandleUnreadyPlayers_(); + if (GAME_OPTION(模式) == 0) { + return StageErrCode::CHECKOUT; + } else { + return Battle(); + } + } + + virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) override + { + if (Global().IsReady(pid)) { + return StageErrCode::OK; + } + auto& player = Main().players_[pid]; + if (card_.Type() == "") { + if (player.comb_->IsAllFilled()) { + player.comb_->Fill(1, card_); + } else { + player.comb_->SeqFill(card_); + } + } + return StageErrCode::READY; + } + + AtomReqErrCode Set_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const uint32_t num) + { + if (Global().IsReady(pid)) { + reply() << "您本回合已经行动完成,无法重复设置"; + return StageErrCode::FAILED; + } + if (card_.Type() == "move") { + reply() << "[错误] 移动道具需要输入两个位置,包含起始和结束位置"; + return StageErrCode::FAILED; + } + + auto& player = Main().players_[pid]; + if (player.comb_->IsWall(num) && !player.comb_->CanReplace(num) && card_.Type() != "erase" && card_.Type() != "reshape") { + reply() << "[错误] 该位置为普通墙块,无法放置"; + return StageErrCode::FAILED; + } + if (player.comb_->IsFilled(num) && !player.comb_->CanReplace(num) && card_.Type() != "erase" && card_.Type() != "reshape" && GAME_OPTION(模式) == 0) { + reply() << "[错误] 该位置已经有砖块了,当前位置不可被覆盖"; + return StageErrCode::FAILED; + } + if (card_.Type() == "erase" && !player.comb_->IsWall(num) && !player.comb_->IsFilled(num)) { + reply() << "[错误] 消除失败,该位置为空,试试其它位置吧"; + return StageErrCode::FAILED; + } + const auto [point, extra_point] = player.comb_->Fill(num, card_); + auto sender = reply(); + if (card_.Type() == "erase") { + sender << "消除位置 " << num << " 成功"; + } else if (card_.Type() == "reshape") { + if (player.comb_->IsFilled(num)) { + sender << "将位置 " << num << " 转化为癞子"; + } else { + sender << "重塑位置 " << num << " 成功"; + } + } else { + sender << "设置数字 " << num << " 成功"; + } + if (point > 0) sender << ",本次操作收获 " + std::to_string(point) + " 点积分"; + else if (point < 0) sender << ",本次操作损失 " + std::to_string(-point) + " 点积分"; + if (GAME_OPTION(连线奖励)) { + if (extra_point > 0) sender << ",连线额外奖励 " + std::to_string(extra_point) + " 点积分"; + else if (extra_point < 0) sender << ",损失奖励得分 " + std::to_string(-extra_point) + " 点积分"; + } + return StageErrCode::READY; + } + + AtomReqErrCode Move_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const uint32_t from, const uint32_t to) + { + if (Global().IsReady(pid)) { + reply() << "您本回合已经行动完成,无法重复设置"; + return StageErrCode::FAILED; + } + if (card_.Type() != "move") { + reply() << "[错误] 本回合并非移动道具,不能执行移动操作"; + return StageErrCode::FAILED; + } + + auto& player = Main().players_[pid]; + if (!player.comb_->IsWall(from) && !player.comb_->IsFilled(from)) { + reply() << "[错误] 移动的起始位置为空,仅能移动墙块和砖块"; + return StageErrCode::FAILED; + } + if (player.comb_->IsWall(to) && !player.comb_->CanReplace(to)) { + reply() << "[错误] 移动的结束位置为墙块且不可覆盖"; + return StageErrCode::FAILED; + } + if (player.comb_->IsFilled(to) && !player.comb_->CanReplace(to) && GAME_OPTION(模式) == 0) { + reply() << "[错误] 移动的结束位置为砖块且不可覆盖"; + return StageErrCode::FAILED; + } + const auto [point, extra_point] = player.comb_->Move(from, to); + auto sender = reply(); + sender << "成功将位置 " << from << " 的砖块移动至 " << to; + if (point > 0) sender << ",本次操作收获 " + std::to_string(point) + " 点积分"; + else if (point < 0) sender << ",本次操作损失 " + std::to_string(-point) + " 点积分"; + if (GAME_OPTION(连线奖励)) { + if (extra_point > 0) sender << ",连线额外奖励 " + std::to_string(extra_point) + " 点积分"; + else if (extra_point < 0) sender << ",损失奖励得分 " + std::to_string(-extra_point) + " 点积分"; + } + return StageErrCode::READY; + } + + AtomReqErrCode Pass_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + if (Global().IsReady(pid)) { + reply() << "您本回合已经行动完成,无法重复设置"; + return StageErrCode::FAILED; + } + if (card_.Type() == "") { + reply() << "[错误] 本回合为普通砖块,仅特殊道具可以pass"; + return StageErrCode::FAILED; + } + reply() << "您选择跳过本回合"; + return StageErrCode::READY; + } + + AtomReqErrCode Info_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + SendInfo(reply); + return StageErrCode::OK; + } + + void SendInfo(MsgSenderBase& sender) + { + sender() << Markdown{comb_html_}; + const std::string style = "" + GetStyle(Global().ResourceDir()); + if (card_.Type() == "") { + sender() << Markdown(style + card_.ToHtml(Global().ResourceDir()), 64); + } else { + sender() << Image((Global().ResourceDir() / std::filesystem::path(card_.Type() + ".png")).string()); + } + } + + // 对战逻辑参(fu)考(zhi)星星姬 + AtomReqErrCode Battle() + { + if (Main().alive_ <= 1) { + return StageErrCode::CHECKOUT; + } + + std::string result; + std::vector alive_players; + + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (Main().player_out_[pid] == 0) { + alive_players.push_back(pid); + } + } + + int32_t min_round_ = (Main().alive_ + 1) / 2 - 1; + while (Main().fought_list_.size() > min_round_) { + Main().fought_list_.erase(Main().fought_list_.begin()); + } + + if (Main().round_ >= 3) { + std::unordered_map fight_map; + std::vector list = alive_players; + bool b; + + do { + b = false; + fight_map.clear(); + std::shuffle(list.begin(), list.end(), Main().g); + + for (size_t i = 0; i < list.size() - 1; i += 2) { + for (const auto& hist : Main().fought_list_) { + if (hist.find(list[i]) != hist.end() && hist.at(list[i]) == list[i + 1]) { + b = true; + break; + } + if (hist.find(list[i + 1]) != hist.end() && hist.at(list[i + 1]) == list[i]) { + b = true; + break; + } + } + if (!b) { + fight_map[list[i]] = list[i + 1]; + } + } + } while (b); + + Main().fought_list_.push_back(fight_map); + + while (Main().fought_list_.size() > min_round_) { + Main().fought_list_.erase(Main().fought_list_.begin()); + } + + for (const auto& entry : fight_map) { + const int32_t pid1 = entry.first; + const int32_t pid2 = entry.second; + const int64_t score1 = Main().PlayerScore(pid1); + const int64_t score2 = Main().PlayerScore(pid2); + const std::string name1 = Main().GetName(Global().PlayerName(pid1)); + const std::string name2 = Main().GetName(Global().PlayerName(pid2)); + + int damage = (int)((score2 - score1) * Main().dRate_); + if (score1 > score2) { + result += name1 + " vs " + name2 + " (" + std::to_string(damage) + ")\n"; + Main().players_[pid2].hp += damage; + } else if (score1 < score2) { + result += name1 + " (" + std::to_string(-damage) + ") vs " + name2 + "\n"; + Main().players_[pid1].hp -= damage; + } else { + result += name1 + " vs " + name2 + " (0)\n"; + } + } + + if (list.size() % 2 == 1) { + int32_t player0 = list.back(); + int32_t mirror; + do { + mirror = list[rand() % (list.size() - 1)]; + } while (mirror == Main().last_mirror_); + Main().last_mirror_ = mirror; + + const int64_t score0 = Main().PlayerScore(player0); + const int64_t scoreM = Main().PlayerScore(mirror); + if (score0 >= scoreM) { + result += Main().GetName(Global().PlayerName(player0)) + " vs " + Main().GetName(Global().PlayerName(mirror)) + "(镜像)\n"; + } else { + int damage = (int)((score0 - scoreM) * Main().dRate_); + result += Main().GetName(Global().PlayerName(player0)) + " (" + std::to_string(damage) + ") vs " + Main().GetName(Global().PlayerName(mirror)) + "(镜像)\n"; + Main().players_[player0].hp += damage; + } + } + int32_t fought_round = (Main().round_ - 2 - (Main().round_ - 1) / 7); + if (Main().dRate_ < 1) { + Main().dRate_ = std::min(std::exp(fought_round * 0.22 / Global().PlayerNum()) * Main().iRate_, 1.0); + } + if (Main().dRate_ > 1) { + Main().dRate_ = std::max(std::exp(fought_round * -0.44 / Global().PlayerNum()) * Main().iRate_, 1.0); + } + } else { + result += "本轮不进行玩家对战"; + } + + std::vector list = alive_players; + for (PlayerID pid : list) { + if (Main().players_[pid].hp <= 0) { + Main().alive_--; + Main().player_out_[pid] = 1; + Global().Boardcast() << At(pid) << "已被淘汰!"; + Global().Eliminate(pid); + } + } + + Global().Boardcast() << result; + return StageErrCode::CHECKOUT; + } + + const AreaCard card_; + const std::string comb_html_; +}; + +class SelectStage : public SubGameStage<> +{ + public: + SelectStage(MainStage& main_stage, const uint64_t round, std::vector::iterator& it2_) + : StageFsm(main_stage, "公共配牌阶段", + MakeStageCommand(*this, "选择并放置砖块", &SelectStage::Select_, ArithChecker(1, 11, "选卡"), ArithChecker(1, 73, "位置")), + MakeStageCommand(*this, "查看当前蜂巢情况和选卡卡池,可用于图片重发", &SelectStage::Info_, VoidChecker("赛况"))) + , comb_html_(main_stage.CombHtml("## 第 " + std::to_string(round) + " 回合[公共配牌阶段]")) + { + current_players.clear(); + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (Main().player_out_[pid] == 0) { + current_players.push_back(pid); + } + } + for (int i = 0; i < current_players.size() + 1; i++) { + if (it2_ == Main().cards2_.end()) { + it2_ = Main().cards2_.begin(); + } + const auto& card = *(it2_++); + tmp_cards_.push_back(card); + } + std::seed_seq seed(Main().seed_str_.begin(), Main().seed_str_.end()); + std::mt19937 g(seed); + std::shuffle(current_players.begin(), current_players.end(), Main().g); + + std::sort(current_players.begin(), current_players.end(), [this](const PlayerID &p1, const PlayerID &p2) { + if (Main().players_[p1].hp != Main().players_[p2].hp) { + return Main().players_[p1].hp < Main().players_[p2].hp; + } + return Main().PlayerScore(p1) < Main().PlayerScore(p2); + }); + } + + virtual void OnStageBegin() override + { + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (pid != current_players[0]) { + Global().SetReady(pid); + } + } + SendInfo(Global().BoardcastMsgSender()); + Global().Boardcast() << "请 " << At(current_players[0]) << " 选择"; + Global().StartTimer(GAME_OPTION(局时)); + } + private: + void HandleUnreadyPlayer_() + { + PlayerID pid = current_players[0]; + if (!Global().IsReady(pid) || Main().player_leave_[pid]) { + auto& player = Main().players_[pid]; + auto sender = Global().Boardcast(); + const AreaCard card_ = tmp_cards_[0]; + Selected(1); + if (card_.Type() == "") { + if (player.comb_->IsAllFilled()) { + // 云顶模式盘面已满,放入位置1 + const auto [point, extra_point] = player.comb_->Fill(1, card_); + sender << At(pid) << "因为超时未做选择,自动选择 1 号卡牌,因盘面已满填入位置 1"; + if (point > 0) sender << ",意外收获 " + std::to_string(point) + " 点积分"; + else if (point < 0) sender << ",损失 " + std::to_string(-point) + " 点积分"; + if (GAME_OPTION(连线奖励)) { + if (extra_point > 0) sender << "和连线额外奖励 " + std::to_string(extra_point) + " 点积分"; + else if (extra_point < 0) sender << ",损失奖励得分 " + std::to_string(-extra_point) + " 点积分"; + } + } else { + // 超时自动放入第一个空位 + const auto [num, result] = player.comb_->SeqFill(card_); + sender << At(pid) << "因为超时未做选择,自动选择 1 号卡牌,并填入空位置 " << num; + if (result.score_ > 0) { + sender << ",意外收获 " << result.score_ << " 点积分"; + } + if (GAME_OPTION(连线奖励) && result.extra_score_ > 0) { + sender << "和连线奖励分数 " << result.extra_score_ << " 点积分"; + } + } + } else { + sender << At(pid) << "因为超时未做选择,自动选择并跳过特殊道具"; + } + Global().HookUnreadyPlayers(); + } + } + + virtual CheckoutErrCode OnPlayerLeave(const PlayerID pid) override + { + Main().player_leave_[pid] = true; + return StageErrCode::CONTINUE; + } + + virtual CheckoutErrCode OnStageTimeout() override + { + HandleUnreadyPlayer_(); + return StageOver(); + } + + virtual CheckoutErrCode OnStageOver() override + { + HandleUnreadyPlayer_(); + return StageOver(); + } + + AtomReqErrCode StageOver() + { + if (current_players.empty()) { + return StageErrCode::CHECKOUT; + } + comb_html_ = Main().CombHtml("## 第 " + std::to_string(Main().round_) + " 回合[公共配牌阶段]"); + SendInfo(Global().BoardcastMsgSender()); + Global().Boardcast() << "请 " << At(current_players[0]) << " 选择"; + Global().ClearReady(current_players[0]); + Global().StartTimer(GAME_OPTION(局时)); + return StageErrCode::CONTINUE; + } + + virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) override + { + if (Global().IsReady(pid)) { + return StageErrCode::OK; + } + auto& player = Main().players_[pid]; + const AreaCard card_ = tmp_cards_[0]; + Selected(1); + if (card_.Type() == "") { + if (player.comb_->IsAllFilled()) { + player.comb_->Fill(1, card_); + } else { + player.comb_->SeqFill(card_); + } + } + return StageErrCode::READY; + } + + AtomReqErrCode Select_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const uint32_t id, const uint32_t num) + { + if (Global().IsReady(pid)) { + reply() << "[错误] 当前并非您的选卡回合"; + return StageErrCode::FAILED; + } + if (id < 1 || id > tmp_cards_.size()) { + reply() << "[错误] 无效的选牌ID,请输入 1-" << tmp_cards_.size() << " 之间的数字"; + return StageErrCode::FAILED; + } + const AreaCard card_ = tmp_cards_[id - 1]; + auto& player = Main().players_[pid]; + if (player.comb_->IsWall(num) && !player.comb_->CanReplace(num) && card_.Type() != "erase" && card_.Type() != "reshape") { + reply() << "[错误] 该位置为普通墙块,无法放置"; + return StageErrCode::FAILED; + } + if (card_.Type() == "erase" && !player.comb_->IsWall(num) && !player.comb_->IsFilled(num)) { + reply() << "[错误] 消除失败,该位置为空,试试其它位置吧"; + return StageErrCode::FAILED; + } + Selected(id); + const auto [point, extra_point] = player.comb_->Fill(num, card_); + auto sender = reply(); + if (card_.Type() == "erase") { + sender << "消除位置 " << num << " 成功"; + } else if (card_.Type() == "reshape") { + if (player.comb_->IsFilled(num)) { + sender << "将位置 " << num << " 转化为癞子"; + } else { + sender << "重塑位置 " << num << " 成功"; + } + } else { + sender << "选择 " << id << " 号砖块,设置数字 " << num << " 成功"; + } + if (point > 0) sender << ",本次操作收获 " + std::to_string(point) + " 点积分"; + else if (point < 0) sender << ",本次操作损失 " + std::to_string(-point) + " 点积分"; + if (GAME_OPTION(连线奖励)) { + if (extra_point > 0) sender << ",连线额外奖励 " + std::to_string(extra_point) + " 点积分"; + else if (extra_point < 0) sender << ",损失奖励得分 " + std::to_string(-extra_point) + " 点积分"; + } + return StageErrCode::READY; + } + + void Selected(const uint32_t id) + { + tmp_cards_.erase(tmp_cards_.begin() + id - 1); + current_players.erase(current_players.begin()); + } + + AtomReqErrCode Info_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + SendInfo(reply); + return StageErrCode::OK; + } + + std::string SelectCardHtml_() + { + html::Table avatar_table(1, current_players.size() + 1); + avatar_table.SetTableStyle("cellpadding=\"0\" cellspacing=\"5\""); + avatar_table.Get(0, 0).SetContent(" 选卡顺序:"); + for (int i = 0; i < current_players.size(); i++) { + avatar_table.Get(0, i + 1).SetContent(Global().PlayerAvatar(current_players[i], 40)); + } + html::Table card_table(1, tmp_cards_.size() * 2); + card_table.SetTableStyle("align=\"center\" cellpadding=\"0\" cellspacing=\"0\" "); + for (int i = 0; i < tmp_cards_.size(); i++) { + card_table.Get(0, i * 2).SetContent(std::to_string(i + 1) + "."); + card_table.Get(0, i * 2 + 1).SetContent(tmp_cards_[i].ToHtml(Global().ResourceDir())); + } + const std::string style = "" + GetStyle(Global().ResourceDir()); + return style + avatar_table.ToString() + card_table.ToString(); + } + + void SendInfo(MsgSenderBase& sender) + { + sender() << Markdown{comb_html_}; + sender() << Markdown(SelectCardHtml_(), 300); + } + + std::vector current_players; + std::vector tmp_cards_; + std::string comb_html_; +}; + +void MainStage::NewStage_(SubStageFsmSetter& setter) +{ + if (GAME_OPTION(模式) == 1 && round_ % 7 == 0 && round_ != 0) { + setter.Emplace(*this, ++round_, it2_); + } else { + const auto& card = *(it_++); + setter.Emplace(*this, ++round_, card); + } + return; +} + +void MainStage::FirstStageFsm(SubStageFsmSetter setter) +{ + if (GAME_OPTION(模式) == 1) { + Global().Boardcast() << "[提示] 本局为「云顶」对战模式:砖块可被覆盖,第3回合起进行玩家对战,血量归零淘汰,剩余1人时游戏结束。" + << (GAME_OPTION(连线奖励) ? "" : "由于未开启连线奖励,血量调整为150。"); + } + if (!GAME_OPTION(道具)) { + Global().Boardcast() << "[提示] 本局未开启道具,整局游戏不会出现任何特殊道具"; + } + NewStage_(setter); +} + +void MainStage::NextStageFsm(SelectStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) +{ + NewStage_(setter); +} + +bool MainStage::hasBots() +{ + std::regex pattern(R"(机器人\d+号)"); + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (std::regex_match(Global().PlayerName(pid), pattern)) { + return true; + } + } + return false; +} + +void MainStage::NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) +{ + if (!CheckGameOver_(players_) && it_ != cards_.end()) { + NewStage_(setter); + return; + } + + if (it_ == cards_.end()) Global().Boardcast() << "卡池耗尽,游戏结束"; + + if (GAME_OPTION(模式) == 1 && alive_ == 1) { + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (player_out_[pid] == 0) { + Global().Boardcast() << "游戏结束,恭喜胜者:" << At(pid) << "!"; + } + } + } + + Global().Boardcast() << Markdown(CombHtml("## 终局")); + const std::string game_card[4] = {"经典", "癞子", "空气", "混乱"}; + if (GAME_OPTION(种子).empty()) { + Global().Boardcast() << "【本局卡池】" + game_card[GAME_OPTION(卡池)] + (GAME_OPTION(道具) ? "" : "\n【特殊道具】关闭") + "\n随机数种子:" + seed_str_; + // achievements + auto max_player = std::max_element(players_.begin(), players_.end(), [](const Player& a, const Player& b) { return a.comb_->Score() < b.comb_->Score(); }); + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (GAME_OPTION(模式) == 0) { + if (GAME_OPTION(卡池) == 0) { + if (players_[pid].comb_->Score() >= 250) Global().Achieve(pid, Achievement::经典开篇); + if (players_[pid].comb_->Score() >= 300) Global().Achieve(pid, Achievement::不凡经典); + if (players_[pid].comb_->Score() >= 350) Global().Achieve(pid, Achievement::传世经典); + } else if (GAME_OPTION(卡池) == 1) { + if (players_[pid].comb_->Score() >= 350) Global().Achieve(pid, Achievement::万象归一); + } else if (GAME_OPTION(卡池) == 2) { + if (players_[pid].comb_->Score() >= 250) Global().Achieve(pid, Achievement::空灵之境); + } else if (GAME_OPTION(卡池) == 3) { + if (players_[pid].comb_->Score() >= 250) Global().Achieve(pid, Achievement::乱象初现); + if (players_[pid].comb_->Score() >= 300) Global().Achieve(pid, Achievement::破碎旅程); + if (players_[pid].comb_->Score() >= 350) Global().Achieve(pid, Achievement::混乱主宰); + } + } + if ((GAME_OPTION(卡池) == 2 || GAME_OPTION(卡池) == 3) && players_[pid].comb_->AirAllLine()) { + Global().Achieve(pid, Achievement::空中楼阁); + } + if ((GAME_OPTION(卡池) == 1 || GAME_OPTION(卡池) == 3) && players_[pid].comb_->WildAll10() && players_[pid].comb_->Score() == max_player->comb_->Score()) { + Global().Achieve(pid, Achievement::十全十美); + } + if (players_[pid].comb_->BreakLine() == 2) Global().Achieve(pid, Achievement::蜂巢开拓者); + if (players_[pid].comb_->BreakLine() == 1) Global().Achieve(pid, Achievement::蜂巢编织者); + if (players_[pid].comb_->BreakLine() == 0) Global().Achieve(pid, Achievement::蜂巢完美者); + if (players_[pid].comb_->Length9()) Global().Achieve(pid, Achievement::贯穿星河); + } + } else { + Global().Boardcast() << "【本局卡池】" + game_card[GAME_OPTION(卡池)] + (GAME_OPTION(道具) ? "" : "\n【特殊道具】关闭") + "\n自定义种子:" + seed_str_; + } +} + +auto* MakeMainStage(MainStageFactory factory) { return factory.Create(); } + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // gamespace lgtbot + diff --git a/games/opencomb/opencomb.h b/games/opencomb/opencomb.h new file mode 100644 index 00000000..5f21b222 --- /dev/null +++ b/games/opencomb/opencomb.h @@ -0,0 +1,580 @@ +#pragma once + +const uint32_t k_total_block = 73; +const uint32_t k_direct_max = 3; +enum class Direct { TOP_LEFT = 0, VERT = 1, TOP_RIGHT = 2}; +// 算分检索 +const int32_t line[3][11][9] = { + { + {49, 58, 66, -1, -1, -1, -1, -1, -1}, + {32, 41, 50, 59, 67, -1, -1, -1, -1}, + {15, 24, 33, 42, 51, 60, 68, -1, -1}, + {7, 16, 25, 34, 43, 52, 61, 69, -1}, + {0, 8, 17, 26, 35, 44, 53, 62, 70}, + {1, 9, 18, 27, 36, 45, 54, 63, 71}, + {2, 10, 19, 28, 37, 46, 55, 64, 72}, + {3, 11, 20, 29, 38, 47, 56, 65, -1}, + {4, 12, 21, 30, 39, 48, 57, -1, -1}, + {5, 13, 22, 31, 40, -1, -1, -1, -1}, + {6, 14, 23, -1, -1, -1, -1, -1, -1} + }, + { + {0, 1, 2, 3, 4, 5, 6, -1, -1}, + {7, 8, 9, 10, 11, 12, 13, 14, -1}, + {15, 16, 17, 18, 19, 20, 21, 22, 23}, + {24, 25, 26, 27, 28, 29, 30, 31, -1}, + {32, 33, 34, 35, 36, 37, 38, 39, 40}, + {41, 42, 43, 44, 45, 46, 47, 48, -1}, + {49, 50, 51, 52, 53, 54, 55, 56, 57}, + {58, 59, 60, 61, 62, 63, 64, 65, -1}, + {66, 67, 68, 69, 70, 71, 72, -1, -1}, + {-1, -1, -1, -1, -1, -1, -1, -1, -1}, + {-1, -1, -1, -1, -1, -1, -1, -1, -1} + }, + { + {0, 7, 15, -1, -1, -1, -1, -1, -1}, + {1, 8, 16, 24, 32, -1, -1, -1, -1}, + {2, 9, 17, 25, 33, 41, 49, -1, -1}, + {3, 10, 18, 26, 34, 42, 50, 58, -1}, + {4, 11, 19, 27, 35, 43, 51, 59, 66}, + {5, 12, 20, 28, 36, 44, 52, 60, 67}, + {6, 13, 21, 29, 37, 45, 53, 61, 68}, + {14, 22, 30, 38, 46, 54, 62, 69, -1}, + {23, 31, 39, 47, 55, 63, 70, -1, -1}, + {40, 48, 56, 64, 71, -1, -1, -1, -1}, + {57, 65, 72, -1, -1, -1, -1, -1, -1} + } +}; +// 盘面(-1半砖,-2角落,-3无操作) +const int32_t board[18][9] = { + {-2, -1, 15, -1, 32, -1, 49, -1, -2}, + {-3, 7, -3, 24, -3, 41, -3, 58, -3}, + {0, -3, 16, -3, 33, -3, 50, -3, 66}, + {-3, 8, -3, 25, -3, 42, -3, 59, -3}, + {1, -3, 17, -3, 34, -3, 51, -3, 67}, + {-3, 9, -3, 26, -3, 43, -3, 60, -3}, + {2, -3, 18, -3, 35, -3, 52, -3, 68}, + {-3, 10, -3, 27, -3, 44, -3, 61, -3}, + {3, -3, 19, -3, 36, -3, 53, -3, 69}, + {-3, 11, -3, 28, -3, 45, -3, 62, -3}, + {4, -3, 20, -3, 37, -3, 54, -3, 70}, + {-3, 12, -3, 29, -3, 46, -3, 63, -3}, + {5, -3, 21, -3, 38, -3, 55, -3, 71}, + {-3, 13, -3, 30, -3, 47, -3, 64, -3}, + {6, -3, 22, -3, 39, -3, 56, -3, 72}, + {-3, 14, -3, 31, -3, 48, -3, 65, -3}, + {-2, -3, 23, -3, 40, -3, 57, -3, -2}, + {-3, -1, -3, -1, -3, -1, -3, -1, -3} +}; +// 游戏地图 +const uint32_t k_map_size = 7; +const std::vector> map_string = { + {"随机", 0}, {"经典", 1}, {"环巢", 2}, {"漩涡", 3}, {"飞机", 4}, {"面具", 5}, {"塔楼", 6}, {"三叶草", 7} +}; +const int32_t maps[k_map_size][k_total_block] = { + { // 1#经典(格数:20) + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 0, 0, 0, 1, 1, 1, + 1, 1, 0, 0, 0, 0, 1, 1, + 1, 1, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 0, 0, 0, 1, 1, 1, + 1, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + }, + { // 2#环巢(格数:23,作者:铁蛋) + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, 0, 1, 1, + 1, 1, 0, 0, 1, 0, 0, 1, 1, + 1, 0, 1, 1, 1, 1, 0, 1, + 1, 0, 2, 1, 0, 1, 2, 0, 1, + 1, 0, 1, 1, 1, 1, 0, 1, + 1, 1, 0, 0, 1, 0, 0, 1, 1, + 1, 1, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + }, + { // 3#漩涡(格数:21,作者:纤光) + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 0, 0, 1, 1, 1, + 1, 1, 1, 0, 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, 0, 0, 1, + 1, 0, 1, 0, 0, 0, 1, 0, 1, + 1, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 0, 1, 1, 1, + 1, 1, 1, 0, 0, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + }, + { // 4#飞机(格数:21,作者:纤光) + 1, 1, 1, 0, 1, 1, 1, + 1, 1, 1, 0, 0, 1, 1, 1, + 1, 1, 1, 1, 0, 1, 1, 0, 1, + 1, 1, 1, 0, 0, 1, 0, 1, + 1, 1, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 0, 0, 1, 0, 1, + 1, 1, 1, 1, 0, 1, 1, 0, 1, + 1, 1, 1, 0, 0, 1, 1, 1, + 1, 1, 1, 0, 1, 1, 1, + }, + { // 5#面具(格数:23,作者:纤光) + 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 0, 1, 0, 1, 1, 1, 1, + 1, 1, 0, 0, 1, 0, 0, 1, + 1, 1, 0, 0, 1, 0, 0, 0, 1, + 1, 1, 0, 0, 1, 0, 0, 1, + 1, 1, 0, 1, 0, 1, 1, 1, 1, + 1, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + }, + { // 6#塔楼(格数:23,作者:纤光) + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 0, 1, + 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 0, 0, 0, 1, 0, 0, 1, + 1, 0, 1, 0, 0, 0, 0, 1, 1, + 1, 0, 0, 0, 1, 0, 0, 1, + 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 0, 1, + 1, 1, 1, 1, 1, 1, 1, + }, + { // 7#三叶草(格数:22,作者:九九归一) + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 0, 0, 1, 1, + 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 0, 0, 2, 0, 0, 1, 1, + 1, 0, 0, 0, 2, 2, 1, 0, 1, + 1, 0, 0, 2, 0, 0, 1, 1, + 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + }, +}; + +static std::string GetStyle(std::string resource_path) +{ + return R"( +)"; +} + +class AreaCard +{ + public: + AreaCard() = delete; // wild card change to 10/10/10 + + AreaCard(std::string type) : type_(type) {} + + AreaCard(const int32_t a, const int32_t b, const int32_t c) : points_(std::in_place, std::array{a, b, c}) {} + + template + bool IsMatch(const int32_t point) const { return points_->at(static_cast(direct)) == 10 || point == 10 || points_->at(static_cast(direct)) == point; } + + template + std::optional Point() const + { + if (points_.has_value()) { + return points_->at(static_cast(direct)); + } else { + return std::nullopt; + } + } + + std::string CardName() const + { + std::string str; + if (points_.has_value()) { + str = "card_"; + for (const int32_t point : *points_) { + if (point == 10) { + str += "X"; + } else { + str += std::to_string(point); + } + } + return str; + } if (type_.has_value()) { + if (type_ == "wall") str += "墙块"; + if (type_ == "wall_broken") str += "破碎墙块"; + if (type_ == "erase") str += "骷髅"; + if (type_ == "move") str += "移动"; + if (type_ == "reshape") str += "重塑"; + return str; + } + return "[Error]砖块信息为空"; + } + + std::string ToHtml(std::string image_path, const bool can_replace = false) const + { + std::string div = "
"; + div += "() + ".png\">"; + div += "() + ".png\">"; + div += "() + ".png\">"; + if (can_replace) div += ""; + div += "
"; + return div; + } + + template + std::string ImageName() const + { + if (points_.has_value()) { + int32_t point = points_->at(static_cast(direct)); + if (point == 10) { + if (direct == Direct::VERT) return "Xv"; + if (direct == Direct::TOP_LEFT) return "Xl"; + if (direct == Direct::TOP_RIGHT) return "Xr"; + } else if (point == 0) { + if (direct == Direct::VERT) return "0v"; + if (direct == Direct::TOP_LEFT) return "0l"; + if (direct == Direct::TOP_RIGHT) return "0r"; + } else { + return std::to_string(point); + } + } + return ""; + } + + std::string Type() const + { + if (type_.has_value()) { + return *type_; + } else { + return ""; + } + } + + private: + std::optional> points_; + std::optional type_; +}; + +class Area +{ + public: + friend class OpenComb; + + Area(const uint32_t num) : num_(num), box_(nullptr), is_wall_(0), can_replace_(false), card_(std::nullopt) {} + + void SetBox(html::Box* box) { box_ = box; } + + private: + html::Box* box_; + std::optional card_; + uint32_t is_wall_; // 0无墙 1墙块 2破碎墙块 + bool can_replace_; // 是否可无限替换 + const uint32_t num_; +}; + +class OpenComb +{ + public: + OpenComb(std::string image_path, const uint32_t map) : image_path_(std::move(image_path)), map_(map), table_(k_max_row, k_max_column) + { + analysisMap_(); + table_.SetTableStyle("align=\"center\" cellpadding=\"0\" cellspacing=\"0\" "); + for (int32_t row = 0; row < table_.Row(); ++row) { + for (int32_t col = 0; col < table_.Column(); ++col) { + const int32_t id = board[row][col]; + const bool is_full_box = id != -1 && id != -3; + if (is_full_box) { + table_.MergeDown(row, col, 2); + } + html::Box& box = table_.Get(row, col); + if (id == -1) { + box.SetContent(Image_("wall_half")); + } else if (id == -2) { + box.SetContent(Image_("wall_full")); + } else if (id != -3) { + box.SetContent(EmptyAreaHtml_(maps[map_][id], 0, areas_[id].num_)); + areas_[id].is_wall_ = maps[map_][id]; + areas_[id].SetBox(&box); + } + } + } + initial_table_ = ToHtml(); + } + + OpenComb(const OpenComb& comb) = delete; + OpenComb(OpenComb&& comb) = delete; + + struct CombScore + { + int32_t score_; + int32_t extra_score_; + friend CombScore operator+(const CombScore& s1, const CombScore& s2) + { + return CombScore{.score_ = s1.score_ + s2.score_, .extra_score_ = s1.extra_score_ + s2.extra_score_}; + } + }; + + CombScore Reshape(const uint32_t num) + { + auto& area = areas_[numToid[num]]; + if (area.is_wall_) { + area.is_wall_ = 3 - area.is_wall_; + area.box_->SetContent(EmptyAreaHtml_(area.is_wall_, area.can_replace_, num)); + } else if (area.card_.has_value()) { + const AreaCard wild = AreaCard(10, 10, 10); + area.card_ = wild; + const auto img_str = wild.ToHtml(image_path_, area.can_replace_); + area.box_->SetContent(img_str); + } else { + area.can_replace_ = true; + area.box_->SetContent(EmptyAreaHtml_(0, true, num)); + } + return CaculateCombScore_(); + } + + CombScore Fill(const uint32_t num, const AreaCard& card) + { + if (card.Type() == "reshape") { + return Reshape(num); + } + auto& area = areas_[numToid[num]]; + if (card.Type() == "erase") { + area.is_wall_ = 0; + area.card_ = std::nullopt; + area.box_->SetContent(EmptyAreaHtml_(0, area.can_replace_, num)); + } else if (card.Type() == "wall") { + area.is_wall_ = 1; + area.card_ = std::nullopt; + area.box_->SetContent(EmptyAreaHtml_(1, area.can_replace_, num)); + } else if (card.Type() == "wall_broken") { + area.is_wall_ = 2; + area.card_ = std::nullopt; + area.box_->SetContent(EmptyAreaHtml_(2, area.can_replace_, num)); + } else { + area.is_wall_ = 0; + area.card_ = card; + const auto img_str = card.ToHtml(image_path_, area.can_replace_); + area.box_->SetContent(img_str); + } + return CaculateCombScore_(); + } + + CombScore Move(const uint32_t from, const uint32_t to) + { + auto& area_from = areas_[numToid[from]]; + auto& area_to = areas_[numToid[to]]; + if (area_from.is_wall_) { // 墙块移动 + area_to.is_wall_ = area_from.is_wall_; + area_from.is_wall_ = 0; + area_to.card_ = std::nullopt; + area_to.box_->SetContent(EmptyAreaHtml_(area_to.is_wall_ ,area_to.can_replace_, to)); + area_from.box_->SetContent(EmptyAreaHtml_(0, area_from.can_replace_, from)); + } else { // 普通砖块移动 + area_to.card_ = area_from.card_; + area_to.is_wall_ = 0; + const auto img_str = area_to.card_->ToHtml(image_path_, area_to.can_replace_); + area_to.box_->SetContent(img_str); + area_from.card_ = std::nullopt; + area_from.box_->SetContent(EmptyAreaHtml_(0, area_from.can_replace_, from)); + } + return CaculateCombScore_(); + } + + std::pair SeqFill(const AreaCard& card) + { + for (uint32_t i = 1; i < areas_.size(); ++i) { + auto& area = areas_[i]; + if (!area.is_wall_ && !area.card_.has_value()) { + const auto img_str = card.ToHtml(image_path_, area.can_replace_); + area.box_->SetContent(img_str); + area.card_ = card; + return {area.num_, CaculateCombScore_()}; + } + } + assert(false); + return {UINT32_MAX, CombScore{0, 0}}; // unexpected case + } + + bool IsAllFilled() const + { + for (uint32_t i = 0; i < areas_.size(); ++i) { + if (!areas_[i].is_wall_ && !areas_[i].card_.has_value()) { + return false; + } + } + return true; + } + + int32_t Score() const { return score_; } + int32_t ExtraScore() const { return extra_score_; } + + int32_t BreakLine() const { return breakline_count_; } + bool AirAllLine() const { return air_allline_; } + bool WildAll10() const { return wild_all10_; } + bool Length9() const { return length9_; } + + uint32_t IsWall(const uint32_t num) const { return areas_[numToid[num]].is_wall_; } + + bool IsFilled(const uint32_t num) const { return areas_[numToid[num]].card_.has_value(); } + + bool CanReplace(const uint32_t num) const { return (IsWall(num) != 1 && !IsFilled(num)) || areas_[numToid[num]].can_replace_; } + + std::string ToHtml() const { return table_.ToString(); } + + std::string GetInitTable_() const { return initial_table_; } + + std::string Image_(std::string name) const { return "![](file:///" + image_path_ + std::move(name) + ".png)"; } + + private: + static constexpr uint32_t k_size = 3 + 1; + static constexpr uint32_t k_max_row = k_size * 4 + 2; + static constexpr uint32_t k_max_column = k_size * 2 + 1; + + CombScore CaculateCombScore_() + { + breakline_count_ = 0; + air_allline_ = true; + wild_all10_ = true; + CombScore result = CaculateOneDirect_() + + CaculateOneDirect_() + + CaculateOneDirect_(); + int32_t point = result.score_ - score_; + int32_t extra_point = result.extra_score_ - extra_score_; + score_ = result.score_; + extra_score_ = result.extra_score_; + return CombScore{point, extra_point}; // return score change + } + + template + CombScore CaculateOneDirect_() + { + int32_t score = 0; + int32_t extra_score = 0; + for (int32_t i = 0; i < 11; i++) { + std::optional point; + int32_t count = 0; + bool connect = true; + for (int32_t j = 0; j < 9; j++) { + int32_t id = line[static_cast(direct)][i][j]; + if (id == -1) { // 到达地图边缘 + LineConnect(connect, point, count, score, extra_score); + break; + } + if (areas_[id].card_.has_value()) { // 砖块 + if (!point.has_value()) { // 第一块或墙块 + point = areas_[id].card_->Point(); + count++; + } else { + if (areas_[id].card_->IsMatch(*point)) { // 匹配 + count++; + if (*point == 10 && areas_[id].card_->Point() != 10) { // 癞子线出现普通砖块 + point = areas_[id].card_->Point(); + wild_all10_ = false; + } + if (*point != 10 && areas_[id].card_->Point() == 10) wild_all10_ = false; // 普通线出现癞子(成就失败) + } else { + connect = false; + if (*point == 0 || areas_[id].card_->Point() == 0) air_allline_ = false; // 0线被其他数字阻断(成就失败) + } + } + if (*point == 0 && connect == false) air_allline_ = false; + } else if (areas_[id].is_wall_) { // 墙块(结算当前分数) + LineConnect(connect, point, count, score, extra_score); + connect = true; + point = std::nullopt; + count = 0; + } else { // 空区域 + connect = false; + if (point.has_value()) { + if (*point == 0) air_allline_ = false; + if (*point == 10) wild_all10_ = false; + } + } + if (j == 8) { // 地图最长边 + LineConnect(connect, point, count, score, extra_score); + } + } + } + return CombScore{score, extra_score}; + } + + // 连线分数结算 + void LineConnect(const bool connect, const std::optional point, const int32_t count, int32_t& score, int32_t& extra_score) { + if (connect) { + if (!point.has_value()) { + return; + } + score += *point * count; + if (count >= 3) { + const double multiple[10] = {0, 0, 0, 0.3, 0.4, 0.4, 0.5, 0.5, 0.6, 0.6}; + extra_score += *point * count * multiple[count]; + } + if (count == 9) { length9_ = true; } + } else { + breakline_count_++; + } + } + + std::string EmptyAreaHtml_(const uint32_t is_wall, const bool can_replace, const uint32_t num) const + { + std::string div = "
"; + div += "

" + std::to_string(num) + "

"; + if (can_replace) div += ""; + div += "
"; + return div; + } + + void analysisMap_() + { + int wall = std::count(std::begin(maps[map_]), std::end(maps[map_]), 0); + int empty = 0; + for (int i = 0; i < k_total_block; i++) { + if (maps[map_][i] != 0) { + numToid.push_back(i); + areas_.push_back(++wall); + } else { + numToid.insert(numToid.begin() + empty, i); + areas_.push_back(++empty); + } + } + numToid.insert(numToid.begin(), -1); + } + + const std::string image_path_; + const uint32_t map_; + html::Table table_; + std::string initial_table_; + std::vector areas_; + std::vector numToid; + + int32_t score_ = 0; + int32_t extra_score_ = 0; + + int32_t breakline_count_ = 0; + bool air_allline_ = true; + bool wild_all10_ = true; + bool length9_ = false; +}; diff --git a/games/opencomb/option.cmake b/games/opencomb/option.cmake new file mode 100644 index 00000000..e69de29b diff --git a/games/opencomb/options.h b/games/opencomb/options.h new file mode 100644 index 00000000..f472454b --- /dev/null +++ b/games/opencomb/options.h @@ -0,0 +1,9 @@ +EXTEND_OPTION("每回合超时时间x秒", 局时, (ArithChecker(10, 3600, "局时(秒)")), 120) +EXTEND_OPTION("随机种子(关联卡池&道具,配置需一致)", 种子, (AnyArg("种子", "我是随便输入的一个字符串")), "") +EXTEND_OPTION("游戏卡池:决定卡池中砖块的种类", 卡池, AlterChecker({{"经典", 0}, {"癞子", 1}, {"空气", 2}, {"混乱", 3}}), 0) +EXTEND_OPTION("游戏模式:游戏采用的基础放置和对战规则", 模式, AlterChecker({{"传统", 0}, {"云顶", 1}}), 0) +EXTEND_OPTION("开启连线额外得分(长度3及以上时获得额外分数)", 连线奖励, (BoolChecker("开启", "关闭")), true) +EXTEND_OPTION("游戏道具:设置是否在卡池中添加特殊道具", 道具, (BoolChecker("开启", "关闭")), true) +EXTEND_OPTION("游戏地图", 地图, AlterChecker( + {{"随机", 0}, {"经典", 1}, {"环巢", 2}, {"漩涡", 3}, {"飞机", 4}, {"面具", 5}, {"塔楼", 6}, {"三叶草", 7}} +), 1) \ No newline at end of file diff --git a/games/opencomb/resource/0l.png b/games/opencomb/resource/0l.png new file mode 100644 index 00000000..fee460a2 Binary files /dev/null and b/games/opencomb/resource/0l.png differ diff --git a/games/opencomb/resource/0r.png b/games/opencomb/resource/0r.png new file mode 100644 index 00000000..73a3d3d6 Binary files /dev/null and b/games/opencomb/resource/0r.png differ diff --git a/games/opencomb/resource/0v.png b/games/opencomb/resource/0v.png new file mode 100644 index 00000000..d1cfe6b0 Binary files /dev/null and b/games/opencomb/resource/0v.png differ diff --git a/games/opencomb/resource/1.png b/games/opencomb/resource/1.png new file mode 100644 index 00000000..21d54a67 Binary files /dev/null and b/games/opencomb/resource/1.png differ diff --git a/games/opencomb/resource/2.png b/games/opencomb/resource/2.png new file mode 100644 index 00000000..1b2719d8 Binary files /dev/null and b/games/opencomb/resource/2.png differ diff --git a/games/opencomb/resource/3.png b/games/opencomb/resource/3.png new file mode 100644 index 00000000..4a377ef5 Binary files /dev/null and b/games/opencomb/resource/3.png differ diff --git a/games/opencomb/resource/4.png b/games/opencomb/resource/4.png new file mode 100644 index 00000000..b55f6fe9 Binary files /dev/null and b/games/opencomb/resource/4.png differ diff --git a/games/opencomb/resource/5.png b/games/opencomb/resource/5.png new file mode 100644 index 00000000..37230881 Binary files /dev/null and b/games/opencomb/resource/5.png differ diff --git a/games/opencomb/resource/6.png b/games/opencomb/resource/6.png new file mode 100644 index 00000000..22630fd2 Binary files /dev/null and b/games/opencomb/resource/6.png differ diff --git a/games/opencomb/resource/7.png b/games/opencomb/resource/7.png new file mode 100644 index 00000000..93db9172 Binary files /dev/null and b/games/opencomb/resource/7.png differ diff --git a/games/opencomb/resource/8.png b/games/opencomb/resource/8.png new file mode 100644 index 00000000..68eb6ede Binary files /dev/null and b/games/opencomb/resource/8.png differ diff --git a/games/opencomb/resource/9.png b/games/opencomb/resource/9.png new file mode 100644 index 00000000..0ff09fe5 Binary files /dev/null and b/games/opencomb/resource/9.png differ diff --git a/games/opencomb/resource/Xl.png b/games/opencomb/resource/Xl.png new file mode 100644 index 00000000..e68360e9 Binary files /dev/null and b/games/opencomb/resource/Xl.png differ diff --git a/games/opencomb/resource/Xr.png b/games/opencomb/resource/Xr.png new file mode 100644 index 00000000..6939c8d2 Binary files /dev/null and b/games/opencomb/resource/Xr.png differ diff --git a/games/opencomb/resource/Xv.png b/games/opencomb/resource/Xv.png new file mode 100644 index 00000000..996c0027 Binary files /dev/null and b/games/opencomb/resource/Xv.png differ diff --git a/games/opencomb/resource/arial.ttf b/games/opencomb/resource/arial.ttf new file mode 100644 index 00000000..8682d946 Binary files /dev/null and b/games/opencomb/resource/arial.ttf differ diff --git a/games/opencomb/resource/card.png b/games/opencomb/resource/card.png new file mode 100644 index 00000000..8d0b8562 Binary files /dev/null and b/games/opencomb/resource/card.png differ diff --git a/games/opencomb/resource/card_replace.png b/games/opencomb/resource/card_replace.png new file mode 100644 index 00000000..d2b30f8d Binary files /dev/null and b/games/opencomb/resource/card_replace.png differ diff --git a/games/opencomb/resource/erase.png b/games/opencomb/resource/erase.png new file mode 100644 index 00000000..262f41d3 Binary files /dev/null and b/games/opencomb/resource/erase.png differ diff --git a/games/opencomb/resource/move.png b/games/opencomb/resource/move.png new file mode 100644 index 00000000..c8aa057e Binary files /dev/null and b/games/opencomb/resource/move.png differ diff --git a/games/opencomb/resource/reshape.png b/games/opencomb/resource/reshape.png new file mode 100644 index 00000000..0f7abcc9 Binary files /dev/null and b/games/opencomb/resource/reshape.png differ diff --git a/games/opencomb/resource/wall.png b/games/opencomb/resource/wall.png new file mode 100644 index 00000000..a88ab0ee Binary files /dev/null and b/games/opencomb/resource/wall.png differ diff --git a/games/opencomb/resource/wall_broken.png b/games/opencomb/resource/wall_broken.png new file mode 100644 index 00000000..e983a422 Binary files /dev/null and b/games/opencomb/resource/wall_broken.png differ diff --git a/games/opencomb/resource/wall_full.png b/games/opencomb/resource/wall_full.png new file mode 100644 index 00000000..bd9509a2 Binary files /dev/null and b/games/opencomb/resource/wall_full.png differ diff --git a/games/opencomb/resource/wall_half.png b/games/opencomb/resource/wall_half.png new file mode 100644 index 00000000..bd9f3eb6 Binary files /dev/null and b/games/opencomb/resource/wall_half.png differ diff --git a/games/opencomb/rule.md b/games/opencomb/rule.md new file mode 100644 index 00000000..bd02a894 --- /dev/null +++ b/games/opencomb/rule.md @@ -0,0 +1,39 @@ +## 开放蜂巢 + +- **游戏人数:** 1~10 +- **原作:** 铁蛋 + +### 规则说明 +- 基础连线得分采用数字蜂巢规则:玩家每回合需要把一块数字砖块填入某个方格中,当某条竖线或者斜线均为一种数字即可得分,得分数量为该数字乘以直线长度。但如果一行内仅包含癞子线,得分为长度×10。 +- **连线奖励(默认开启):** 当连线长度达到一定长度时,可以获得额外分数(计算时分数向下取整) + + 长度为 3,额外获得 分数×0.3 + + 长度为 4 或 5,额外获得 分数×0.4 + + 长度为 6 或 7,额外获得 分数×0.5 + + 长度为 8 或 9,额外获得 分数×0.6 +- 游戏包含5种特殊特殊道具:**(所有特殊道具可以pass)** + 1. **墙块:** 放在任意空位,相同数字只要两边连着墙块就能得分。 + 2. **破碎墙块:** 放在任意空位,可被其他砖块覆盖,被覆盖前视作墙块。 + 3. **骷髅:** 消除一个墙块或砖块,可以直接清除外围墙块。 + 4. **移动:** 移动任意块至其他空位置,可以移动外围墙块,但只能移动到空位。 + 5. **重塑:** 可对任意位置使用,不同位置具有不同的效果: + + 对墙块:将墙块转化为破碎墙块,或将破碎墙块变回墙块 + + 对砖块:将此位置的砖块替换为癞子 + + 对空地:将此位置设为金色,金色区域可以被任意砖块**无限覆盖** +- **首轮必定为特殊道具(且不计算在卡池内)**,其他道具在后续回合中随机出现 +- 因墙块和移动道具的存在,游戏中会存在一条线被分成多部份的特殊情况。**但只要相同数字的连线两边都连着墙块,均可以得分**。具体见下方例子: + + 一条线的情况为“墙555墙99墙”,则这条线的得分为5×3+9×2=33分,5和9均可以得分 + + 一条线的情况为“墙38墙44X墙(X为癞子线)”,则这条线的得分为0+4×3=12分,仅4联通墙块可以得分 +- **※结束条件:当有一名玩家填满自己的板面时,游戏将立即结束!** + +### 卡池说明 +- 每个卡池中都包括2个墙块、2个破碎墙块、4个骷髅、3个移动、2个重塑。其他砖块分为以下几种模式: +- **【经典】** 仅使用原版砖块,每种2张且包含2个癞子 +- **【癞子】** 原版砖块每种缩减至1张,卡池中新增所有种类的单行癞子砖块每种1张(27张),普通癞子仍然为2张 +- **【空气】** 原版砖块每种缩减至1张,卡池中新增所有种类的单行空气砖块每种1张(27张),普通癞子仍然为2张 *(空气线即0数字线,不能得分)* +- **【混乱】** 卡池中新增所有种类的混乱砖块每种1张(24张),在原版砖块中**抽取10张**,在癞子线砖块中**抽取10张**,在空气线砖块中**抽取10张**,**同时包含两张000**,普通癞子仍然为2张 *(混乱砖块:同时包含癞子线和空气线)* + +### 「云顶」对战模式 +- 对战使用云顶之巢规则:每个人拥有200点血量(关闭连线奖励为150)。从第3回合起,正常轮时结束随机挑选一名对手(奇数名玩家时会有一位玩家对战镜像),比拼当前分数,较低者会减去分数之差的血量(镜像的血量不会影响玩家本体)。 +- 血量归零玩家淘汰,剩余1人时游戏结束。**最终得分为盘面分数,不计算淘汰顺序的影响** +- 砖块允许被新砖块或墙块覆盖,但是**普通墙块不允许覆盖**(金色区域除外) +- 第8、15、22等轮(回合数+7)会从“卡池2”选出等于(人数+1)枚棋子并且公示(卡池2不包含癞子和特殊道具,若卡池2已空,则重置卡池重新抽取),由血量低往高进行选择,如果血量相同,则分数更低的玩家先进行选择。如果分数相同,由系统随机选择顺序。 \ No newline at end of file diff --git a/games/opencomb/unittest.cc b/games/opencomb/unittest.cc new file mode 100644 index 00000000..79e7f5f5 --- /dev/null +++ b/games/opencomb/unittest.cc @@ -0,0 +1,375 @@ +// Copyright (c) 2018-present, JiaQi Yu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include "game_framework/unittest_base.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +GAME_TEST(1, one_player) +{ + ASSERT_PUB_MSG(OK, 0, "种子 123"); // start with erase + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(CHECKOUT, 0, "22"); // erase + ASSERT_PUB_MSG(CHECKOUT, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 0, "2"); + ASSERT_PUB_MSG(CHECKOUT, 0, "3"); + ASSERT_PUB_MSG(CHECKOUT, 0, "4"); + ASSERT_PUB_MSG(CHECKOUT, 0, "5"); + ASSERT_PUB_MSG(CHECKOUT, 0, "6"); + ASSERT_PUB_MSG(CHECKOUT, 0, "24 10"); // move + ASSERT_PUB_MSG(CHECKOUT, 0, "7"); + ASSERT_PUB_MSG(CHECKOUT, 0, "8"); + ASSERT_PUB_MSG(CHECKOUT, 0, "9"); + ASSERT_PUB_MSG(CHECKOUT, 0, "10"); // reshape + ASSERT_PUB_MSG(CHECKOUT, 0, "10"); // wall + ASSERT_PUB_MSG(CHECKOUT, 0, "11"); + ASSERT_PUB_MSG(CHECKOUT, 0, "12"); + ASSERT_PUB_MSG(CHECKOUT, 0, "13"); + ASSERT_PUB_MSG(CHECKOUT, 0, "14"); + ASSERT_PUB_MSG(CHECKOUT, 0, "15"); + ASSERT_PUB_MSG(CHECKOUT, 0, "16"); + ASSERT_PUB_MSG(CHECKOUT, 0, "17"); + ASSERT_PUB_MSG(CHECKOUT, 0, "18"); + ASSERT_PUB_MSG(CHECKOUT, 0, "19"); + ASSERT_PUB_MSG(CHECKOUT, 0, "20"); + ASSERT_PUB_MSG(CHECKOUT, 0, "22"); + ASSERT_PUB_MSG(CHECKOUT, 0, "24"); + ASSERT_SCORE(92); + ASSERT_ACHIEVEMENTS(0); +} + +GAME_TEST(2, two_player) +{ + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(OK, 0, "21"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "1"); + ASSERT_PUB_MSG(OK, 0, "2"); + ASSERT_PUB_MSG(CHECKOUT, 1, "2"); + ASSERT_PUB_MSG(OK, 0, "3"); + ASSERT_PUB_MSG(CHECKOUT, 1, "3"); + ASSERT_PUB_MSG(OK, 0, "4"); + ASSERT_PUB_MSG(CHECKOUT, 1, "4"); + ASSERT_PUB_MSG(OK, 0, "5"); + ASSERT_PUB_MSG(CHECKOUT, 1, "5"); + ASSERT_PUB_MSG(OK, 0, "6"); + ASSERT_PUB_MSG(CHECKOUT, 1, "6"); + ASSERT_PUB_MSG(OK, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 1, "pass"); + ASSERT_PUB_MSG(OK, 0, "7"); + ASSERT_PUB_MSG(CHECKOUT, 1, "7"); + ASSERT_PUB_MSG(OK, 0, "8"); + ASSERT_PUB_MSG(CHECKOUT, 1, "8"); + ASSERT_PUB_MSG(OK, 0, "9"); + ASSERT_PUB_MSG(CHECKOUT, 1, "9"); +} + +GAME_TEST(1, failed_selection) +{ + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(FAILED, 0, "10"); + ASSERT_PUB_MSG(CHECKOUT, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 0, "1"); + ASSERT_PUB_MSG(FAILED, 0, "1"); + ASSERT_PUB_MSG(FAILED, 0, "21"); +} + +GAME_TEST(2, timeout_eliminate) +{ + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(OK, 0, "pass"); + ASSERT_TIMEOUT(CHECKOUT); // pass special + ASSERT_PUB_MSG(OK, 1, "1"); + ASSERT_PUB_MSG(CHECKOUT, 0, "1"); + ASSERT_PUB_MSG(OK, 0, "2"); + ASSERT_TIMEOUT(CHECKOUT); + ASSERT_PUB_MSG(FAILED, 1, "2"); // is seq filled + ASSERT_PUB_MSG(OK, 1, "3"); + ASSERT_TIMEOUT(CHECKOUT); + ASSERT_PUB_MSG(FAILED, 0, "3"); // is seq filled + ASSERT_PUB_MSG(OK, 0, "4"); +} + +GAME_TEST(2, timeout_hook) +{ + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_TIMEOUT(CHECKOUT); + ASSERT_PUB_MSG(CHECKOUT, 0, "1"); // player 1 seq fill 1 + ASSERT_PUB_MSG(CHECKOUT, 0, "2"); // player 1 is hooked, seq fill 2 + ASSERT_PUB_MSG(FAILED, 1, "1"); // is seq filled + ASSERT_PUB_MSG(FAILED, 1, "2"); // is seq filled + ASSERT_PUB_MSG(OK, 1, "3"); +} + +GAME_TEST(2, yunding_two_player) +{ + ASSERT_PUB_MSG(OK, 0, "模式 云顶"); + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(OK, 0, "21"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + for (int i = 0; i < 5; i++) { + ASSERT_PUB_MSG(OK, 0, "2"); + ASSERT_PUB_MSG(CHECKOUT, 1, "2"); + } + ASSERT_PUB_MSG(OK, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 1, "pass"); + ASSERT_PUB_MSG(FAILED, 1, "2 7"); // SelectStage + ASSERT_PUB_MSG(CONTINUE, 0, "3 7"); + ASSERT_PUB_MSG(CHECKOUT, 1, "2 7"); + for (int i = 0; i < 4; i++) { + ASSERT_PUB_MSG(OK, 0, "8"); + ASSERT_PUB_MSG(CHECKOUT, 1, "8"); + } + ASSERT_PUB_MSG(OK, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 1, "pass"); + ASSERT_PUB_MSG(OK, 0, "8"); + ASSERT_PUB_MSG(CHECKOUT, 1, "8"); + ASSERT_SCORE(0, 23); + ASSERT_ACHIEVEMENTS(0); +} + +GAME_TEST(2, yunding_timeout_test) +{ + ASSERT_PUB_MSG(OK, 0, "模式 云顶"); + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(OK, 0, "21"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + ASSERT_PUB_MSG(OK, 0, "2"); + ASSERT_PUB_MSG(CHECKOUT, 1, "2"); + ASSERT_TIMEOUT(CHECKOUT); + ASSERT_PUB_MSG(CHECKOUT, 0, "3"); + ASSERT_PUB_MSG(FAILED, 1, "22"); +} + +GAME_TEST(2, yunding_leave_test1) +{ + ASSERT_PUB_MSG(OK, 0, "模式 云顶"); + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(OK, 0, "21"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + ASSERT_LEAVE(CONTINUE, 0); + ASSERT_PUB_MSG(CHECKOUT, 1, "1"); + for (int i = 0; i < 4; i++) { + ASSERT_PUB_MSG(CHECKOUT, 1, "1"); + } + ASSERT_PUB_MSG(CHECKOUT, 1, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 1, "2 1"); + for (int i = 0; i < 4; i++) { + ASSERT_PUB_MSG(CHECKOUT, 1, "8"); + } + ASSERT_PUB_MSG(CHECKOUT, 1, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 1, "8"); + ASSERT_SCORE(0, 23); +} + +GAME_TEST(2, yunding_leave_test2) +{ + ASSERT_PUB_MSG(OK, 0, "模式 云顶"); + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(OK, 0, "21"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + ASSERT_LEAVE(CONTINUE, 1); + ASSERT_PUB_MSG(CHECKOUT, 0, "1"); + for (int i = 0; i < 4; i++) { + ASSERT_PUB_MSG(CHECKOUT, 0, "1"); + } + ASSERT_PUB_MSG(CHECKOUT, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 0, "2 1"); + for (int i = 0; i < 4; i++) { + ASSERT_PUB_MSG(CHECKOUT, 0, "1"); + } + ASSERT_PUB_MSG(CHECKOUT, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 0, "1"); + ASSERT_SCORE(0, 23); +} + +GAME_TEST(2, yunding_SelectStage_leave_test1) +{ + ASSERT_PUB_MSG(OK, 0, "模式 云顶"); + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(OK, 0, "21"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + for (int i = 0; i < 5; i++) { + ASSERT_PUB_MSG(OK, 0, "2"); + ASSERT_PUB_MSG(CHECKOUT, 1, "2"); + } + ASSERT_PUB_MSG(OK, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 1, "pass"); + ASSERT_LEAVE(CONTINUE, 1); // SelectStage + ASSERT_PUB_MSG(CHECKOUT, 0, "1 7"); + for (int i = 0; i < 4; i++) { + ASSERT_PUB_MSG(CHECKOUT, 0, "1"); + } + ASSERT_PUB_MSG(CHECKOUT, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 0, "1"); + ASSERT_SCORE(0, 23); +} + +GAME_TEST(2, yunding_SelectStage_leave_test2) +{ + ASSERT_PUB_MSG(OK, 0, "模式 云顶"); + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(OK, 0, "21"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + for (int i = 0; i < 5; i++) { + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "1"); + } + ASSERT_PUB_MSG(OK, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 1, "pass"); + ASSERT_LEAVE(CONTINUE, 0); // SelectStage + ASSERT_PUB_MSG(FAILED, 1, "3 7"); + ASSERT_PUB_MSG(CHECKOUT, 1, "1 7"); + for (int i = 0; i < 4; i++) { + ASSERT_PUB_MSG(CHECKOUT, 1, "1"); + } + ASSERT_PUB_MSG(CHECKOUT, 1, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 1, "1"); + ASSERT_SCORE(0, 23); +} + +GAME_TEST(2, yunding_SelectStage_leave_test3) +{ + ASSERT_PUB_MSG(OK, 0, "模式 云顶"); + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(OK, 0, "21"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + for (int i = 0; i < 5; i++) { + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "1"); + } + ASSERT_PUB_MSG(OK, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 1, "pass"); + ASSERT_PUB_MSG(CONTINUE, 0, "3 7"); // SelectStage + ASSERT_LEAVE(CONTINUE, 0); + ASSERT_PUB_MSG(CHECKOUT, 1, "2 7"); + for (int i = 0; i < 4; i++) { + ASSERT_PUB_MSG(CHECKOUT, 1, "8"); + } + ASSERT_PUB_MSG(CHECKOUT, 1, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 1, "8"); + ASSERT_SCORE(0, 23); +} + +GAME_TEST(3, yunding_SelectStage_leave_test4) +{ + ASSERT_PUB_MSG(OK, 0, "模式 云顶"); + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(OK, 0, "21"); + ASSERT_PUB_MSG(OK, 1, "21"); + ASSERT_PUB_MSG(CHECKOUT, 2, "21"); + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(OK, 1, "21"); + ASSERT_PUB_MSG(CHECKOUT, 2, "21"); + for (int i = 0; i < 5; i++) { + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(OK, 1, "1"); + ASSERT_PUB_MSG(CHECKOUT, 2, "1"); + } + ASSERT_PUB_MSG(OK, 0, "pass"); + ASSERT_PUB_MSG(OK, 1, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 2, "pass"); + ASSERT_LEAVE(CONTINUE, 1); // SelectStage + ASSERT_PUB_MSG(CONTINUE, 0, "4 7"); + ASSERT_PUB_MSG(CHECKOUT, 2, "2 7"); + ASSERT_PUB_MSG(OK, 0, "8"); + ASSERT_PUB_MSG(CHECKOUT, 2, "8"); + ASSERT_LEAVE(CONTINUE, 2); + for (int i = 0; i < 3; i++) { + ASSERT_PUB_MSG(CHECKOUT, 0, "9"); + } + ASSERT_PUB_MSG(CHECKOUT, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 0, "9"); + for (int i = 0; i < 21; i++) { + ASSERT_TIMEOUT(CHECKOUT); + } + ASSERT_SCORE(0, 89, 74); +} + +GAME_TEST(2, yunding_SelectStage_tiemout_test) +{ + ASSERT_PUB_MSG(OK, 0, "模式 云顶"); + ASSERT_PUB_MSG(OK, 0, "种子 123"); + ASSERT_PUB_MSG(OK, 0, "地图 经典"); + ASSERT_TRUE(StartGame()); + ASSERT_PUB_MSG(OK, 0, "21"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "21"); + for (int i = 0; i < 5; i++) { + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "1"); + } + ASSERT_PUB_MSG(OK, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 1, "pass"); + ASSERT_TIMEOUT(CONTINUE); // SelectStage + ASSERT_PUB_MSG(FAILED, 1, "3 7"); + ASSERT_PUB_MSG(CHECKOUT, 1, "1 7"); + for (int i = 0; i < 4; i++) { + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "1"); + } + ASSERT_PUB_MSG(OK, 0, "pass"); + ASSERT_PUB_MSG(CHECKOUT, 1, "pass"); + ASSERT_PUB_MSG(OK, 0, "1"); + ASSERT_PUB_MSG(CHECKOUT, 1, "1"); + ASSERT_SCORE(0, 23); +} + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // gamespace lgtbot + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + gflags::ParseCommandLineFlags(&argc, &argv, true); + return RUN_ALL_TESTS(); +} diff --git a/games/othello/mygame.cc b/games/othello/mygame.cc index 172174e3..d2a2e67d 100644 --- a/games/othello/mygame.cc +++ b/games/othello/mygame.cc @@ -24,12 +24,12 @@ const GameProperties k_properties { .developer_ = "森高", .description_ = "双方一起落子的奥赛罗游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 2; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 2; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 2) { reply() << "该游戏至少 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -40,7 +40,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; diff --git a/games/poker_squares/mygame.cc b/games/poker_squares/mygame.cc index 5406b03e..d84c2ca9 100644 --- a/games/poker_squares/mygame.cc +++ b/games/poker_squares/mygame.cc @@ -21,11 +21,11 @@ template using SubGameStage = StageFsm using MainGameStage = StageFsm; // 0 indicates no max-player limits -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } // The default score multiple for the game. The value of 0 denotes a testing game. // We recommend to increase the multiple by one for every 7~8 minutes the game lasts. -uint32_t Multiple(const MyGameOptions& options) { return GET_OPTION_VALUE(options, 种子).empty() ? 2 : 0; } +uint32_t Multiple(const CustomOptions& options) { return GET_OPTION_VALUE(options, 种子).empty() ? 2 : 0; } const GameProperties k_properties { .name_ = "扑克矩阵", .developer_ = "森高", @@ -40,7 +40,7 @@ const std::vector k_rule_commands = {}; // The commands for initialize the options when starting a game by "#新游戏 ..." const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 1; return NewGameMode::SINGLE_USER; @@ -50,7 +50,7 @@ const std::vector k_init_options_commands = { // The function is invoked before a game starts. You can make final adaption for the options. // The return value of false denotes failure to start a game. -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { return true; } diff --git a/games/quixo/mygame.cc b/games/quixo/mygame.cc index 71a2371e..094900a1 100644 --- a/games/quixo/mygame.cc +++ b/games/quixo/mygame.cc @@ -28,12 +28,12 @@ const GameProperties k_properties { .developer_ = "森高", .description_ = "通过取出并重新放入棋子,先连成五子者获胜的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) { return 2; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return 2; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为" << generic_options_readonly.PlayerNum(); @@ -44,7 +44,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; diff --git a/games/rainbow_treasure_hunter/achievements.h b/games/rainbow_treasure_hunter/achievements.h new file mode 100644 index 00000000..e69de29b diff --git a/games/rainbow_treasure_hunter/board.h b/games/rainbow_treasure_hunter/board.h new file mode 100644 index 00000000..f31ef6ff --- /dev/null +++ b/games/rainbow_treasure_hunter/board.h @@ -0,0 +1,742 @@ + +#include +#include + +struct Map { + int size; + int treasure; + int bomb; + int special; + int hp; + int limit; +}; +const Map maps[] = { + { 6, 6, 4, 1, 1, 4 }, // 小地图 + { 9, 9, 12, 2, 2, 6 }, // 中地图 + { 12, 18, 18, 4, 2, 9 }, // 大地图 + { 15, 36, 24, 6, 2, 12 }, // 特大地图 +}; + +struct ItemNumOption { + int treasure; + int bomb; + int humidifier = 0; + int ink = 0; + + int operator[](size_t idx) const + { + switch (idx) { + case 0: return treasure; + case 1: return bomb; + case 2: return humidifier; + case 3: return ink; + default: return -1; + } + } +}; + + +class Player +{ + public: + Player(const string &name, const string &avatar, const int &hp) : name(name), avatar(avatar), hp(hp) {} + + const string name; + const string avatar; + + int hp; + int score = 0; + int change_hp = 0; + int change_score = 0; + + bool has_dug = false; + pair current; +}; + + +const int k_color_count = 6; +const int k_item_count = 4; + +enum class GridType { + R, B, Y, O, G, P, + TREASURE, + BOMB, + HUMIDIFIER, + INK, +}; + +const vector GridTypeNames = { + "R", "B", "Y", "O", "G", "P", + "", + "✹", + "♨", + "⚗", +}; +string toString(GridType type) +{ + int index = static_cast(type); + if (index >= 0 && index < GridTypeNames.size()) { + return GridTypeNames[index]; + } + return "UNKNOWN"; +} + +class Grid +{ + public: + string GetClasses(const bool show_all, const bool stain = false) const + { + if (!show && !show_all) return "hidden"; + + string base = dig_count > 0 ? "current" : (extra_mark ? "extra" : "show"); + if (!IsEmpty()) { + return base; + } else if (stain && !show_all) { + return base + " stain"; + } else { + return base + " color-" + toString(type); + } + } + + string GetContent(const bool show_all, const bool stain = false) const + { + if (!show && !show_all) return ""; + + if (show_all && type == GridType::HUMIDIFIER) { + return + "
" + "" + toString(type) + "" + "" + to_string(humidifier_add) + "" + "
"; + } else if (!IsEmpty()) { + return toString(type); + } else if (stain && !show_all) { + return "▒▒"; + } else { + return toString(type) + to_string(num); + } + } + + bool IsEmpty() const { return static_cast(type) < k_color_count; } + + private: + GridType type; + int num = 0; + int humidifier_add = 0; + + bool show = false; + int dig_count = 0; + bool extra_mark = false; + + friend class Board; +}; + + +class Board +{ + public: + Board(string ResourceDir, const int playerNum) : resource_path_(std::move(ResourceDir)), playerNum(playerNum) {} + // 图片资源文件夹 + const string resource_path_; + + // 地图初始配置 + int size; + // 玩家 + const int playerNum; + vector players; + // 地图 + vector> grid_map; + + // 道具模式 + int item_mode; + + // 初始化地图 + void Initialize( + const uint32_t map_option, + const vector& weights, + const int32_t item_mode, + const int32_t size_option, + const ItemNumOption& options + ) { + ItemNumOption init_options; + this->item_mode = item_mode; + size = size_option >= 0 ? size_option : maps[map_option].size; + init_options.treasure = (options.treasure >= 0 ? options.treasure : maps[map_option].treasure); + init_options.bomb = (options.bomb >= 0 ? options.bomb : maps[map_option].bomb); + + if (item_mode == 1) { + std::vector items = { + { GridType::HUMIDIFIER, options.humidifier }, + { GridType::INK, options.ink }, + }; + AssignItemCounts(items, maps[map_option].special); + init_options.humidifier = items[0].actual; + init_options.ink = items[1].actual; + } + + grid_map.resize(size); + for (int i = 0; i < size; i++) { + grid_map[i].resize(size); + } + GenerateRandomMap(weights, init_options); + } + + // 获取玩家和地图的markdown字符串 + string GetMarkdown(const int round, const bool show_all = false) const + { + return GetHeadTable(round) + GetBoard(show_all) + GetButtomTable(); + } + + // 获取顶部表格 + string GetHeadTable(const int round) const + { + html::Table headTable(playerNum + 1, 7); + headTable.SetTableStyle("align=\"center\" cellpadding=\"2\""); + headTable.SetRowStyle(0, "bgcolor=\"E7E7E7\" style=\"text-align:center; font-size:12px; font-weight:bold;\""); + headTable.MergeRight(0, 0, 2); + headTable.Get(0, 0).SetStyle("style=\"width:280px;\"").SetContent("玩家"); + headTable.Get(0, 2).SetStyle("style=\"width:80px;\"").SetContent(HTML_COLOR_FONT_HEADER(#BD8900) "【总得分】" HTML_FONT_TAIL); + headTable.MergeRight(0, 3, 2); + headTable.Get(0, 3).SetStyle("style=\"width:32px;\"").SetContent("生命值"); + headTable.Get(0, 5).SetStyle("style=\"width:40px;\"").SetContent("选择"); + headTable.Get(0, 6).SetStyle("style=\"width:50px;\"").SetContent("状态变化"); + for (int pid = 0; pid < playerNum; pid++) { + const Player& p = players[pid]; + headTable.Get(pid + 1, 0).SetStyle("style=\"width:40px;\"").SetContent(p.avatar); + headTable.Get(pid + 1, 1).SetStyle("style=\"width:240px; text-align:left;\"").SetContent(HTML_SIZE_FONT_HEADER(2) + p.name + HTML_FONT_TAIL); + headTable.Get(pid + 1, 2).SetStyle("style=\"width:80px;\"").SetContent(HTML_COLOR_FONT_HEADER(#BD8900) "" + to_string(p.score) + "" HTML_FONT_TAIL); + // 生命值 + if (p.hp > 0) { + headTable.Get(pid + 1, 3).SetStyle("style=\"width:20px; text-align:right;\"").SetContent(""); + headTable.Get(pid + 1, 4).SetStyle("style=\"width:12px; text-align:left;\"").SetContent(HTML_COLOR_FONT_HEADER(#EC4646) + to_string(p.hp) + HTML_FONT_TAIL); + } else { + headTable.MergeRight(pid + 1, 3, 2); + headTable.Get(pid + 1, 3).SetStyle("style=\"width:32px;\"").SetColor("#E5E5E5").SetContent("淘汰"); + } + headTable.Get(pid + 1, 5).SetStyle("style=\"width:40px;\"").SetContent(p.has_dug ? string(1, 'A' + p.current.first) + to_string(p.current.second + 1) : "--"); + // 状态变化 + auto& cell = headTable.Get(pid + 1, 6); + cell.SetStyle("style=\"width:50px;\""); + if (p.change_hp != 0) { + bool isPositive = p.change_hp > 0; + string color = isPositive ? "#2E7D32" : "#C62828"; + string value = (isPositive ? "+" : "") + to_string(p.change_hp); + string image = ""; + string html = "" + value + image + ""; + cell.SetColor(isPositive ? "#D6F5D6" : "#FAD6D6").SetContent(html); + } else if (p.change_score != 0) { + bool isPositive = p.change_score > 0; + string color = isPositive ? "#2E7D32" : "#C62828"; + string value = (isPositive ? "+" : "") + to_string(p.change_score); + string html = "" + value + ""; + cell.SetColor(isPositive ? "#D6F5D6" : "#FAD6D6").SetContent(html); + } + } + return "### 第 " + to_string(round) + " 回合" + headTable.ToString(); + } + + // 获取地图html + string GetBoard(bool show_all = false) const + { + html::Table map(size + 2, size + 2); + map.SetTableStyle("align=\"center\" style=\"border-collapse: collapse;\""); + // 坐标 + for (int i = 1; i < size + 1; i++) { + map.Get(i, 0).SetStyle("class=\"pos\"").SetContent(char('A' + i - 1)); + map.Get(i, size + 1).SetStyle("class=\"pos\"").SetContent(char('A' + i - 1)); + map.Get(0, i).SetStyle("class=\"pos\"").SetContent(to_string(i)); + map.Get(size + 1, i).SetStyle("class=\"pos\"").SetContent(to_string(i)); + } + // 方格 + for (int x = 0; x < size; x++) { + for (int y = 0; y < size; y++) { + const Grid grid = grid_map[x][y]; + bool stain = CountSurroundingHiddenInks(x, y) > 0; + map.Get(x + 1, y + 1).SetStyle("class=\"grid " + grid.GetClasses(show_all, stain) + "\"").SetContent(grid.GetContent(show_all, stain)); + } + } + return style + map.ToString(); + } + + // 获取底部图例 + string GetButtomTable() const + { + int table_rows = item_mode ? k_color_count + k_item_count - 2 : k_color_count; + html::Table buttomTable(table_rows, 4); + buttomTable.SetTableStyle("align=\"center\" style=\"font-size:12px;\""); + + struct InfoColor { + string fontColor; + string bgColor; + string label; + string description; + }; + const InfoColor infoColor[] = { + { "#C91D32", "#FADADE", "R 红色", "统计周围 8 格的炸弹总数" }, + { "#0070C0", "#D9E1F4", "B 蓝色", "统计本行本列的炸弹总数" }, + { "#BD8900", "#FFF3CA", "Y 黄色", "统计周围 8 格的宝藏总数" }, + { "#C55C10", "#F9CBAA", "O 橙色", "统计周围 8 格的宝藏和炸弹总数" }, + { "#588E31", "#E3F2D9", "G 绿色", "统计周围 8 格以及本行本列的宝藏总数(不重复计算)" }, + { "#7030A0", "#F6CCFF", "P 紫色", "统计周围 8 格以及本行本列的炸弹总数(不重复计算)" }, + }; + int row = 0; + for (int i = 0; i < k_color_count; i++) { + string html = "" + + "" + infoColor[i].label + ":" + + infoColor[i].description + ""; + buttomTable.Get(row++, 0).SetStyle("style=\"width:350px; text-align:left\"").SetContent(html); + } + if (item_mode > 0) { + string special[] = { + "♨ 加湿器:周围8格的颜色会增加数值 1-3,且增加数值相同。", + "⚗ 墨水瓶:挖到的人平分 5 分。周围8格的颜色会被污渍(▒)覆盖,道具不受污渍影响;当墨水瓶被挖走污渍消失。", + }; + buttomTable.Get(row++, 0).SetStyle("style=\"width:350px; text-align:left\"").SetContent(special[0]); + buttomTable.Get(row++, 0).SetStyle("style=\"width:350px; text-align:left\"").SetContent(special[1]); + } + + buttomTable.Get(0, 1).SetStyle("style=\"width:30px;\"").SetContent("图例"); + buttomTable.Get(0, 2).SetStyle("style=\"width:80px;\"").SetContent("名称"); + buttomTable.Get(0, 3).SetStyle("style=\"width:60px;\"").SetContent("剩余数量"); + struct InfoItem { + string name; + GridType type; + }; + const InfoItem infoItem[] = { + { "宝藏", GridType::TREASURE }, + { "炸弹", GridType::BOMB }, + { "加湿器", GridType::HUMIDIFIER }, + { "墨水瓶", GridType::INK }, + }; + row = 1; + for (int i = 0; i < (item_mode ? k_item_count : 2); i++) { + buttomTable.Get(row, 1).SetStyle("style=\"width:30px;\"").SetContent(toString(infoItem[i].type)); + buttomTable.Get(row, 2).SetStyle("style=\"width:80px;\"").SetContent(infoItem[i].name); + buttomTable.Get(row, 3).SetStyle("style=\"width:60px;\"").SetContent(to_string(countLeftGridType(infoItem[i].type))); + row++; + } + buttomTable.Get(row, 1).SetStyle("style=\"width:30px;\"").SetContent("
"); + buttomTable.MergeRight(row, 2, 2); + buttomTable.Get(row, 2).SetStyle("style=\"width:140px;\"").SetContent("本回合挖掘位置"); + row++; + if (item_mode == 1) { + buttomTable.Get(row, 1).SetStyle("style=\"width:30px;\"").SetContent("
"); + buttomTable.MergeRight(row, 2, 2); + buttomTable.Get(row, 2).SetStyle("style=\"width:140px;\"").SetContent("本回合额外信息"); + row++; + } + + return buttomTable.ToString(); + } + + // 统计某一个类型的剩余数量 + int countLeftGridType(const GridType type) const + { + int count = 0; + for (int x = 0; x < size; x++) + for (int y = 0; y < size; y++) + if (grid_map[x][y].type == type && !grid_map[x][y].show) + count++; + return count; + } + + // 检查坐标是否合法 + static string CheckCoordinate(string &s) + { + // 长度必须为2 + if (s.length() != 2 && s.length() != 3) { + return "[错误] 输入的坐标长度只能为 2 或 3,如:A1"; + } + // 大小写不敏感 + if (s[0] <= 'z' && s[0] >= 'a') { + s[0] = s[0] - 'a' + 'A'; + } + // 检查是否为合法输入 + if (s[0] > 'Z' || s[0] < 'A' || s[1] > '9' || s[1] < '0') { + return "[错误] 请输入合法的坐标(字母+数字),如:A1"; + } + if (s.length() == 3 && (s[2] > '9' || s[2] < '0')) { + return "[错误] 请输入合法的坐标(字母+数字),如:A1"; + } + return "OK"; + } + + // 将字符串转为一个位置pair。必须确保字符串是合法的再执行这个操作。 + static pair TranString(string s) + { + int nowX = s[0] - 'A', nowY = s[1] - '0' - 1; + if (s.length() == 3) + { + nowY = (s[1] - '0') * 10 + s[2] - '0' - 1; + } + pair ret; + ret.first = nowX; + ret.second = nowY; + return ret; + } + + // 玩家行动 + pair PlayerAction(const PlayerID pid, string s, const int round) + { + string result = CheckCoordinate(s); + if (result != "OK") return make_pair(false, result); + const auto [X, Y] = TranString(s); + + if (X < 0 || X > size - 1 || Y < 0 || Y > size - 1) { + return make_pair(false, "[错误] 挖掘位置超出了地图的范围"); + } + + return Action(pid, X, Y, round); + } + + pair Action(const PlayerID pid, const int X, const int Y, const int round) { + Player& p = players[pid]; + bool stain = CountSurroundingHiddenInks(X, Y) > 0; + Grid& grid = grid_map[X][Y]; + if (grid.show) { + switch (grid.type) { + case GridType::TREASURE: return {false, "[错误] 想再挖一次就能多一份宝藏?可没那么简单。试试其他位置吧~"}; + case GridType::BOMB: return {false, "[错误] 这里已经炸成渣了,再挖也只剩焦土。试试其他位置吧~"}; + case GridType::HUMIDIFIER: return {false, "[错误] 加湿器已经在这里努力工作了。换个地方试试吧~"}; + case GridType::INK: return {false, "[错误] 墨水瓶已经被回收过了,这里已经空无一物。去别处看看吧~"}; + default: return {false, "[错误] 这片区域已经被别人翻过啦。试试其他位置吧~"}; + } + } + + grid.dig_count++; + p.has_dug = true; + p.current = {X, Y}; + string reply; + switch (grid.type) { + case GridType::TREASURE: + reply = stain + ? "你挖到了一坨污渍,污渍下面是……哇,是【宝藏】!" + : "你挖到到了一个【宝藏】!"; + break; + case GridType::BOMB: + reply = (stain + ? "你挖到了一坨污渍,污渍下面是……糟糕,【炸弹】爆炸了!" + : "糟糕!你挖到了【炸弹】!") + + string(round <= 2 ? "(前 2 回合炸弹不生效)" : ""); + if (round > 2) { p.hp -= 1; p.change_hp = -1; } + break; + case GridType::HUMIDIFIER: + reply = stain + ? "你挖到了一坨污渍,污渍下面是……居然是【加湿器】!显示为【" + to_string(grid.humidifier_add) + "档】" + : "你挖到了1个【加湿器】!地下怎么会有这个?!观察到是【" + to_string(grid.humidifier_add) + "档】"; + break; + case GridType::INK: + reply = stain + ? "你挖到了一坨污渍,污渍下面是……是……怎么是【墨水瓶】?!" + : "你挖到了1个【墨水瓶】!好心人,世界将因为你的善举更加洁净"; + InkSurroundingExtraMark(X, Y); + break; + default: + reply = stain + ? "你挖到了一坨污渍,下面什么也没有。还好探测器还能用,屏幕上显示【" + grid.GetContent(true) + "】" + : "你什么也没有挖到,探测器显示的结果为【" + grid.GetContent(true) + "】"; + } + return {true, reply}; + } + + void InkSurroundingExtraMark(const int x, const int y) + { + auto HasSurroundingHiddenInks = [&](int r, int c) -> bool { + for (int i = max(0, r - 1); i <= min(size - 1, r + 1); ++i) { + for (int j = max(0, c - 1); j <= min(size - 1, c + 1); ++j) { + if (i == r && j == c) continue; + if (grid_map[i][j].type == GridType::INK && !grid_map[i][j].show && grid_map[i][j].dig_count == 0) { + return true; + } + } + } + return false; + }; + for (int i = max(0, x - 1); i <= min(size - 1, x + 1); i++) { + for (int j = max(0, y - 1); j <= min(size - 1, y + 1); j++) { + if (grid_map[i][j].IsEmpty() && !HasSurroundingHiddenInks(i, j) && grid_map[i][j].dig_count == 0) { + grid_map[i][j].extra_mark = true; + } + } + } + } + + void ClearRoundStatus() + { + for (int x = 0; x < size; x++) { + for (int y = 0; y < size; y++) { + grid_map[x][y].dig_count = 0; + grid_map[x][y].extra_mark = false; + } + } + for (auto& player: players) { + player.has_dug = false; + player.change_hp = 0; + player.change_score = 0; + } + } + + void UpdatePlayerStatus() + { + for (int pid = 0; pid < playerNum; ++pid) { + Player& player = players[pid]; + if (!player.has_dug) continue; + + auto [X, Y] = player.current; + Grid& grid = grid_map[X][Y]; + grid.show = true; + + int score = 0; + switch (grid.type) { + case GridType::TREASURE: score = 60 / grid.dig_count; break; + case GridType::INK: score = 5 / grid.dig_count; break; + default:; + } + player.score += score; + player.change_score = score; + } + } + + int AliveCount() const + { + return std::count_if(players.begin(), players.end(), [](const Player& p) { + return p.hp > 0; + }); + } + + private: + // 特殊道具数量随机分配 + struct Item { + GridType type; + int specified; + int actual = 0; + }; + static void AssignItemCounts(vector& items, int total_count) + { + int specified_sum = 0, unspecified_cnt = 0; + for (auto& it : items) { + if (it.specified >= 0) specified_sum += it.specified; + else unspecified_cnt++; + } + int remaining = max(0, total_count - specified_sum); + vector parts(unspecified_cnt, 0); + if (unspecified_cnt > 0) { + vector cuts; + cuts.reserve(unspecified_cnt + 2); + cuts.push_back(0); + for (int i = 0; i < unspecified_cnt - 1; i++) { + cuts.push_back(rand() % (remaining + 1)); + } + cuts.push_back(remaining); + sort(cuts.begin(), cuts.end()); + for (int i = 0; i < unspecified_cnt; ++i) { + parts[i] = cuts[i + 1] - cuts[i]; + } + } + int idx = 0; + for (auto& it : items) { + if (it.specified >= 0) it.actual = it.specified; + else it.actual = parts[idx++]; + } + } + + // 随机生成地图 + void GenerateRandomMap(const vector& weights, const ItemNumOption& options) + { + for (int i = 0; i < k_item_count; i++) { + int placed = 0; + while (placed < options[i]) { + int x = rand() % size; + int y = rand() % size; + if (grid_map[x][y].IsEmpty()) { + GridType type = static_cast(k_color_count + i); + if (type == GridType::HUMIDIFIER) grid_map[x][y].humidifier_add = rand() % 3 + 1; + grid_map[x][y].type = type; + placed++; + } + } + } + + for (int x = 0; x < size; x++) { + for (int y = 0; y < size; y++) { + Grid& grid = grid_map[x][y]; + if (!grid.IsEmpty()) continue; + + std::random_device rd; + std::mt19937 gen(rd()); + std::discrete_distribution<> dist(weights.begin(), weights.end()); + GridType type = static_cast(dist(gen)); + + grid.type = type; + int num = CountHumidifierAdd(x, y); + + switch (type) { + case GridType::R: num += CountSurroundingBombs(x, y); break; + case GridType::B: num += CountRowColBombs(x, y); break; + case GridType::Y: num += CountSurroundingTreasures(x, y); break; + case GridType::O: num += CountSurroundingBombs(x, y) + CountSurroundingTreasures(x, y); break; + case GridType::G: num += CountRowColSurroundingTreasures(x, y); break; + case GridType::P: num += CountRowColSurroundingBombs(x, y); break; + default: num = -1; + } + grid.num = num; + } + } + } + + // 道具辅助计算 + int CountHumidifierAdd(const int r, const int c) const + { + int count = 0; + for (int i = max(0, r - 1); i <= min(size - 1, r + 1); i++) { + for (int j = max(0, c - 1); j <= min(size - 1, c + 1); j++) { + if (i == r && j == c) continue; + if (grid_map[i][j].type == GridType::HUMIDIFIER) count += grid_map[i][j].humidifier_add; + } + } + return count; + } + + int CountSurroundingHiddenInks(const int r, const int c) const + { + int count = 0; + for (int i = max(0, r - 1); i <= min(size - 1, r + 1); i++) { + for (int j = max(0, c - 1); j <= min(size - 1, c + 1); j++) { + if (i == r && j == c) continue; + if (grid_map[i][j].type == GridType::INK && !grid_map[i][j].show) count++; + } + } + return count; + } + + // 颜色辅助计算 + int CountSurroundingBombs(const int r, const int c) const + { + int count = 0; + for (int i = max(0, r - 1); i <= min(size - 1, r + 1); i++) { + for (int j = max(0, c - 1); j <= min(size - 1, c + 1); j++) { + if (i == r && j == c) continue; + if (grid_map[i][j].type == GridType::BOMB) count++; + } + } + return count; + } + + int CountRowColBombs(const int r, const int c) const + { + int count = 0; + for (int j = 0; j < size; j++) + if (grid_map[r][j].type == GridType::BOMB) count++; + for (int i = 0; i < size; i++) + if (grid_map[i][c].type == GridType::BOMB) count++; + if (grid_map[r][c].type == GridType::BOMB) count--; + return count; + } + + int CountSurroundingTreasures(const int r, const int c) const + { + int count = 0; + for (int i = max(0, r - 1); i <= min(size - 1, r + 1); i++) { + for (int j = max(0, c - 1); j <= min(size - 1, c + 1); j++) { + if (i == r && j == c) continue; + if (grid_map[i][j].type == GridType::TREASURE) count++; + } + } + return count; + } + + int CountRowColSurroundingBombs(const int r, const int c) const + { + set s; + for (int j = 0; j < size; j++) + if (grid_map[r][j].type == GridType::BOMB) + s.insert(to_string(r) + "," + to_string(j)); + for (int i = 0; i < size; i++) + if (grid_map[i][c].type == GridType::BOMB) + s.insert(to_string(i) + "," + to_string(c)); + for (int i = max(0, r - 1); i <= min(size - 1, r + 1); i++) { + for (int j = max(0, c - 1); j <= min(size - 1, c + 1); j++) { + if (i == r && j == c) continue; + if (grid_map[i][j].type == GridType::BOMB) + s.insert(to_string(i) + "," + to_string(j)); + } + } + return s.size(); + } + + int CountRowColSurroundingTreasures(const int r, const int c) const + { + set s; + for (int j = 0; j < size; j++) + if (grid_map[r][j].type == GridType::TREASURE) + s.insert(to_string(r) + "," + to_string(j)); + for (int i = 0; i < size; i++) + if (grid_map[i][c].type == GridType::TREASURE) + s.insert(to_string(i) + "," + to_string(c)); + for (int i = max(0, r - 1); i <= min(size - 1, r + 1); i++) { + for (int j = max(0, c - 1); j <= min(size - 1, c + 1); j++) { + if (i == r && j == c) continue; + if (grid_map[i][j].type == GridType::TREASURE) + s.insert(to_string(i) + "," + to_string(j)); + } + } + return s.size(); + } + + // 地图style + const string style = R"( + +)"; + +}; diff --git a/games/rainbow_treasure_hunter/icon.png b/games/rainbow_treasure_hunter/icon.png new file mode 100644 index 00000000..cda4ae76 Binary files /dev/null and b/games/rainbow_treasure_hunter/icon.png differ diff --git a/games/rainbow_treasure_hunter/mygame.cc b/games/rainbow_treasure_hunter/mygame.cc new file mode 100644 index 00000000..d6f34b71 --- /dev/null +++ b/games/rainbow_treasure_hunter/mygame.cc @@ -0,0 +1,345 @@ +// Copyright (c) 2018-present, JiaQi Yu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include + +#include "game_framework/stage.h" +#include "game_framework/util.h" +#include "utility/html.h" + +using namespace std; + +#include "board.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +class MainStage; +template using SubGameStage = StageFsm; +template using MainGameStage = StageFsm; +const GameProperties k_properties { + .name_ = "彩虹奇兵", // the game name which should be unique among all the games + .developer_ = "铁蛋", + .description_ = "挖宝游戏。推理宝藏、炸弹位置。分高获胜!", +}; +uint64_t MaxPlayerNum(const CustomOptions& options) { return 12; } +uint32_t Multiple(const CustomOptions& options) { return 1; } +const MutableGenericOptions k_default_generic_options{ + .is_formal_{false}, +}; +const std::vector k_rule_commands = {}; + +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +{ + uint32_t& map_option = GET_OPTION_VALUE(game_options, 地图); + const int32_t size_option = GET_OPTION_VALUE(game_options, 边长); + const int32_t treasure_option = GET_OPTION_VALUE(game_options, 宝藏); + const int32_t bomb_option = GET_OPTION_VALUE(game_options, 炸弹); + const int32_t humidifier_option = GET_OPTION_VALUE(game_options, 加湿器); + const int32_t ink_option = GET_OPTION_VALUE(game_options, 墨水瓶); + + if (size_option == -1) { + const char* map_names[] = {"中地图", "大地图", "特大地图"}; + uint32_t player_num = generic_options_readonly.PlayerNum(); + for (int i = 0; i < 3; i++) { + if (player_num > maps[i].limit && player_num <= maps[i + 1].limit && map_option < i + 1) { + map_option = i + 1; + reply() << "[警告] 玩家数 " << player_num << " 超出当前地图人数限制,自动将大小调整为 " << map_names[i]; + break; + } + } + } else { + reply() << "[警告] 本局游戏使用自定义边长配置,地图大小为 " << size_option << "*" << size_option; + } + + vector origin_proportion= {1, 1, 1, 1, 1, 1}; + vector &modified = GET_OPTION_VALUE(game_options, 配比); + if (modified != origin_proportion) { + if (modified.size() < 6) { + modified.insert(modified.end(), 6 - modified.size(), 0); + } else if (modified.size() > 6) { + modified.resize(6); + } + if (all_of(modified.begin(), modified.end(), [](int n) { return n == 0; })) { + reply() << "[错误] 无效配置:配比不能全部为 0,请修正配置!"; + return false; + } + reply() << "[警告] 本局6种颜色的配比非默认配置\n" + << "·当前随机权重为:\n" + << "R红(" << modified[0] << ")、B蓝(" << modified[1] << ")、Y黄(" << modified[2] << ")\n" + << "O橙(" << modified[3] << ")、G绿(" << modified[4] << ")、P紫(" << modified[5] << ")"; + } + + srand((unsigned int)time(NULL)); + const int32_t max_limit = size_option >= 0 ? size_option * size_option : maps[map_option].size * maps[map_option].size; + const int32_t treasure = treasure_option >= 0 ? treasure_option : maps[map_option].treasure; + const int32_t bomb = bomb_option >= 0 ? bomb_option : maps[map_option].bomb; + const int32_t humidifier = humidifier_option >= 0 ? humidifier_option : 0; + const int32_t ink = ink_option >= 0 ? ink_option : 0; + + int32_t total_special = 0; + if (GET_OPTION_VALUE(game_options, 道具) == 1) { + total_special = max(humidifier + ink, maps[map_option].special); + } + int32_t total_num = treasure + bomb + total_special; + if (total_num > max_limit) { + reply() << "[错误] 无效配置:当前地图大小最多可容纳非空地元素 " << max_limit << " 个,当前为:" << total_num << "(" << treasure << "+" << bomb << "+" << total_special << "),请修正配置!"; + return false; + } + + if (generic_options_readonly.PlayerNum() < 2) { + reply() << "该游戏至少 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); + return false; + } + return true; +} + +const std::vector k_init_options_commands = { + InitOptionsCommand("使用预设地图类型", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const uint32_t& map_option, const uint32_t& item_mode) { + GET_OPTION_VALUE(game_options, 地图) = map_option; + GET_OPTION_VALUE(game_options, 道具) = item_mode; + return NewGameMode::MULTIPLE_USERS; + }, + AlterChecker({{"小地图", 0}, {"中地图", 1}, {"大地图", 2}, {"特大地图", 3}}), + OptionalDefaultChecker>(0, map{{"经典", 0}, {"全部", 1}})), + InitOptionsCommand("自定义游戏配置(边长、生命、道具数量)", + [] ( + CustomOptions& game_options, + MutableGenericOptions& generic_options, + const int32_t& size_option, + const int32_t& hp_option, + const int32_t& treasure_option, + const int32_t& bomb_option, + const int32_t& humidifier_option, + const int32_t& ink_option + ) { + uint32_t& map_option = GET_OPTION_VALUE(game_options, 地图); + for (int i = 3; i >= 0; i--) { + if (size_option >= maps[i].size && map_option < i) { + map_option = i; + break; + } + } + GET_OPTION_VALUE(game_options, 边长) = size_option; + GET_OPTION_VALUE(game_options, 生命) = hp_option; + GET_OPTION_VALUE(game_options, 宝藏) = treasure_option; + GET_OPTION_VALUE(game_options, 炸弹) = bomb_option; + GET_OPTION_VALUE(game_options, 加湿器) = humidifier_option; + GET_OPTION_VALUE(game_options, 墨水瓶) = ink_option; + if (humidifier_option >= 0 || ink_option >= 0) { + GET_OPTION_VALUE(game_options, 道具) = 1; + } + return NewGameMode::MULTIPLE_USERS; + }, + ArithChecker(4, 20, "边长"), + OptionalDefaultChecker>(-1, 1, 10, "生命"), + OptionalDefaultChecker>(-1, 0, 200, "宝藏"), + OptionalDefaultChecker>(-1, 0, 200, "炸弹"), + OptionalDefaultChecker>(-1, 0, 200, "加湿器"), + OptionalDefaultChecker>(-1, 0, 200, "墨水瓶")), + InitOptionsCommand("独自一人开始游戏", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const uint32_t& map_option, const uint32_t& item_mode) + { + GET_OPTION_VALUE(game_options, 地图) = map_option; + GET_OPTION_VALUE(game_options, 道具) = item_mode; + generic_options.bench_computers_to_player_num_ = maps[map_option].limit; + return NewGameMode::SINGLE_USER; + }, + VoidChecker("单机"), + OptionalDefaultChecker>(1, map{{"小地图", 0}, {"中地图", 1}, {"大地图", 2}, {"特大地图", 3}}), + OptionalDefaultChecker>(0, map{{"经典", 0}, {"全部", 1}})), +}; + +// ========== GAME STAGES ========== + +class RoundStage; + +class MainStage : public MainGameStage +{ + public: + MainStage(StageUtility&& utility) + : StageFsm(std::move(utility), + MakeStageCommand(*this, "查看当前游戏进展情况", &MainStage::Status_, VoidChecker("赛况"))) + , round_(0) + , player_scores_(Global().PlayerNum(), 0) + , board(Global().ResourceDir(), Global().PlayerNum()) + {} + + virtual int64_t PlayerScore(const PlayerID pid) const override { return player_scores_[pid]; } + + std::vector player_scores_; + + // 回合数 + int round_; + // 地图 + Board board; + // 回合赛况 + string round_status_; + + private: + CompReqErrCode Status_(const PlayerID pid, const bool is_public, MsgSenderBase& reply){ + reply() << Markdown(round_status_, max(600, (board.size + 2) * 43)); + return StageErrCode::OK; + } + + void FirstStageFsm(SubStageFsmSetter setter) + { + srand((unsigned int)time(NULL)); + const int32_t hp_option = GAME_OPTION(生命) >= 0 ? GAME_OPTION(生命) : maps[GAME_OPTION(地图)].hp; + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + board.players.emplace_back(Global().PlayerName(pid), Global().PlayerAvatar(pid, 40), hp_option); + } + board.Initialize( + GAME_OPTION(地图), + GAME_OPTION(配比), + GAME_OPTION(道具), + GAME_OPTION(边长), + { + GAME_OPTION(宝藏), + GAME_OPTION(炸弹), + GAME_OPTION(加湿器), + GAME_OPTION(墨水瓶), + } + ); + + setter.Emplace(*this, ++round_); + } + + void NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) + { + if (board.countLeftGridType(GridType::TREASURE) > 0 && board.AliveCount() > 1) { + setter.Emplace(*this, ++round_); + return; + } + + Global().Boardcast() << Markdown(board.GetMarkdown(round_), max(600, (board.size + 2) * 43)); + board.ClearRoundStatus(); + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + player_scores_[pid] = board.players[pid].score; + } + + if (board.AliveCount() == 0) { + Global().Boardcast() << "所有玩家均被淘汰,游戏结束!"; + } else if (board.countLeftGridType(GridType::TREASURE) == 0) { + Global().Boardcast() << "地图内无剩余宝藏,游戏结束!"; + } else { + int left = board.countLeftGridType(GridType::TREASURE); + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (board.players[pid].hp > 0) { + player_scores_[pid] += left * 60; + } + } + Global().Boardcast() << "存活人数仅剩一人,游戏结束!剩余的 " << left << " 个宝藏将直接计入存活玩家的得分"; + } + Global().Boardcast() << Markdown(board.GetBoard(true), (board.size + 2) * 43); + } +}; + +class RoundStage : public SubGameStage<> +{ + public: + RoundStage(MainStage& main_stage, const uint64_t round) + : StageFsm(main_stage, "第 " + std::to_string(round) + " 回合" , + MakeStageCommand(*this, "选择坐标挖掘", &RoundStage::Action_, AnyArg("坐标", "A1"))) + {} + + virtual void OnStageBegin() override + { + Main().round_status_ = Main().board.GetMarkdown(Main().round_); + Global().Boardcast() << Markdown(Main().round_status_, max(600, (Main().board.size + 2) * 43)); + Main().board.ClearRoundStatus(); + + Global().Boardcast() << "【剩余宝藏】" << Main().board.countLeftGridType(GridType::TREASURE) << " 个\n" + << "请私信裁判选择坐标进行挖掘" << (Main().round_ <= 2 ? ",前 2 回合炸弹不生效。" : "。") + << "时限 " << GAME_OPTION(时限) << " 秒,超时未行动扣除生命值"; + Global().StartTimer(GAME_OPTION(时限)); + } + + private: + AtomReqErrCode Action_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const string str) + { + if (is_public) { + reply() << "[错误] 请私信裁判进行行动"; + return StageErrCode::FAILED; + } + if (Global().IsReady(pid)) { + reply() << "[错误] 本回合您已经行动完成"; + return StageErrCode::FAILED; + } + + auto [success, result] = Main().board.PlayerAction(pid, str, Main().round_); + reply() << result; + if (!success) return StageErrCode::FAILED; + + if (Main().board.players[pid].hp <= 0 && Main().board.AliveCount() > 0) { + Global().Eliminate(pid); + } + + return StageErrCode::READY; + } + + virtual CheckoutErrCode OnStageTimeout() override + { + return HandleStageOver(); + } + + virtual CheckoutErrCode OnPlayerLeave(const PlayerID pid) override + { + Main().board.players[pid].hp = 0; + return StageErrCode::CONTINUE; + } + + virtual CheckoutErrCode OnStageOver() override + { + return HandleStageOver(); + } + + CheckoutErrCode HandleStageOver() + { + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (!Main().board.players[pid].has_dug && Main().board.players[pid].hp > 0) { + Main().board.players[pid].hp -= 1; + Main().board.players[pid].change_hp = -1; + if (Main().board.players[pid].hp <= 0 && Main().board.AliveCount() > 0) { + Global().Eliminate(pid); + } + } + } + + Main().board.UpdatePlayerStatus(); + return StageErrCode::CHECKOUT; + } + + virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) override + { + if (Global().IsReady(pid)) { + return StageErrCode::OK; + } + pair pos; + int attempt = 0; + bool success; + do { + pos = { rand() % Main().board.size, rand() % Main().board.size }; + success = Main().board.Action(pid, pos.first, pos.second, Main().round_).first; + } while (!success && ++attempt <= 500); + + if (Main().board.players[pid].hp <= 0 && Main().board.AliveCount() > 0) { + Global().Eliminate(pid); + } + return StageErrCode::READY; + } +}; + +auto* MakeMainStage(MainStageFactory factory) { return factory.Create(); } + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + diff --git a/games/rainbow_treasure_hunter/option.cmake b/games/rainbow_treasure_hunter/option.cmake new file mode 100644 index 00000000..e69de29b diff --git a/games/rainbow_treasure_hunter/options.h b/games/rainbow_treasure_hunter/options.h new file mode 100644 index 00000000..22c785d0 --- /dev/null +++ b/games/rainbow_treasure_hunter/options.h @@ -0,0 +1,17 @@ +EXTEND_OPTION("每回合的时间限制", 时限, (ArithChecker(30, 3600, "超时时间(秒)")), 150) +EXTEND_OPTION("【配置游戏地图类型】
" + "[小] 6\\*6面积 6宝藏 4炸弹,1生命,最多 4人
" + "[中] 9\\*9面积 9宝藏 12炸弹,2生命,最多 6人
" + "[大] 12\\*12面积 18宝藏 18炸弹,2生命,最多 9人
" + "[特大] 15\\*15面积 36宝藏 24炸弹,2生命,最多 12人", 地图, + AlterChecker({{"小", 0}, {"中", 1}, {"大", 2}, {"特大", 3}}), 0) +EXTEND_OPTION("配置游戏的道具模式", 道具, AlterChecker({{"经典", 0}, {"全部", 1}}), 0) +EXTEND_OPTION("自定义边长(默认根据地图类型变化)", 边长, (ArithChecker(4, 20, "边长")), -1) +EXTEND_OPTION("自定义生命值(默认根据地图类型变化)", 生命, (ArithChecker(1, 10, "生命")), -1) +EXTEND_OPTION("自定义宝藏 数量(默认根据地图类型变化)", 宝藏, (ArithChecker(0, 200, "数量")), -1) +EXTEND_OPTION("自定义炸弹 ✹ 数量(默认根据地图类型变化)", 炸弹, (ArithChecker(0, 200, "数量")), -1) +EXTEND_OPTION("自定义加湿器 ♨ 数量(默认根据地图类型变化)", 加湿器, (ArithChecker(0, 200, "数量")), -1) +EXTEND_OPTION("自定义墨水瓶 ⚗ 数量(默认根据地图类型变化)", 墨水瓶, (ArithChecker(0, 200, "数量")), -1) +EXTEND_OPTION("自定义6种颜色的随机概率占比(如不足6项自动补零)
" + "【配置顺序】R红、B蓝、Y黄、O橙、G绿、P紫", 配比, + (RepeatableChecker>("权重", "1 1 1 1 1 1")), (std::vector{1, 1, 1, 1, 1, 1})) \ No newline at end of file diff --git a/games/rainbow_treasure_hunter/resource/QisiJoanna.ttf b/games/rainbow_treasure_hunter/resource/QisiJoanna.ttf new file mode 100644 index 00000000..35b5d9ed Binary files /dev/null and b/games/rainbow_treasure_hunter/resource/QisiJoanna.ttf differ diff --git a/games/rainbow_treasure_hunter/resource/hp.png b/games/rainbow_treasure_hunter/resource/hp.png new file mode 100644 index 00000000..3784f53c Binary files /dev/null and b/games/rainbow_treasure_hunter/resource/hp.png differ diff --git a/games/rainbow_treasure_hunter/rule.md b/games/rainbow_treasure_hunter/rule.md new file mode 100644 index 00000000..00bacf1b --- /dev/null +++ b/games/rainbow_treasure_hunter/rule.md @@ -0,0 +1,40 @@ +## 彩虹奇兵 + +- **游戏人数:** 2-12 +- **原作:** 大萝卜姬 + +### 游戏简介 +- 玩家将在一张随机生成的 9×9 地图上进行挖掘。地图中隐藏着 9 个宝藏 和 12 个炸弹。通过分析地图上已知的线索信息,推理出宝藏与炸弹的位置。游戏结束时,**得分最高的玩家获胜!** +- 不同大小地图的具体配置见下方所示: + + 【小地图】6*6面积 6宝藏 4炸弹,1生命,最多 4 人 + + 【中地图】9*9面积 9宝藏 12炸弹,2生命,最多 6 人 + + 【大地图】12*12面积 18宝藏 18炸弹,2生命,最多 9 人 + + 【特大地图】15*15面积 36宝藏 24炸弹,2生命,最多 12 人 + +### 游戏流程 +- 玩家每回合挖掘一个格子(**不可pass**),挖掘的结果公开展示。 + + 每个宝藏60分,如果多人挖掘同一个宝藏,则挖到的人平分宝藏。 + + 挖到炸弹时减少 1 点生命值,**生命为 0 则淘汰,且得分保留**
+ (前 2 回合炸弹不生效) +- 挖掘到空格时会出现颜色和数字,二者遵循以下规则: + + R 红色:统计周围 8 格的炸弹总数 + + B 蓝色:统计本行本列的炸弹总数 + + Y 黄色:统计周围 8 格的宝藏总数 + + O 橙色:统计周围 8 格的宝藏和炸弹总数 + + G 绿色:统计周围 8 格以及本行本列的宝藏总数(不重复计算) + + P 紫色:统计周围 8 格以及本行本列的炸弹总数(不重复计算) +- 当所有宝藏被挖掘,或存活玩家不超过一人时,游戏结束。**地图上剩余的宝藏将直接计入存活玩家的得分**,最终根据各玩家总得分确定排名 + +### 额外道具 +- ♨ 加湿器:周围8格的颜色会增加数值 1-3,且增加数值相同。 +- ⚗ 墨水瓶:**挖到的人平分 5 分。** 周围8格的颜色会被污渍(▒)覆盖,道具不受污渍影响;当墨水瓶被挖走污渍消失。 + + \ No newline at end of file diff --git a/games/rainbow_treasure_hunter/unittest.cc b/games/rainbow_treasure_hunter/unittest.cc new file mode 100644 index 00000000..2c27b88e --- /dev/null +++ b/games/rainbow_treasure_hunter/unittest.cc @@ -0,0 +1,29 @@ +// Copyright (c) 2018-present, JiaQi Yu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include "game_framework/unittest_base.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +GAME_TEST(1, player_not_enough) +{ + ASSERT_FALSE(StartGame()); +} + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + gflags::ParseCommandLineFlags(&argc, &argv, true); + return RUN_ALL_TESTS(); +} diff --git a/games/renju/mygame.cc b/games/renju/mygame.cc index c2fff851..3663ad0d 100644 --- a/games/renju/mygame.cc +++ b/games/renju/mygame.cc @@ -30,12 +30,12 @@ const GameProperties k_properties { .developer_ = "森高", .description_ = "双方一起落子的五子棋游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } /* 0 means no max-player limits */ -uint32_t Multiple(const MyGameOptions& options) { return 2; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } /* 0 means no max-player limits */ +uint32_t Multiple(const CustomOptions& options) { return 2; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为" << generic_options_readonly.PlayerNum(); @@ -46,7 +46,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; diff --git a/games/rogue_pasture/mygame.cc b/games/rogue_pasture/mygame.cc index 9708f95f..b601b7e9 100644 --- a/games/rogue_pasture/mygame.cc +++ b/games/rogue_pasture/mygame.cc @@ -21,16 +21,16 @@ const GameProperties k_properties { .developer_ = "宽容", .description_ = "放牧得分的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 1; } -uint32_t Multiple(const MyGameOptions& options) { return 0; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 1; } +uint32_t Multiple(const CustomOptions& options) { return 0; } const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { return true; } +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { return true; } const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { return NewGameMode::SINGLE_USER; }, diff --git a/games/six_nimmt/mygame.cc b/games/six_nimmt/mygame.cc index 947416d3..28087575 100644 --- a/games/six_nimmt/mygame.cc +++ b/games/six_nimmt/mygame.cc @@ -24,8 +24,8 @@ const GameProperties k_properties { .developer_ = "铁蛋", .description_ = "玩家照着大小顺序摆牌,尽可能获得更少牛头的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 10; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { +uint64_t MaxPlayerNum(const CustomOptions& options) { return 10; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { if (GET_OPTION_VALUE(options, 倍数) != vector{5, 10, 11, 55, 110}) { return 0; } @@ -33,7 +33,7 @@ uint32_t Multiple(const MyGameOptions& options) { case 0: return 1; case 1: return 1; case 2: return 2; - case 3: return 4; + case 3: return 3; case 4: return 0; default: return 0; } @@ -41,7 +41,7 @@ uint32_t Multiple(const MyGameOptions& options) { const MutableGenericOptions k_default_generic_options{}; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 2) { reply() << "该游戏至少 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -75,7 +75,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 6; return NewGameMode::SINGLE_USER; @@ -108,6 +108,8 @@ class MainStage : public MainGameStage vector players; // 等待放置卡牌玩家临时列表 vector current_players; + // 广播游戏结束提示 + bool boardcast = false; private: @@ -144,13 +146,13 @@ class MainStage : public MainGameStage (GAME_OPTION(模式) == 3 && table.CheckPlayerHead(66, players)) || (GAME_OPTION(模式) == 4 && round_ == GAME_OPTION(手牌)); if (!players[0].hand.empty() || !game_end) { - if (players[0].hand.empty()) { Global().Boardcast() << "[提示] 手牌用尽但未到达游戏结束条件,将重新洗牌继续游戏!"; table.tableStatus.clear(); table.tableStatus.resize(GAME_OPTION(行数)); table.ShuffleCards(players, GAME_OPTION(卡牌)); - } else if (game_end) { + } else if (game_end && !boardcast) { + boardcast = true; Global().Boardcast() << "[提示] 已经有玩家达到目标分数!游戏将在本轮结束时结算分数"; } setter.Emplace(*this, ++round_); diff --git a/games/sync_mahjong/mygame.cc b/games/sync_mahjong/mygame.cc index ad921e87..c08052ed 100644 --- a/games/sync_mahjong/mygame.cc +++ b/games/sync_mahjong/mygame.cc @@ -28,7 +28,7 @@ const std::vector k_rule_commands = {}; const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [](MyGameOptions& game_options, MutableGenericOptions& generic_options) + [](CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 4; return NewGameMode::SINGLE_USER; @@ -36,7 +36,7 @@ const std::vector k_init_options_commands = { VoidChecker("单机")), }; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (const auto player_num = generic_options_readonly.PlayerNum(); player_num < 3) { reply() << "该游戏至少 3 人参加,当前玩家数为 " << player_num; @@ -45,9 +45,9 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener return true; } -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 4; } +uint64_t MaxPlayerNum(const CustomOptions& options) { return 4; } -uint32_t Multiple(const MyGameOptions& options) +uint32_t Multiple(const CustomOptions& options) { if (!GET_OPTION_VALUE(options, 种子).empty()) { return 0; diff --git a/games/sync_unity_chess/mygame.cc b/games/sync_unity_chess/mygame.cc index 9fb73831..a4811274 100644 --- a/games/sync_unity_chess/mygame.cc +++ b/games/sync_unity_chess/mygame.cc @@ -28,12 +28,12 @@ const GameProperties k_properties { .description_ = "同时落子的通过三连珠进行棋盘染色的棋类游戏", }; const uint64_t k_max_player = game_util::unity_chess::k_max_player; // 0 indicates no max-player limits -uint64_t MaxPlayerNum(const MyGameOptions& options) { return k_max_player; } -uint32_t Multiple(const MyGameOptions& options) { return 2; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game +uint64_t MaxPlayerNum(const CustomOptions& options) { return k_max_player; } +uint32_t Multiple(const CustomOptions& options) { return 2; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 2) { reply() << "该游戏至少 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -44,7 +44,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 3; return NewGameMode::SINGLE_USER; diff --git a/games/throw_handkerchief/mygame.cc b/games/throw_handkerchief/mygame.cc index 04ffebbb..ba7adb7d 100644 --- a/games/throw_handkerchief/mygame.cc +++ b/games/throw_handkerchief/mygame.cc @@ -23,8 +23,8 @@ const GameProperties k_properties { .developer_ = "铁蛋", .description_ = "玩家分别丢手帕和回头看,抓住最合适的时机才能获胜", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } -uint32_t Multiple(const MyGameOptions& options) { +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } +uint32_t Multiple(const CustomOptions& options) { return min(3U, max(GET_OPTION_VALUE(options, 模式) == 0 && GET_OPTION_VALUE(options, 生命) == 300 ? 1 : GET_OPTION_VALUE(options, 生命) / 150, 1U)); } const MutableGenericOptions k_default_generic_options; @@ -32,7 +32,7 @@ const std::vector k_rule_commands = {}; const std::vector k_init_options_commands = { InitOptionsCommand("设定游戏模式", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options, const uint32_t& mode) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const uint32_t& mode) { if (mode == 100) { generic_options.bench_computers_to_player_num_ = 2; @@ -45,7 +45,7 @@ const std::vector k_init_options_commands = { AlterChecker({{"快速", 0}, {"高级", 1}, {"实时", 2}, {"单机", 100}})), }; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); diff --git a/games/throw_handkerchief/rule.md b/games/throw_handkerchief/rule.md index 6fb40879..882934b4 100644 --- a/games/throw_handkerchief/rule.md +++ b/games/throw_handkerchief/rule.md @@ -7,6 +7,8 @@ - 双方分别轮流扮演丢手帕的一方和回头看的一方,均需在60秒内执行丢或回头(普通模式玩家仅需要设定行动时间即可) - 若回头时对方的手帕未丢出,视为失误并受到惩罚,惩罚次数**达到3次**时,直接判负;若回头时手帕已丢出,则记录手帕丢出后直到被发现的经过时间,累计时间超过3分钟(先手为2:45)的玩家判负 - 默认包含**完美回头**规则:当回头的时间与丢手帕的时间相同时,视为完美回头,达成完美回头的玩家**直接获胜**;若不开启此规则完美回头**视为1秒** + +### 游戏模式 - 【高级场】初始时间延长到5分钟(先手为4:45),且惩罚改为**达到5次或比对方多3次**时判负 - 【自定义】初始时间少于300秒时采用快速场规则,大于300秒时采用高级场规则,且先手默认减少15秒时间 - 【实时模式】此模式下仅统计玩家发送消息时的真实秒数,在玩家回头时**直接进入下一回合**。如超时未操作,视为在第60秒时行动。**请注意:**在该模式下,双方均行动之前,倒计时提示**永远**会警告双方玩家,即使玩家已经行动完成 diff --git a/games/tianji_horse_racing/mygame.cc b/games/tianji_horse_racing/mygame.cc index c8dbf3bb..042be495 100644 --- a/games/tianji_horse_racing/mygame.cc +++ b/games/tianji_horse_racing/mygame.cc @@ -26,12 +26,12 @@ const GameProperties k_properties { .developer_ = "铁蛋", .description_ = "按恰当顺序选择数字,尽可能成为唯一最大数的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 2; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 2; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 2) { reply() << "该游戏至少 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -50,7 +50,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 5; return NewGameMode::SINGLE_USER; diff --git a/games/unity_chess/mygame.cc b/games/unity_chess/mygame.cc index 5fbd30ce..9e32ec22 100644 --- a/games/unity_chess/mygame.cc +++ b/games/unity_chess/mygame.cc @@ -31,14 +31,14 @@ const GameProperties k_properties { .description_ = "通过三连珠进行棋盘染色的棋类游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 2; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 2; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options{ .is_formal_{false}, }; const std::vector k_rule_commands = {}; -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏必须 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -49,7 +49,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; diff --git a/games/wordle/mygame.cc b/games/wordle/mygame.cc index a7bb9d91..0ea8c27a 100644 --- a/games/wordle/mygame.cc +++ b/games/wordle/mygame.cc @@ -32,8 +32,8 @@ const GameProperties k_properties { .description_ = "猜测英文单词的游戏", }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 2; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 1; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 1; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options{ .is_formal_{false}, }; @@ -100,7 +100,7 @@ string cmpWordle(string a,string b) } -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { reply() << "该游戏必须 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -110,8 +110,16 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener } const std::vector k_init_options_commands = { + InitOptionsCommand("设定游戏配置", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const uint32_t& length, const uint32_t& hard) { + GET_OPTION_VALUE(game_options, 长度) = length; + GET_OPTION_VALUE(game_options, 高难) = hard; + return NewGameMode::MULTIPLE_USERS; + }, + ArithChecker(2, 11, "长度"), + OptionalDefaultChecker>(0, 0, 2, "高难")), InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 2; return NewGameMode::SINGLE_USER; @@ -180,14 +188,17 @@ class MainStage : public MainGameStage // reply() << "这里输出当前游戏情况"; // Returning |OK| means the game stage - string s1, s2, g1, g2; - s1 = player_word_[0]; - s2 = player_word_[1]; - g1 = player_now_[0]; - g2 = player_now_[1]; - Global().Boardcast() << g1 << " " << g2 ; - Global().Boardcast() << cmpWordle(s2, g1) << " " << cmpWordle(s1, g2); - + if (wordLength >= 5) { + string s1, s2, g1, g2; + s1 = player_word_[0]; + s2 = player_word_[1]; + g1 = player_now_[0]; + g2 = player_now_[1]; + Global().Boardcast() << g1 << " " << g2 ; + Global().Boardcast() << cmpWordle(s2, g1) << " " << cmpWordle(s1, g2); + } else { + Global().Boardcast() << player_now_[pid] << "\n" << cmpWordle(player_word_[!pid], player_now_[pid]); + } return StageErrCode::OK; } diff --git a/games/world_without_thieves/mygame.cc b/games/world_without_thieves/mygame.cc index 2c97d09f..dac114cc 100644 --- a/games/world_without_thieves/mygame.cc +++ b/games/world_without_thieves/mygame.cc @@ -25,9 +25,10 @@ const GameProperties k_properties { .name_ = "天下无贼", // the game name which should be unique among all the games .developer_ = "睦月", .description_ = "通过在民/警/贼身份中切换,尽可能活到最后的游戏", + .shuffled_player_id_ = true, }; -uint64_t MaxPlayerNum(const MyGameOptions& options) { return 0; } // 0 indicates no max-player limits -uint32_t Multiple(const MyGameOptions& options) { return 2; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game +uint64_t MaxPlayerNum(const CustomOptions& options) { return 0; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 2; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options; const std::vector k_rule_commands = {}; @@ -54,7 +55,7 @@ std::string str(int x) return z; } -bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() < 3) { reply() << "该游戏至少 3 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); @@ -65,7 +66,7 @@ bool AdaptOptions(MsgSenderBase& reply, MyGameOptions& game_options, const Gener const std::vector k_init_options_commands = { InitOptionsCommand("独自一人开始游戏", - [] (MyGameOptions& game_options, MutableGenericOptions& generic_options) + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { generic_options.bench_computers_to_player_num_ = 6; return NewGameMode::SINGLE_USER; diff --git a/third_party/mahjong b/third_party/mahjong index 4aaf6de1..f5f69290 160000 --- a/third_party/mahjong +++ b/third_party/mahjong @@ -1 +1 @@ -Subproject commit 4aaf6de109ecdbce3f0e190a11df889d91378d90 +Subproject commit f5f69290fe728b1461a9d5c0227eb11b37075cdb diff --git a/tools/score_updater.cc b/tools/score_updater.cc index 7d1f069b..58692941 100644 --- a/tools/score_updater.cc +++ b/tools/score_updater.cc @@ -75,7 +75,7 @@ void UpdateMatchScore(sqlite::database& db, const uint64_t match_id, const std:: for (const auto& info : score_infos) { db << "UPDATE user_with_match SET zero_sum_score = ?, top_score = ?, level_score = ?, rank_score = ? " "WHERE match_id = ? AND user_id = ?;" - << info.zero_sum_score_ << info.top_score_ << info.level_score_ << info.rank_score_ << match_id << info.uid_; + << info.zero_sum_score_ << info.top_score_ << info.level_score_ << info.rank_score_ << match_id << info.uid_.GetStr(); std::cout << "uid=" << info.uid_ << "\tmid=" << match_id << "\tzero_sum_score=" << info.zero_sum_score_