From 7ad7db737ceaae921735701f52d12a8f84243326 Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Wed, 3 Sep 2025 23:47:14 -0400 Subject: [PATCH 01/12] six_nimmt: update rule.md --- games/six_nimmt/rule.md | 1 + 1 file changed, 1 insertion(+) 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张牌阵中 - 在牌阵中放置时遵循以下规则: From 9e68c81d7d4525ea360192a761e2fc22a054fe3e Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Wed, 3 Sep 2025 23:50:05 -0400 Subject: [PATCH 02/12] blocked_road: add InitOptionsCommand to set num and size --- games/blocked_road/mygame.cc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/games/blocked_road/mygame.cc b/games/blocked_road/mygame.cc index c6e3d1bc..a2364a8f 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) { From 30463c0aa92f3b275d473f0290a6d540e30814aa Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Wed, 3 Sep 2025 23:54:04 -0400 Subject: [PATCH 03/12] hp_killer: add prisoner --- games/hp_killer/mygame.cc | 140 ++++++++++++++++++++++++++++++----- games/hp_killer/occupation.h | 1 + games/hp_killer/rule.md | 8 +- 3 files changed, 128 insertions(+), 21 deletions(-) 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 From 7fb78c0b5de9d914a575a4e4a7a26eb260cdca46 Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Thu, 4 Sep 2025 00:03:44 -0400 Subject: [PATCH 04/12] long_night: refactor player move record --- games/long_night/board.h | 20 +++++++--- games/long_night/grid.h | 79 ++++++++++++++++++++++++++++++++++---- games/long_night/mygame.cc | 73 ++++++++++------------------------- 3 files changed, 107 insertions(+), 65 deletions(-) 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_) { // 无逃生舱最后生还胜利 From 90a1ff972aa7c624199050c6f5fba6d74f0472f8 Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Thu, 4 Sep 2025 00:10:19 -0400 Subject: [PATCH 05/12] garnet_thief: new game --- games/garnet_thief/achievements.h | 0 games/garnet_thief/icon.png | Bin 0 -> 17182 bytes games/garnet_thief/mygame.cc | 523 ++++++++++++++++++++++++++++++ games/garnet_thief/option.cmake | 0 games/garnet_thief/options.h | 3 + games/garnet_thief/rule.md | 25 ++ games/garnet_thief/unittest.cc | 49 +++ 7 files changed, 600 insertions(+) create mode 100644 games/garnet_thief/achievements.h create mode 100644 games/garnet_thief/icon.png create mode 100644 games/garnet_thief/mygame.cc create mode 100644 games/garnet_thief/option.cmake create mode 100644 games/garnet_thief/options.h create mode 100644 games/garnet_thief/rule.md create mode 100644 games/garnet_thief/unittest.cc 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 0000000000000000000000000000000000000000..94fe487e06a2b06190d7da620ca32ebdba31e263 GIT binary patch literal 17182 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. 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(); +} From 358aaba0344870d424df4f7fbb5ef4d1f55049da Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Sat, 4 Oct 2025 21:17:38 -0400 Subject: [PATCH 06/12] nerduel: show both equations at game end and fix typo in rules --- games/nerduel/mygame.cc | 3 +++ games/nerduel/rule.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) 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 From 7ba031b76c58004813fc8299b6f618f8a39f43db Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Sat, 4 Oct 2025 21:55:39 -0400 Subject: [PATCH 07/12] remove redundant semicolon in bot_core --- bot_core/bot_core.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); From 640dedc5ca5994e9604d77392a3dd77cc9a01f0f Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Sat, 4 Oct 2025 22:13:55 -0400 Subject: [PATCH 08/12] othello: prevent placing outside the board --- game_util/othello.h | 3 +++ 1 file changed, 3 insertions(+) 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 From 52fc251490f8ef54577e013ced08296138a5fe36 Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Sat, 4 Oct 2025 22:25:26 -0400 Subject: [PATCH 09/12] quixo: fix incorrect piece count when push fails --- game_util/quixo.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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; From 0166fa46a26765d6b41f9e57fa590d6133e2c413 Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Sat, 4 Oct 2025 22:57:49 -0400 Subject: [PATCH 10/12] dvalue_tender: fix invalid round count option --- games/dvalue_tender/mygame.cc | 15 +++++++-------- games/dvalue_tender/options.h | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) 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) From b08f40efc54f368f9349582520181bbcf88bec01 Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Tue, 7 Oct 2025 01:39:28 -0400 Subject: [PATCH 11/12] fix Windows gflags compilation error in GitHub Actions --- .github/workflows/cmake.yml | 15 ++++++++++++--- CMakeLists.txt | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) 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() From 3779d25b26a770df322b0b01b9075ffb2bff8039 Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Tue, 7 Oct 2025 23:21:44 -0400 Subject: [PATCH 12/12] blocked_road: fix infinite loop issue in computer move logic --- games/blocked_road/mygame.cc | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/games/blocked_road/mygame.cc b/games/blocked_road/mygame.cc index a2364a8f..1722be5e 100644 --- a/games/blocked_road/mygame.cc +++ b/games/blocked_road/mygame.cc @@ -345,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;