diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index e3be9296..25a8a139 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -17,7 +17,7 @@ jobs: - run: | sudo apt update sudo apt install gcc-12 g++-12 - + - name: Install dependences run: sudo apt-get remove libunwind-14 -y; sudo apt-get install -y libgoogle-glog-dev libgflags-dev libgtest-dev libsqlite3-dev libqt5webkit5-dev python3-pybind11 @@ -27,7 +27,9 @@ jobs: submodules: recursive - name: Configure CMake - run: cmake -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 -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ + -DWITH_GCOV=OFF -DWITH_ASAN=OFF -DWITH_GLOG=OFF -DWITH_SQLITE=ON -DWITH_TEST=ON -DWITH_TOOLS=ON -DWITH_GAMES=ON shell: bash env: CC: gcc-12 @@ -71,7 +73,14 @@ 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 -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + # Temporary: fix gflags compile error + # define GFLAGS_IS_A_DLL=0 to avoid "operator '&&' has no left operand" in gflags.h + # -Dgoogle=gflags resolves undefined 'google::RegisterFlagValidator' symbol + 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_TOOLS=ON -DWITH_GAMES=ON \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DCMAKE_CXX_FLAGS="-DGFLAGS_IS_A_DLL=0 -Dgoogle=gflags" - name: Build working-directory: build diff --git a/CMakeLists.txt b/CMakeLists.txt index bdca7069..b9101054 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsigned-char -g") include_directories(${CMAKE_CURRENT_SOURCE_DIR}/third_party/json/include) if (WITH_IMAGE) + find_package(gflags REQUIRED) add_subdirectory(third_party/markdown2image) endif() diff --git a/bot_core/bot_core.cc b/bot_core/bot_core.cc index e150f3ac..92cd652e 100644 --- a/bot_core/bot_core.cc +++ b/bot_core/bot_core.cc @@ -48,8 +48,8 @@ static ErrCode HandleRequest(BotCtx& bot, const std::optional gid, cons return EC_MATCH_USER_NOT_IN_MATCH; } if (match->gid() != gid && gid.has_value()) { - reply() << "[错误] 您未在本群参与游戏\n"; - "若您想执行元指令,请尝试在请求前加\"" META_COMMAND_SIGN "\",或通过\"" META_COMMAND_SIGN "帮助\"查看所有支持的元指令"; + reply() << "[错误] 您未在本群参与游戏\n" + "若您想执行元指令,请尝试在请求前加\"" META_COMMAND_SIGN "\",或通过\"" META_COMMAND_SIGN "帮助\"查看所有支持的元指令"; return EC_MATCH_NOT_THIS_GROUP; } return match->Request(uid, gid, msg, reply); diff --git a/game_util/othello.h b/game_util/othello.h index fbc7ca88..d42c9f34 100644 --- a/game_util/othello.h +++ b/game_util/othello.h @@ -71,6 +71,9 @@ class Board bool Place(const Coor& coor, const ChessType type) { + if (coor.row_ < 0 || coor.row_ >= k_size_ || coor.col_ < 0 || coor.col_ >= k_size_) { + return false; + } auto& box = Get_(coor); if (box.cur_type_ != ChessType::NONE) { return false; // there is already a chess diff --git a/game_util/quixo.h b/game_util/quixo.h index 0d34c2a6..9a5f58b1 100644 --- a/game_util/quixo.h +++ b/game_util/quixo.h @@ -111,6 +111,9 @@ class Board if (src_type != Type::_ && src_type != type) { return ErrCode::INVALID_SRC; } + if (!TryPush_(src_coor, dst_coor, type) && !TryPush_(src_coor, dst_coor, type)) { + return ErrCode::INVALID_DST; + } if (src_type == Type::_) { if (type == Type::X1 || type == Type::X2) { ++chess_counts_[static_cast(Symbol::X)]; @@ -118,9 +121,6 @@ class Board ++chess_counts_[static_cast(Symbol::O)]; } } - if (!TryPush_(src_coor, dst_coor, type) && !TryPush_(src_coor, dst_coor, type)) { - return ErrCode::INVALID_DST; - } areas_[dst_coor.x_][dst_coor.y_] = type; last_move_coor_.emplace(dst_coor); return ErrCode::OK; diff --git a/games/blocked_road/mygame.cc b/games/blocked_road/mygame.cc index c6e3d1bc..1722be5e 100644 --- a/games/blocked_road/mygame.cc +++ b/games/blocked_road/mygame.cc @@ -39,6 +39,14 @@ bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const Gener } const std::vector k_init_options_commands = { + InitOptionsCommand("设置棋子数和边长", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const uint32_t& num, const uint32_t& size) + { + GET_OPTION_VALUE(game_options, 棋子) = num; + GET_OPTION_VALUE(game_options, 边长) = size; + return NewGameMode::MULTIPLE_USERS; + }, + ArithChecker(3, 6, "棋子"), OptionalDefaultChecker>(4, 4, 6, "边长")), InitOptionsCommand("独自一人开始游戏", [] (CustomOptions& game_options, MutableGenericOptions& generic_options) { @@ -337,15 +345,19 @@ class RoundStage : public SubGameStage<> if (pid == Main().currentPlayer) { int X, Y, addx, addy; string result; - while (result != "OK") { - int c = rand() % GAME_OPTION(棋子) + 1; - for (int i = 1; i <= Main().board.size; i++) { - for (int j = 1; j <= Main().board.size; j++) { - if (Main().board.chess[i][j] == pid + 1 && c >= 0) { - c--; X = i; Y = j; + int try_count = 0; + while (result != "OK" && try_count++ < 1000) { + std::vector> pieces; + for (int i = 1; i <= Main().board.size; ++i) { + for (int j = 1; j <= Main().board.size; ++j) { + if (Main().board.chess[i][j] == pid + 1) { + pieces.push_back({i, j}); } } } + int c = rand() % GAME_OPTION(棋子); + X = pieces[c].first; + Y = pieces[c].second; addx = addy = 0; if (rand() % 2) { addx = rand() % 2 == 1 ? 1 : -1; diff --git a/games/dvalue_tender/mygame.cc b/games/dvalue_tender/mygame.cc index 8532e16a..560995f9 100644 --- a/games/dvalue_tender/mygame.cc +++ b/games/dvalue_tender/mygame.cc @@ -365,22 +365,21 @@ void MainStage::FirstStageFsm(SubStageFsmSetter setter) void MainStage::NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) { - round_++; - // Global().Boardcast()< 10 && (player_wins_[0] < 6 && player_wins_[1] < 6) ) + (round_ < GAME_OPTION(回合数) && (player_wins_[0] < win_need && player_wins_[1] < win_need) ) + ||(round_ > GAME_OPTION(回合数) && (player_wins_[0] < win_need + 1 && player_wins_[1] < win_need + 1) ) ) { - setter.Emplace(*this, round_); + setter.Emplace(*this, ++round_); return; } } - else if(round_ == 10){ + else if(round_ == GAME_OPTION(回合数)){ if(player_wins_[0] == player_wins_[1]){ - setter.Emplace(*this, round_); + setter.Emplace(*this, ++round_); return; } } diff --git a/games/dvalue_tender/options.h b/games/dvalue_tender/options.h index a3de36c5..cd32b58b 100644 --- a/games/dvalue_tender/options.h +++ b/games/dvalue_tender/options.h @@ -1,3 +1,3 @@ -EXTEND_OPTION("回合数", 回合数, (ArithChecker(1, 20, "回合数")), 15) +EXTEND_OPTION("回合数", 回合数, (ArithChecker(3, 20, "回合数")), 9) EXTEND_OPTION("金币", 金币, (ArithChecker(10, 1000000, "金币")), 30) EXTEND_OPTION("每回合时间限制", 时限, (ArithChecker(10, 3600, "超时时间(秒)")), 90) diff --git a/games/garnet_thief/achievements.h b/games/garnet_thief/achievements.h new file mode 100644 index 00000000..e69de29b diff --git a/games/garnet_thief/icon.png b/games/garnet_thief/icon.png new file mode 100644 index 00000000..94fe487e Binary files /dev/null and b/games/garnet_thief/icon.png differ diff --git a/games/garnet_thief/mygame.cc b/games/garnet_thief/mygame.cc new file mode 100644 index 00000000..857aae27 --- /dev/null +++ b/games/garnet_thief/mygame.cc @@ -0,0 +1,523 @@ +// 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; + +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 0; } // 0 indicates no max-player limits +uint32_t Multiple(const CustomOptions& options) { return 0; } // 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, 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; +} + +const std::vector k_init_options_commands = { + InitOptionsCommand("独自一人开始游戏", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) + { + generic_options.bench_computers_to_player_num_ = 8; + 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), + player_chips_(Global().PlayerNum(), 2), + player_last_chips_(Global().PlayerNum(), 2), + player_declare_(Global().PlayerNum(), ' '), + player_select_(Global().PlayerNum(), ' ') {} + + virtual int64_t PlayerScore(const PlayerID pid) const override { return player_scores_[pid]; } + + int round_; + std::vector player_scores_; + + vector player_chips_; // 玩家Chips + vector player_last_chips_; // 上回合Chips + vector player_declare_; // 玩家声明 + vector player_select_; // 玩家选择 + + string T_Board = ""; // 表头 + string Board = ""; // 赛况 + + const string clip_color = "9CCAF0"; // Clip底色 + const string declare_color = "FFEBA3"; // 声明身份颜色 + const string win_color = "BAFFA8"; // 加分颜色 + const string lose_color = "FFA07A"; // 扣分颜色 + + const int image_width = Global().PlayerNum() < 8 ? Global().PlayerNum() * 80 + 100 : (Global().PlayerNum() < 16 ? Global().PlayerNum() * 70 + 50 : Global().PlayerNum() * 60 + 40); + + int Alive_() const { return std::count_if(player_chips_.begin(), player_chips_.end(), [](const auto& chips){ return chips > 0; }); } + + string GetName(string x); + string GetStatusBoard(); + + private: + CompReqErrCode Status_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + string status_Board = GetStatusBoard(); + reply() << Markdown(T_Board + status_Board + Board + "", image_width); + return StageErrCode::OK; + } + + void FirstStageFsm(SubStageFsmSetter setter) + { + srand((unsigned int)time(NULL)); + for (int i = 0; i < Global().PlayerNum(); i++) { + player_chips_[i] = player_last_chips_[i] = GAME_OPTION(Chips); + } + + T_Board += ""; + for (int i = 0; i < Global().PlayerNum(); i++) { + T_Board += ""; + if (i % 4 == 3) T_Board += ""; + } + T_Board += "
"; + + T_Board += "
" + to_string(i + 1) + " 号: " + GetName(Global().PlayerName(i)) + " 
"; + T_Board += ""; + for (int i = 0; i < Global().PlayerNum(); i++) { + T_Board += ""; + } + T_Board += ""; + + string status_Board = GetStatusBoard(); + + string PreBoard = ""; + 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"; + } + } + + Global().Boardcast() << PreBoard; + Global().Boardcast() << Markdown(T_Board + status_Board + "
序号"; + T_Board += to_string(i + 1) + " 号"; + T_Board += "
", image_width); + setter.Emplace(*this, ++round_); + } + + void NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) + { + + bool chips_all_zero = all_of(player_chips_.begin(), player_chips_.end(), [](int64_t c) { return c == 0; }); + if ((++round_) <= GAME_OPTION(回合数) && !chips_all_zero) { + setter.Emplace(*this, round_); + return; + } + if (chips_all_zero) { + Global().Boardcast() << "所有玩家都被淘汰,游戏结束!"; + } + for (int pid = 0; pid < Global().PlayerNum(); pid++) { + player_scores_[pid] = player_chips_[pid]; + } + } +}; + +class DeclareStage; +class SelectStage; + +class RoundStage : public SubGameStage +{ + public: + RoundStage(MainStage& main_stage, const uint64_t round) + : StageFsm(main_stage, "第" + std::to_string(round) + "回合", + MakeStageCommand(*this, "【测试功能】向其他玩家发送私信消息", &RoundStage::SendMsg_, + ArithChecker(1, main_stage.player_scores_.size(), "序号"), RepeatableChecker>("私信内容", "私信内容"))) + {} + + void calc(); + + private: + CompReqErrCode SendMsg_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const uint32_t target, const vector messages) + { + if (pid == target - 1) { + reply() << "[错误] 不能向自己发送私信。"; + return StageErrCode::FAILED; + } + if (Main().player_chips_[target - 1] <= 0) { + reply() << "[错误] 不能向淘汰的玩家发送私信。"; + return StageErrCode::FAILED; + } + if (is_public) { + reply() << "[错误] 请私信执行此行动。"; + return StageErrCode::FAILED; + } + ostringstream oss; + for (size_t i = 0; i < messages.size(); ++i) { + if (i > 0) oss << " "; + oss << messages[i]; + } + string msg = oss.str(); + Global().Tell(PlayerID(target - 1)) << "【游戏消息《石榴石窃贼》】\n" + << "收到来自 [" << (pid + 1) << "号]" << Global().PlayerName(pid) << " 的私信,内容:\n" + << msg; + reply() << "向 [" << target << "号]" << Global().PlayerName(target - 1) << " 发送私信成功"; + return StageErrCode::OK; + } + + void FirstStageFsm(SubStageFsmSetter setter) + { + setter.Emplace(Main()); + } + + void NextStageFsm(DeclareStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) + { + string status_Board = Main().GetStatusBoard(); + + string b = "R" + to_string(Main().round_) + "声明"; + for (int pid = 0; pid < Global().PlayerNum(); pid++) { + b += ""; + if (Main().player_declare_[pid] == 'M') b += "黑手党"; + else if (Main().player_declare_[pid] == 'C') b += "卡特尔"; + else if (Main().player_declare_[pid] == 'P') b += "警察"; + else if (Main().player_declare_[pid] == 'B') b += "乞丐"; + else b += " "; + b += ""; + } + b += ""; + Main().Board += b; + + Global().Boardcast() << Markdown(Main().T_Board + status_Board + Main().Board + "", Main().image_width); + setter.Emplace(Main()); + } + + void NextStageFsm(SelectStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) + { + RoundStage::calc(); + } +}; + +const map role_map = { + {"黑手党", 'M'}, {"M", 'M'}, + {"卡特尔", 'C'}, {"C", 'C'}, + {"警察", 'P'}, {"P", 'P'}, + {"乞丐", 'B'}, {"B", 'B'}, +}; + +class DeclareStage : public SubGameStage<> +{ + public: + DeclareStage(MainStage& main_stage) + : StageFsm(main_stage, "声明阶段" , + MakeStageCommand(*this, "选择声明的身份", &DeclareStage::DeclareRole_, AlterChecker(role_map))) + {} + + virtual void OnStageBegin() override + { + Global().Boardcast() << "请所有玩家私信选择【声明】的身份,时限 " << GAME_OPTION(时限) << " 秒:\n" + << "黑手党(M) / 卡特尔(C) / 警察(P) / 乞丐(B)"; + if (Main().round_ == 1) { + Global().Boardcast() << "【允许私信】此游戏允许在进行中和其他玩家进行私信沟通,进行合作或协商策略\n" + << "- 测试指令:「<序号> <私信内容>」直接向其他玩家发送私信消息"; + } + Global().StartTimer(GAME_OPTION(时限)); + } + + private: + AtomReqErrCode DeclareRole_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const char role) + { + if (is_public) { + reply() << "[错误] 请私信裁判进行声明。"; + return StageErrCode::FAILED; + } + if (Global().IsReady(pid)) { + reply() << "[错误] 您本回合已经进行过声明了。"; + return StageErrCode::FAILED; + } + Main().player_declare_[pid] = role; + reply() << "声明身份成功"; + return StageErrCode::READY; + } + + virtual CheckoutErrCode OnStageTimeout() override + { + for (int pid = 0; pid < Global().PlayerNum(); pid++) { + if (!Global().IsReady(pid)) { + Main().player_chips_[pid] = 0; + Main().player_declare_[pid] = ' '; + Main().player_select_[pid] = ' '; + Global().Eliminate(pid); + } + } + Global().Boardcast() << "有玩家超时仍未行动,已被淘汰"; + return StageErrCode::CHECKOUT; + } + + virtual CheckoutErrCode OnPlayerLeave(const PlayerID pid) override + { + Main().player_chips_[pid] = 0; + Main().player_declare_[pid] = ' '; + Main().player_select_[pid] = ' '; + return StageErrCode::CONTINUE; + } + + virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) override + { + if (Global().IsReady(pid)) { + return StageErrCode::OK; + } + char roles[4] = {'M', 'C', 'P', 'B'}; + Main().player_declare_[pid] = roles[rand() % 4]; + return StageErrCode::READY; + } +}; + +class SelectStage : public SubGameStage<> +{ + public: + SelectStage(MainStage& main_stage) + : StageFsm(main_stage, "提交阶段", + MakeStageCommand(*this, "选择真实提交的身份", &SelectStage::SelectRole_, AlterChecker(role_map))) + {} + + virtual void OnStageBegin() override + { + Global().Boardcast() << "请所有玩家私信选择【真实】的身份,时限 " << GAME_OPTION(时限) << " 秒:\n" + << "黑手党(M) / 卡特尔(C) / 警察(P) / 乞丐(B)"; + Global().StartTimer(GAME_OPTION(时限)); + } + + private: + AtomReqErrCode SelectRole_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const char role) + { + if (is_public) { + reply() << "[错误] 请私信裁判进行提交。"; + return StageErrCode::FAILED; + } + if (Global().IsReady(pid)) { + reply() << "[错误] 您本回合已经进行过提交了。"; + return StageErrCode::FAILED; + } + Main().player_select_[pid] = role; + reply() << "提交身份成功"; + return StageErrCode::READY; + } + + virtual CheckoutErrCode OnStageTimeout() override + { + for (int pid = 0; pid < Global().PlayerNum(); pid++) { + if (!Global().IsReady(pid)) { + Main().player_chips_[pid] = 0; + Main().player_declare_[pid] = ' '; + Main().player_select_[pid] = ' '; + Global().Eliminate(pid); + } + } + Global().Boardcast() << "有玩家超时仍未行动,已被淘汰"; + return StageErrCode::CHECKOUT; + } + + virtual CheckoutErrCode OnPlayerLeave(const PlayerID pid) override + { + Main().player_chips_[pid] = 0; + Main().player_declare_[pid] = ' '; + Main().player_select_[pid] = ' '; + return StageErrCode::CONTINUE; + } + + virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) override + { + if (Global().IsReady(pid)) { + return StageErrCode::OK; + } + char roles[4] = {'M', 'C', 'P', 'B'}; + if (rand() % 3 == 0) { + Main().player_select_[pid] = roles[rand() % 4]; + } else { + Main().player_select_[pid] = Main().player_declare_[pid]; + } + return StageErrCode::READY; + } +}; + +string MainStage::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; +} + +string MainStage::GetStatusBoard() { + string status_Board = ""; + status_Board += "Chips"; + for (int i = 0; i < Global().PlayerNum(); i++) { + status_Board += ""; + if (player_chips_[i] > 0) { + status_Board += to_string(player_chips_[i]); + if (player_chips_[i] > player_last_chips_[i]) { + status_Board += Global().PlayerNum() >= 16 ? "
" : HTML_ESCAPE_SPACE; + status_Board += "(+" + to_string(player_chips_[i] - player_last_chips_[i]) + ")"; + } else if (player_chips_[i] < player_last_chips_[i]) { + status_Board += Global().PlayerNum() >= 16 ? "
" : HTML_ESCAPE_SPACE; + status_Board += "(-" + to_string(player_last_chips_[i] - player_chips_[i]) + ")"; + } + } else { + status_Board += "" + to_string(player_chips_[i]) + ""; + } + status_Board += ""; + } + status_Board += ""; + return status_Board; +} + +void RoundStage::calc() { + int N = Main().Alive_() / 2; + + int M_count = 0, C_count = 0, P_count = 0, B_count = 0; + for (int pid = 0; pid < Global().PlayerNum(); pid++) { + char s = Main().player_select_[pid]; + if (s == 'M') M_count++; + else if (s == 'C') C_count++; + else if (s == 'P') P_count++; + else if (s == 'B') B_count++; + } + + vector gain(Global().PlayerNum(), 0); + char primary_role = ' '; + int primary_count = 0; + if (M_count > C_count) { + primary_role = 'M'; + primary_count = M_count; + } else if (C_count > M_count) { + primary_role = 'C'; + primary_count = C_count; + } else { + primary_role = 'P'; + primary_count = P_count; + } + + int remaining = N; + if (primary_count > 0 && primary_role != ' ') { + int per = N / primary_count; + if (per > 0) { + for (int pid = 0; pid < Global().PlayerNum(); pid++) { + if (Main().player_select_[pid] == primary_role) { + gain[pid] += per; + } + } + } + int used = per * primary_count; + remaining = N - used; + } else { + remaining = N; + } + + if (remaining > 0 && B_count > 0) { + int perB = remaining / B_count; + if (perB > 0) { + for (int pid = 0; pid < Global().PlayerNum(); pid++) { + if (Main().player_select_[pid] == 'B') { + gain[pid] += perB; + } + } + } + } + + for (int pid = 0; pid < Global().PlayerNum(); pid++) { + if (gain[pid] > 0) { + Main().player_chips_[pid] += gain[pid]; + // 新增:身份不同且获得分数,额外+1 + if (Main().player_declare_[pid] != Main().player_select_[pid]) { + Main().player_chips_[pid] += 1; + } + } + if (gain[pid] == 0 && Main().player_chips_[pid] > 0 && Main().player_declare_[pid] != Main().player_select_[pid]) { + Main().player_chips_[pid] -= 1; + } + } + + string status_Board = Main().GetStatusBoard(); + + string b = "R" + to_string(Main().round_) + "提交"; + for (int pid = 0; pid < Global().PlayerNum(); pid++) { + if (Main().player_chips_[pid] > Main().player_last_chips_[pid]) { + b += ""; + } else if (Main().player_chips_[pid] < Main().player_last_chips_[pid]) { + b += "" ; + } else { + b += ""; + } + if (Main().player_select_[pid] == 'M') b += "黑手党"; + else if (Main().player_select_[pid] == 'C') b += "卡特尔"; + else if (Main().player_select_[pid] == 'P') b += "警察"; + else if (Main().player_select_[pid] == 'B') b += "乞丐"; + else b += " "; + b += ""; + } + b += ""; + Main().Board += b; + + Global().Boardcast() << Markdown(Main().T_Board + status_Board + Main().Board + "", Main().image_width); + + for (int pid = 0; pid < Global().PlayerNum(); pid++) { + Main().player_last_chips_[pid] = Main().player_chips_[pid]; + if (Main().player_chips_[pid] <= 0) { + Main().player_declare_[pid] = ' '; + Main().player_select_[pid] = ' '; + Global().Eliminate(pid); + } + } +} + +auto* MakeMainStage(MainStageFactory factory) { return factory.Create(); } + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + diff --git a/games/garnet_thief/option.cmake b/games/garnet_thief/option.cmake new file mode 100644 index 00000000..e69de29b diff --git a/games/garnet_thief/options.h b/games/garnet_thief/options.h new file mode 100644 index 00000000..8dfb6a61 --- /dev/null +++ b/games/garnet_thief/options.h @@ -0,0 +1,3 @@ +EXTEND_OPTION("回合数", 回合数, (ArithChecker(1, 20, "回合数")), 8) +EXTEND_OPTION("初始 Chips 数量", Chips, (ArithChecker(1, 5, "数量")), 2) +EXTEND_OPTION("每回合时间限制", 时限, (ArithChecker(10, 3600, "超时时间(秒)")), 180) diff --git a/games/garnet_thief/rule.md b/games/garnet_thief/rule.md new file mode 100644 index 00000000..60724160 --- /dev/null +++ b/games/garnet_thief/rule.md @@ -0,0 +1,25 @@ +## 石榴石窃贼 + +- **游戏人数:** 3 及以上 +- **原作:** 游戏的法则 + +### 游戏简介 +- 玩家在每回合中选择不同的社会身份(黑手党、卡特尔、警察、乞丐),通过博弈与欺骗获取 **Chips**。 +- 最终目标是在 8 回合后成为**持有 Chips 最多的玩家**。 + +### 游戏流程 +- **【允许私信】** 此游戏允许在进行中和其他玩家进行私信沟通,进行合作或协商策略 + + 测试指令:「<序号> <私信内容>」直接向其他玩家发送私信消息 +- 游戏开始时,每个玩家都拥有 2 个Chips,**游戏共进行 8 个回合** +- 每回合可分配的 Chips 数量为 `N = 存活玩家数 / 2(向下取整)`。 +- **声明阶段:** 每个玩家要先声明自己所选的身份(黑手党 / 卡特尔 / 警察 / 乞丐),然后裁判公布所有人的声明 +- **提交阶段:** 公布声明后,所有玩家要真正提交自己的身份(可与声明不同),根据真实身份分配 Chips +- **Chips 分配规则** + + [黑手党]或[卡特尔]选择更多的玩家,可均分 N 个Chips + + 当选择[黑手党]和[卡特尔]人数均等或没有选择[黑手党]和[卡特尔]的人,选择[警察]的玩家可均分 N 个Chips + + 如均分 Chips 后仍有 Chips 余下,选择[乞丐]的玩家可均分余下的 Chips + + 所有分配均为**向下取整**(例如:4 个 Chips 被 3 人分,每人只能拿 1 个)。 +- **若声明和真实提交的角色不同,且成功获得 Chips,则额外奖励 1 个 Chips。** +- 每回合结束,未获得 Chips 的玩家中,**如果声明和真实提交的角色不同,需扣除 1 颗Chips** +- 如果 Chips 数量被扣至归零,则立即淘汰,无法参与后续回合 +- **8 回合后**,剩余玩家中**持有 Chips 最多的玩家获胜**。 diff --git a/games/garnet_thief/unittest.cc b/games/garnet_thief/unittest.cc new file mode 100644 index 00000000..c8e07d7a --- /dev/null +++ b/games/garnet_thief/unittest.cc @@ -0,0 +1,49 @@ +// 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()); +} + +GAME_TEST(3, leave_test) +{ + START_GAME(); + + ASSERT_LEAVE(CONTINUE, 0); + ASSERT_LEAVE(CONTINUE, 1); + ASSERT_LEAVE(CHECKOUT, 2); + + ASSERT_SCORE(0, 0, 0); +} + +GAME_TEST(3, timeout_test) +{ + START_GAME(); + + ASSERT_TIMEOUT(CHECKOUT); + + ASSERT_SCORE(0, 0, 0); +} + +} // 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/hp_killer/mygame.cc b/games/hp_killer/mygame.cc index d35f1dfa..96b807ce 100644 --- a/games/hp_killer/mygame.cc +++ b/games/hp_killer/mygame.cc @@ -46,7 +46,7 @@ const char* const k_role_rules[Occupation::Count()] = { - 特殊技能「挡刀 <代号>」:令当前回合**攻击**指定角色造成的减 HP 效果转移到**自己**身上,次数不限)EOF", [static_cast(Occupation(Occupation::恶灵))] = R"EOF(【恶灵 | 杀手阵营】 -- 开局时知道【杀手】的代号(五人场除外) +- 开局时知道【杀手】和【灵媒】的代号(五人场除外) - 死亡后仍可继续行动(「中之人」仍会被公布),直到触发以下任意一种情况时,从下一回合起失去行动能力: - 被【侦探】侦查到**治愈**或**攻击**操作 - 被【灵媒】通灵)EOF", @@ -74,6 +74,13 @@ const char* const k_role_rules[Occupation::Count()] = { - 当被侦探侦查时会显示「攻击 <代号>」 - 无法被守卫「盾反」)EOF", + [static_cast(Occupation(Occupation::囚犯))] = R"EOF(【囚犯 | 杀手阵营】 +- 开局时知道【杀手】的代号(五人场除外) +- 特殊技能「重伤 <代号> N」:对指定角色造成 N 点伤害(N可为 0、5、10) + - 被「重伤」的目标本回合受到的所有治愈无效 + - 当被侦探侦查时会显示「攻击 <代号>」 + - 每种伤害值 N 只能使用一次)EOF", + // civilian team [static_cast(Occupation(Occupation::平民))] = R"EOF(【平民 | 平民阵营】 - 无特殊能力)EOF", @@ -329,6 +336,14 @@ struct CurseAction int32_t hp_; }; +struct SevereInjuryAction +{ + std::string ToString() const { return std::string("重伤 ") + token_.ToChar() + " " + std::to_string(hp_); } + + Token token_; + int32_t hp_; +}; + struct CureAction { std::string ToString() const { return std::string("治愈 ") + token_.ToChar() + " " + std::to_string(hp_); } @@ -389,7 +404,7 @@ struct GoodNightAction std::string ToString() const { return "晚安"; } }; -using ActionVariant = std::variant; class RoleManager; @@ -448,13 +463,19 @@ class RoleBase virtual bool Act(const AttackAction& action, MsgSenderBase& reply, StageUtility& utility); + virtual bool Act(const CureAction& action, MsgSenderBase& reply, StageUtility& utility); + virtual bool Act(const CurseAction& action, MsgSenderBase& reply, StageUtility& utility) { reply() << "攻击失败:您无法使用魔法攻击"; return false; } - virtual bool Act(const CureAction& action, MsgSenderBase& reply, StageUtility& utility); + virtual bool Act(const SevereInjuryAction& action, MsgSenderBase& reply, StageUtility& utility) + { + reply() << "重伤失败:您无法执行该类型行动"; + return false; + } virtual bool Act(const BlockAttackAction& action, MsgSenderBase& reply, StageUtility& utility) { @@ -809,8 +830,6 @@ class MainStage : public MainGameStage<> MakeStageCommand(*this, "查看当前游戏进展情况", &MainStage::Status_, VoidChecker("赛况")), MakeStageCommand(*this, "攻击某名角色", &MainStage::Hurt_, VoidChecker("攻击"), RepeatableChecker>("角色代号", "A"), ArithChecker(0, 25, "血量")), - MakeStageCommand(*this, "[魔女] 诅咒某名角色", &MainStage::Curse_, VoidChecker("诅咒"), - BasicChecker("角色代号", "A"), ArithChecker(5, 10, "血量")), MakeStageCommand(*this, "治愈某名角色", &MainStage::Cure_, VoidChecker("治愈"), BasicChecker("角色代号", "A"), BoolChecker(std::to_string(k_heavy_cure_hp), std::to_string(k_normal_cure_hp))), @@ -818,6 +837,10 @@ class MainStage : public MainGameStage<> BasicChecker("角色代号", "A")), MakeStageCommand(*this, "[替身] 替某名角色承担本回合伤害", &MainStage::BlockHurt_, VoidChecker("挡刀"), OptionalChecker>("角色代号(若为空,则为杀手代号)", "A")), + MakeStageCommand(*this, "[魔女] 诅咒某名角色", &MainStage::Curse_, VoidChecker("诅咒"), + BasicChecker("角色代号", "A"), ArithChecker(5, 10, "血量")), + MakeStageCommand(*this, "[囚犯] 重伤某名角色", &MainStage::SevereInjury_, VoidChecker("重伤"), + BasicChecker("角色代号", "A"), ArithChecker(0, 10, "血量")), MakeStageCommand(*this, "[灵媒] 获取某名死者的职业", &MainStage::Exocrism_, VoidChecker("通灵"), BasicChecker("角色代号", "A")), MakeStageCommand(*this, "[守卫] 盾反某几名角色", &MainStage::ShieldAnti_, VoidChecker("盾反"), @@ -919,6 +942,8 @@ class MainStage : public MainGameStage<> s += "攻击 " + std::string(1, std::get(action->token_hps_[0]).ToChar()); } else if (const auto action = std::get_if(&role.CurAction())) { s += "攻击 " + std::string(1, action->token_.ToChar()); + } else if (const auto action = std::get_if(&role.CurAction())) { + s += "攻击 " + std::string(1, action->token_.ToChar()); } else if (const auto action = std::get_if(&role.CurAction())) { s += "治愈 " + std::string(1, action->token_.ToChar()); } else { @@ -955,6 +980,21 @@ class MainStage : public MainGameStage<> } return false; }; + const auto is_blocked_cure = [&](const RoleBase& cured_role) + { + bool blocked = false; + role_manager_.Foreach([&](const auto& role) { + if (role.GetOccupation() != Occupation::囚犯) { + return; + } + if (const auto si = std::get_if(&role.CurAction())) { + if (si->token_ == cured_role.GetToken()) { + blocked = true; + } + } + }); + return blocked; + }; std::vector be_attackeds(role_manager_.Size(), false); role_manager_.Foreach([&](auto& role) @@ -975,7 +1015,10 @@ class MainStage : public MainGameStage<> } } } else if (const auto action = std::get_if(&role.CurAction())) { - role_manager_.GetRole(action->token_).AddHp(action->hp_); + auto& cured_role = role_manager_.GetRole(action->token_); + if (!is_blocked_cure(cured_role)) { + role_manager_.GetRole(action->token_).AddHp(action->hp_); + } } else if (const auto action = std::get_if(&role.CurAction())) { auto& detected_role = role_manager_.GetRole(action->token_); assert(role.PlayerId().has_value()); @@ -996,6 +1039,10 @@ class MainStage : public MainGameStage<> DisableAct_(exocrism_role, true); sender << ",他从下回合开始将失去行动能力!"; } + } else if (const auto action = std::get_if(&role.CurAction())) { + auto& hurted_role = role_manager_.GetRole(action->token_); + be_attackeds[action->token_.id_] = true; + hurted_role.AddHp(-action->hp_); } }); @@ -1323,14 +1370,17 @@ class MainStage : public MainGameStage<> {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::特工}, + {Occupation::杀手, Occupation::替身, Occupation::囚犯, Occupation::侦探, Occupation::圣女, Occupation::骑士, Occupation::平民, Occupation::平民, Occupation::特工}, }); default: assert(false); @@ -1352,7 +1402,8 @@ class MainStage : public MainGameStage<> std::ranges::sort(occupations); for (const auto& occupation : occupations) { if (occupation == Occupation::杀手 || occupation == Occupation::替身 || occupation == Occupation::恶灵 || - occupation == Occupation::刺客 || occupation == Occupation::双子(邪) || occupation == Occupation::魔女) { + occupation == Occupation::刺客 || occupation == Occupation::双子(邪) || occupation == Occupation::魔女 || + occupation == Occupation::囚犯) { s += HTML_COLOR_FONT_HEADER(red); } else if (occupation == Occupation::内奸 || occupation == Occupation::初版内奸 || occupation == Occupation::特工 || occupation == Occupation::人偶) { @@ -1566,15 +1617,6 @@ class MainStage : public MainGameStage<> return GenericAct_(pid, is_public, reply, std::move(action)); } - AtomReqErrCode Curse_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, - const Token token, const int32_t hp) - { - if (!CheckToken(token, "治愈", reply)) { - return StageErrCode::FAILED; - } - return GenericAct_(pid, is_public, reply, CurseAction{.token_ = token, .hp_ = hp}); - } - AtomReqErrCode Cure_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const Token token, const bool is_heavy) { if (!CheckToken(token, "治愈", reply)) { @@ -1595,6 +1637,10 @@ class MainStage : public MainGameStage<> AtomReqErrCode BlockHurt_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const std::optional& token) { + if (!token.has_value() && GAME_OPTION(身份互通)) { + reply() << "挡刀失败:身份互通模式必须指定挡刀代号"; + return StageErrCode::FAILED; + } if (token.has_value() && !role_manager_.IsValid(*token)) { reply() << "挡刀失败:场上没有该角色"; return StageErrCode::FAILED; @@ -1602,6 +1648,22 @@ class MainStage : public MainGameStage<> return GenericAct_(pid, is_public, reply, BlockAttackAction{token}); } + AtomReqErrCode Curse_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, + const Token token, const int32_t hp) + { + if (!CheckToken(token, "诅咒", reply)) { + return StageErrCode::FAILED; + } + return GenericAct_(pid, is_public, reply, CurseAction{.token_ = token, .hp_ = hp}); + } + + AtomReqErrCode SevereInjury_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const Token token, const int32_t hp) { + if (!CheckToken(token, "重伤", reply)) { + return StageErrCode::FAILED; + } + return GenericAct_(pid, is_public, reply, SevereInjuryAction{.token_ = token, .hp_ = hp}); + } + AtomReqErrCode Exocrism_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const Token token) { if (!role_manager_.IsValid(token)) { @@ -1737,9 +1799,9 @@ class GhostRole : public RoleBase { if (main_stage.Global().PlayerNum() > 5) { if (GET_OPTION_VALUE(main_stage.Global().Options(), 身份互通)) { - return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForTeam(role_manager_, Team::平民); + return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForTeam(role_manager_, Team::平民) + "," + TokenInfoForRole(role_manager_, Occupation::灵媒); } else { - return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForRole(role_manager_, Occupation::杀手); + return RoleBase::PrivateInfo(main_stage) + "," + TokenInfoForRole(role_manager_, Occupation::杀手) + "," + TokenInfoForRole(role_manager_, Occupation::灵媒); } } return RoleBase::PrivateInfo(main_stage); @@ -1851,6 +1913,45 @@ class WitchRole : public RoleBase } }; +class PrisonerRole : public RoleBase { + public: + PrisonerRole(const uint64_t pid, const Token token, const RoleOption& option, + const uint32_t role_num, RoleManager& role_manager) + : RoleBase(pid, token, Occupation::囚犯, Team::杀手, option, role_manager) + , used_hp_levels_{false, false, false} {} + + virtual std::string PrivateInfo(const MainStage& main_stage) const override { + if (main_stage.Global().PlayerNum() > 5) { + 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 SevereInjuryAction& action, MsgSenderBase& reply, StageUtility& utility) override { + auto& target = role_manager_.GetRole(action.token_); + cur_action_ = action; + if (action.hp_ != 0 && action.hp_ != 5 && action.hp_ != 10) { + reply() << "重伤失败:重伤仅能造成 0 / 5 / 10 点伤害"; + return false; + } + const size_t level_idx = action.hp_ / 5; + if (used_hp_levels_[level_idx]) { + reply() << "重伤失败:该伤害等级(" << action.hp_ << ")已使用过"; + return false; + } + used_hp_levels_[level_idx] = true; + reply() << "您对角色 " << action.token_.ToChar() << " 重伤造成 " << action.hp_ << " 点伤害,其本回合受到的治愈将无效"; + return true; + } + + private: + std::array used_hp_levels_; +}; + class CivilianRole : public RoleBase { public: @@ -2141,6 +2242,7 @@ MainStage::RoleMaker MainStage::k_role_makers_[Occupation::Count()] = { [static_cast(Occupation(Occupation::刺客))] = &MainStage::MakeRole_, [static_cast(Occupation(Occupation::双子(邪)))] = &MainStage::MakeRole_>, [static_cast(Occupation(Occupation::魔女))] = &MainStage::MakeRole_, + [static_cast(Occupation(Occupation::囚犯))] = &MainStage::MakeRole_, // civilian team [static_cast(Occupation(Occupation::平民))] = &MainStage::MakeRole_, [static_cast(Occupation(Occupation::圣女))] = &MainStage::MakeRole_, diff --git a/games/hp_killer/occupation.h b/games/hp_killer/occupation.h index c823ef97..6b4dffa6 100644 --- a/games/hp_killer/occupation.h +++ b/games/hp_killer/occupation.h @@ -10,6 +10,7 @@ ENUM_BEGIN(Occupation) ENUM_MEMBER(Occupation, 刺客) ENUM_MEMBER(Occupation, 双子(邪)) ENUM_MEMBER(Occupation, 魔女) + ENUM_MEMBER(Occupation, 囚犯) // civilian team ENUM_MEMBER(Occupation, 平民) ENUM_MEMBER(Occupation, 圣女) diff --git a/games/hp_killer/rule.md b/games/hp_killer/rule.md index de107f8b..449aab4e 100644 --- a/games/hp_killer/rule.md +++ b/games/hp_killer/rule.md @@ -54,6 +54,7 @@ - 恶灵:死亡后仍可行动,直到被除灵 - 刺客:可以造成群体伤害,但是伤害会变低 - 魔女:可以诅咒角色,令其每回合流失 5 点 HP,直到其受到攻击 +- 囚犯:可以对目标造成重伤,使其在本回合内无法受到治愈 #### 平民阵营 @@ -85,13 +86,16 @@ - 杀手 + 替身 + 内奸 + 侦探 + 圣女 + 平民 * 2 - 杀手 + 替身 + 特工 + 侦探 + 圣女 + 平民 * 2 - 8 人场: - - 杀手 + 替身 + 恶灵 + 侦探 + 圣女 + 灵媒 + 平民 * 2 + - 杀手 + 替身 + 恶灵 + 侦探 + 圣女 + 灵媒 + 平民 * 2【移除】 - 杀手 + 替身 + 刺客 + 侦探 + 圣女 + 守卫 + 平民 * 2 + 人偶(NPC) - 杀手 + 替身 + 魔女 + 侦探 + 圣女 + 骑士 + 平民 * 2 + - 杀手 + 替身 + 囚犯 + 侦探 + 圣女 + 骑士 + 平民 * 2 - 9 人场: - 杀手 + 替身 + 恶灵 + 内奸 + 侦探 + 圣女 + 灵媒 + 平民 * 2 - 杀手 + 替身 + 刺客 + 内奸 + 侦探 + 圣女 + 守卫 + 平民 * 2 - 杀手 + 替身 + 魔女 + 内奸 + 侦探 + 圣女 + 骑士 + 平民 * 2 - - 杀手 + 替身 + 恶灵 + 特工 + 侦探 + 圣女 + 灵媒 + 平民 * 2 + - 杀手 + 替身 + 囚犯 + 内奸 + 侦探 + 圣女 + 骑士 + 平民 * 2 + - 杀手 + 替身 + 恶灵 + 特工 + 侦探 + 圣女 + 灵媒 + 平民 * 2【移除】 - 杀手 + 替身 + 刺客 + 特工 + 侦探 + 圣女 + 守卫 + 平民 * 2 - 杀手 + 替身 + 魔女 + 特工 + 侦探 + 圣女 + 骑士 + 平民 * 2 + - 杀手 + 替身 + 囚犯 + 特工 + 侦探 + 圣女 + 骑士 + 平民 * 2 diff --git a/games/long_night/board.h b/games/long_night/board.h index 82ae87bc..d094895a 100644 --- a/games/long_night/board.h +++ b/games/long_night/board.h @@ -332,7 +332,18 @@ class Board all_record += "
[BOSS] 米诺陶斯"; all_record += boss.all_record + "
"; } - return regex_replace(all_record, regex(R"(\]\()"), "] ("); + return clean_markdown(all_record); + } + + static string clean_markdown(const string& input) + { + string result = input; + result = regex_replace(result, regex(R"(\*)"), R"(\*)"); + result = regex_replace(result, regex(R"(_)"), R"(\_)"); + result = regex_replace(result, regex(R"(\[)"), R"(\[)"); + result = regex_replace(result, regex(R"(\])"), R"(\])"); + result = regex_replace(result, regex(R"(`)"), R"(\`)"); + return result; } string GetAllScore() const @@ -366,9 +377,6 @@ class Board // 玩家移动 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; @@ -389,10 +397,10 @@ class Board } // 轨迹记录 if (wall && players[pid].subspace < 0) { - if (!hide) players[pid].move_record += hit[d]; + if (!hide) players[pid].NewStepRecord(direction, "撞"); return false; } - if (!hide) players[pid].move_record += arrow[d]; + if (!hide) players[pid].NewStepRecord(direction); if (players[pid].subspace > 0) { players[pid].subspace--; diff --git a/games/long_night/grid.h b/games/long_night/grid.h index 1b0640f0..41744989 100644 --- a/games/long_night/grid.h +++ b/games/long_night/grid.h @@ -131,17 +131,82 @@ class Player // 抓捕目标 PlayerID target; // 移动相关 - int subspace = -1; // 亚空间剩余步数 - string move_record; // 当前回合行动轨迹 - string all_record; // 历史回合行动轨迹 - string private_record; // 当前回合私信记录 - int hide_remaining = 0; // 隐匿剩余次数 - bool inHeatZone = false;// 在热源区块内 - bool heated = false; // 两次烫伤出局 + int subspace = -1; // 亚空间剩余步数 + string all_record; // 历史回合行动轨迹 + string private_record; // 当前回合私信记录 + int hide_remaining = 0; // 隐匿剩余次数 + bool inHeatZone = false; // 在热源区块内 + bool heated = false; // 两次烫伤出局 // 挂机状态(等待时间缩减) bool hook_status = false; // 玩家分数 Score score; + + // 当前回合行动轨迹 + struct Move { + int direct; + int sound; + pair content; + }; + vector move_record; + + void NewStepRecord(const Direct direct, const string& end = "") { move_record.push_back({static_cast(direct), 0, {end, true}}); } + void UpdateSoundRecord(const Sound sound) { if (!move_record.empty()) move_record.back().sound = static_cast(sound); } + void UpdateEndRecord(const string& content) { if (!move_record.empty()) move_record.back().content = {content, true}; } + void NewContentRecord(const string& content) { move_record.push_back({-1, 0, {content, false}}); } + void ClearMoveRecord() { move_record.clear(); } + + string GetMoveRecord() + { + string result; + const size_t N = move_record.size(); + size_t i = 0; + while (i < N) { + const Move& mv = move_record[i]; + // 能否参与合并:同方向、无声效、无额外内容 + bool mergeable = (mv.direct >= 0 && mv.sound == 0 && mv.content.first.empty()); + if (mergeable) { + size_t j = i, count = 0; + while (j < N) { + const Move& next = move_record[j]; + if (next.direct == mv.direct && next.sound == 0 && next.content.first.empty()) { + count++; j++; + } else { + break; + } + } + if (count >= 3) { // 3 次及以上合并 + result += dirSymbol(mv.direct) + "*" + to_string(count); + i = j; + continue; + } + } + result += formatSingle(mv); + i++; + } + return result; + } + + static string dirSymbol(int d) + { + switch (d) { + case 0: return "↑"; + case 1: return "↓"; + case 2: return "←"; + case 3: return "→"; + default: return ""; + } + } + + static string formatSingle(const Move& mv) + { + string d = dirSymbol(mv.direct); + if (mv.sound == 1) return "[" + d + "沙沙]"; + else if (mv.sound == 2) return "[" + d + "啪啪]"; + else if (!mv.content.first.empty()) { + return mv.content.second ? "(" + d + mv.content.first + ")" : mv.content.first; + } else return d; + } }; diff --git a/games/long_night/mygame.cc b/games/long_night/mygame.cc index 6b74dc67..8c0f50d2 100644 --- a/games/long_night/mygame.cc +++ b/games/long_night/mygame.cc @@ -263,38 +263,6 @@ class MainStage : public MainGameStage 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; -/* *********************************************************** */ } }; @@ -339,7 +307,7 @@ class RoundStage : public SubGameStage<> 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 = ""; + Main().board.players[pid].ClearMoveRecord(); if (pid != currentPlayer) { Global().SetReady(pid); } @@ -419,13 +387,13 @@ class RoundStage : public SubGameStage<> Grid& grid = Main().board.grid_map[player.x][player.y]; // [逃生舱] if (grid.Type() == GridType::EXIT) { - player.move_record += "(逃生)"; + player.UpdateEndRecord("逃生"); 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 += "【传送】"; + player.NewContentRecord("【传送】"); Main().board.TeleportPlayer(player.pid); // 大乱斗随机传送 sender << UnitMaps::RandomHint(UnitMaps::exit_hints) << "\n\n您已抵达【逃生舱】!此逃生舱已失效," << At(player.pid) << " 被随机传送至地图其他地方!"; } else { @@ -467,7 +435,7 @@ class RoundStage : public SubGameStage<> if (grid.Type() == GridType::TRAP) { grid.TrapTrigger(); if (grid.TrapStatus()) { - player.move_record += "(陷阱)"; + player.UpdateEndRecord("陷阱"); sender << UnitMaps::RandomHint(UnitMaps::trap_hints) << "\n\n移动触发【陷阱】,本回合被强制停止行动!"; return true; } @@ -480,7 +448,7 @@ class RoundStage : public SubGameStage<> } if (grid.Type() == GridType::HEAT) { if (player.heated) { - player.move_record += "(热源)"; + player.UpdateEndRecord("热源"); sender << UnitMaps::RandomHint(UnitMaps::heat_active_hints) << "\n\n您本局游戏已进入过【热源】,高温难耐,本回合无法继续前进!"; return true; } else { @@ -516,7 +484,7 @@ class RoundStage : public SubGameStage<> if (hide) { sender << "移动进入【树丛】(隐匿中,不会向其他人发出声响)\n\n"; } else { - player.move_record += "[沙沙]"; + player.UpdateSoundRecord(sound); sender << UnitMaps::RandomHint(UnitMaps::grass_hints) << "\n移动进入【树丛】,请其他玩家留意私信声响信息!\n\n"; SendSoundMessage(player.x, player.y, sound, false); } @@ -524,7 +492,7 @@ class RoundStage : public SubGameStage<> if (hide) { sender << "移动发出【啪啪声】(隐匿中,不会向其他人发出声响)\n\n"; } else { - player.move_record += "[啪啪]"; + player.UpdateSoundRecord(sound); sender << UnitMaps::RandomHint(UnitMaps::papa_hints) << "\n移动发出【啪啪声】,请其他玩家留意私信声响信息!\n\n"; SendSoundMessage(player.x, player.y, sound, false); } @@ -558,7 +526,7 @@ class RoundStage : public SubGameStage<> } return StageErrCode::FAILED; } - if (!hide) player.move_record += "(停止)"; + if (!hide) player.NewContentRecord("(停止)"); active_stop = true; reply() << "您选择主动停止行动,本回合结束!主动停止无法获得四周墙壁信息"; return StageErrCode::READY; @@ -595,10 +563,10 @@ class RoundStage : public SubGameStage<> hide = true; player.hide_remaining--; if (GAME_OPTION(隐匿) == 1) { - player.move_record += "【隐匿行动】"; + player.NewContentRecord("【隐匿行动】"); reply() << "使用隐匿技能,本回合剩余时间转为私聊行动,不会发出声响,不会触发捕捉。剩余次数:" << player.hide_remaining; } else if (GAME_OPTION(隐匿) == 2) { - player.move_record += "�"; + player.NewContentRecord("�"); reply() << "使用隐匿技能,下一步请在私聊行动,不会发出声响,不会触发捕捉。剩余次数:" << player.hide_remaining; } return StageErrCode::OK; @@ -617,10 +585,10 @@ class RoundStage : public SubGameStage<> 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[" << pid.Get() << "号]本回合行动轨迹:\n" << Main().board.players[pid].GetMoveRecord(); } } - sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].move_record; + sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].GetMoveRecord(); } else { sender << Main().board.players[pid].private_record; } @@ -637,7 +605,7 @@ class RoundStage : public SubGameStage<> sender << "本局游戏地图为 " << GAME_OPTION(边长) << "x" << GAME_OPTION(边长) << "\n"; } sender << Markdown(Main().board.GetAllRecord(), 500); - sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].move_record; + sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].GetMoveRecord(); if (!is_public) { sender << "\n\n" << Main().board.players[pid].private_record; } @@ -654,22 +622,23 @@ class RoundStage : public SubGameStage<> sender << At(t) << " 被捕捉!"; if (Main().round_ == 1) { - player.move_record += "(首轮捕捉)【传送】"; - Main().board.players[t].all_record += (Main().board.players[t].all_record == "" ? "
" : "") + string("【首轮被抓传送】"); + if (GAME_OPTION(点杀)) player.NewContentRecord("(首轮捕捉)"); else player.UpdateEndRecord("首轮捕捉"); + player.NewContentRecord("【传送】"); + Main().board.players[t].all_record += (Main().board.players[t].all_record.empty() ? "
" : "") + string("【首轮被抓传送】"); Main().board.TeleportPlayer(t); // 随机传送被捉方 Main().board.TeleportPlayer(player.pid); // 随机传送捕捉方 sender << "\n【首轮玩家保护】\n首轮捕捉不生效:双方均被随机传送至地图其他地方!"; return true; } - player.move_record += "(捕捉)"; + if (GAME_OPTION(点杀)) player.NewContentRecord("(捕捉)"); else player.UpdateEndRecord("捕捉"); 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 += "【传送】"; + player.NewContentRecord("【传送】"); Main().board.TeleportPlayer(player.pid); // 随机传送捕捉方 Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 sender << "\n" << At(player.pid) << " 被随机传送至地图其他地方,捕捉目标顺位发生变更!\n"; @@ -719,7 +688,7 @@ class RoundStage : public SubGameStage<> virtual CheckoutErrCode OnStageTimeout() override { - Main().board.players[currentPlayer].move_record += "(超时)"; + Main().board.players[currentPlayer].NewContentRecord("(超时)"); active_stop = true; if (step == 0) { Main().board.players[currentPlayer].hook_status = true; @@ -766,9 +735,9 @@ class RoundStage : public SubGameStage<> PlayerCatch(player, sender); } } - Global().Boardcast() << "[" << currentPlayer.Get() << "号]玩家本回合的完整行动轨迹:\n" << player.move_record; + Global().Boardcast() << "[" << currentPlayer.Get() << "号]玩家本回合的完整行动轨迹:\n" << player.GetMoveRecord(); // 记录历史行动轨迹 - player.all_record += "
【第 " + to_string(Main().round_) + " 回合】
" + player.move_record; + player.all_record += "
【第 " + to_string(Main().round_) + " 回合】
" + player.GetMoveRecord(); // 仅剩1玩家,游戏结束 if ((Main().Alive_() == 1 && Global().PlayerNum() > 1) || (Main().Alive_() == 0 && Global().PlayerNum() == 1)) { if (Main().withoutE_win_) { // 无逃生舱最后生还胜利 diff --git a/games/nerduel/mygame.cc b/games/nerduel/mygame.cc index 0ebacddc..23165d0c 100644 --- a/games/nerduel/mygame.cc +++ b/games/nerduel/mygame.cc @@ -321,6 +321,9 @@ void MainStage::NextStageFsm(GuessingStage& sub_stage, return; } if (JudgeOver()) { + Global().Boardcast() << "本局游戏双方等式: \n" + << target_[0] << "\n" + << target_[1]; return; } table_.AddLine(); diff --git a/games/nerduel/rule.md b/games/nerduel/rule.md index e295d2c8..e4002076 100644 --- a/games/nerduel/rule.md +++ b/games/nerduel/rule.md @@ -1,4 +1,4 @@ -## Nerdual +## Nerduel - **原作:** dva - **游戏人数:** 2 diff --git a/games/six_nimmt/rule.md b/games/six_nimmt/rule.md index c2fbc112..46c7a073 100644 --- a/games/six_nimmt/rule.md +++ b/games/six_nimmt/rule.md @@ -8,6 +8,7 @@ - 包含数字1-104的卡牌每种一张,卡牌上的牛头数符合以下规律:5的倍数**2个牛头**;10的倍数**3个牛头**;11的倍数**5个牛头**;55的倍数**7个牛头**;其余卡牌均为**1个牛头** ### 游戏流程 +- 游戏的目标为尽可能获得**更少**的牛头,**每个牛头都会让自己失去分数** - 游戏开始时,每个玩家发10张牌,并从剩下牌库抽出依照大小顺序4张摆在桌面,当作牌阵,剩下的牌不使用。 - 每回合所有玩家选择一张手牌打出。根据打出的牌,依照顺序从小到大轮流放置在桌面上4张牌阵中 - 在牌阵中放置时遵循以下规则: