From 4c1fe85bdbd91c7fbd76de418b0c7ba9d1b39e14 Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Tue, 25 Nov 2025 01:45:04 -0500 Subject: [PATCH 01/12] fix is_formal config not loading on restart --- bot_core/bot_ctx.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot_core/bot_ctx.cc b/bot_core/bot_ctx.cc index 3776647..047d518 100644 --- a/bot_core/bot_ctx.cc +++ b/bot_core/bot_ctx.cc @@ -223,6 +223,10 @@ static std::variant LoadConfig(const char* const co ErrorLog() << "LoadConfig set game '" << game_name << "' option failed: " << option_str; } } + if (game_json.contains("is_formal")) { + locked_option->generic_options_.is_formal_ = game_json["is_formal"].get(); + InfoLog() << "LoadConfig set game '" << game_name << "' is_formal: " << static_cast(locked_option->generic_options_.is_formal_); + } } return j; } From 5c59c4b78f7c2ce6a010fb7683425f459a87c4bc Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Wed, 31 Dec 2025 18:16:22 -0500 Subject: [PATCH 02/12] fix auto-deduction triggered when all players eliminated without computer players --- bot_core/match.cc | 6 +++++- game_framework/stage_utility.h | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bot_core/match.cc b/bot_core/match.cc index 86c34bf..afabf24 100644 --- a/bot_core/match.cc +++ b/bot_core/match.cc @@ -522,8 +522,12 @@ void Match::Eliminate(const PlayerID pid) { if (std::exchange(players_[pid].state_, Player::State::ELIMINATED) != Player::State::ELIMINATED) { Tell(pid) << "很遗憾,您被淘汰了,可以通过「" META_COMMAND_SIGN "退出」以退出游戏"; - is_in_deduction_ = std::ranges::all_of(players_, + // Enter deduction mode only if all players are eliminated AND there is at least one alive computer player + const bool all_players_eliminated = std::ranges::all_of(players_, [](const auto& p) { return std::get_if(&p.id_) || p.state_ == Player::State::ELIMINATED; }); + const bool has_alive_computer = std::ranges::any_of(players_, + [](const auto& p) { return std::get_if(&p.id_) && p.state_ != Player::State::ELIMINATED; }); + is_in_deduction_ = all_players_eliminated && has_alive_computer; MatchLog_(InfoLog()) << "Eliminate player pid=" << pid << " is_in_deduction=" << Bool2Str(is_in_deduction_); } } diff --git a/game_framework/stage_utility.h b/game_framework/stage_utility.h index af2ccd0..34bb049 100644 --- a/game_framework/stage_utility.h +++ b/game_framework/stage_utility.h @@ -95,7 +95,8 @@ class PublicStageUtility private: // TODO: Stage should identify which players are computers. `match_.IsInDeduction()` condition should be removed. - bool IsInDeduction() const { return match_.IsInDeduction() || masker_.IsAllPermanentInactive(); } + // bool IsInDeduction() const { return match_.IsInDeduction() || masker_.IsAllPermanentInactive(); } + bool IsInDeduction() const { return match_.IsInDeduction(); } void Leave(const PlayerID pid); From 0afabd6b3f5c6a0a775284cabf2e28dd21ba24ec Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Wed, 31 Dec 2025 19:15:11 -0500 Subject: [PATCH 03/12] fix duplicate deduction message output during player elimination --- game_framework/stage_utility.cc | 3 ++- game_framework/stage_utility.h | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/game_framework/stage_utility.cc b/game_framework/stage_utility.cc index 1387aa9..3d93985 100644 --- a/game_framework/stage_utility.cc +++ b/game_framework/stage_utility.cc @@ -102,7 +102,8 @@ void PublicStageUtility::StopTimer() void PublicStageUtility::Leave(const PlayerID pid) { masker_.SetPermanentInactive(pid); - if (IsInDeduction()) { + if (IsInDeduction() && !is_in_deduction_) { + is_in_deduction_ = true; match_.GroupMsgSender()() << "所有玩家都失去了行动能力,于是游戏将直接推演至终局"; } } diff --git a/game_framework/stage_utility.h b/game_framework/stage_utility.h index 34bb049..0099bb6 100644 --- a/game_framework/stage_utility.h +++ b/game_framework/stage_utility.h @@ -111,6 +111,7 @@ class PublicStageUtility int32_t bot_message_id_{0}; // the ID of each bot message int32_t saved_image_no_{0}; std::optional> timer_finish_time_; + bool is_in_deduction_{false}; }; class AtomicStage; From 7109fcdc72df1ee5c5e5b8c00c52c88df0e49942 Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Fri, 6 Feb 2026 22:49:46 -0500 Subject: [PATCH 04/12] escape_building: new game --- games/escape_building/achievements.h | 7 + games/escape_building/hints.h | 122 +++++ games/escape_building/icon.png | Bin 0 -> 5389 bytes games/escape_building/mygame.cc | 715 +++++++++++++++++++++++++++ games/escape_building/option.cmake | 0 games/escape_building/options.h | 7 + games/escape_building/rule.md | 23 + games/escape_building/unittest.cc | 191 +++++++ 8 files changed, 1065 insertions(+) create mode 100644 games/escape_building/achievements.h create mode 100644 games/escape_building/hints.h create mode 100644 games/escape_building/icon.png create mode 100644 games/escape_building/mygame.cc create mode 100644 games/escape_building/option.cmake create mode 100644 games/escape_building/options.h create mode 100644 games/escape_building/rule.md create mode 100644 games/escape_building/unittest.cc diff --git a/games/escape_building/achievements.h b/games/escape_building/achievements.h new file mode 100644 index 0000000..41900af --- /dev/null +++ b/games/escape_building/achievements.h @@ -0,0 +1,7 @@ +// 游戏成就(未实现) +// EXTEND_ACHIEVEMENT(闪电终结, "在到达第 10 楼前提前获得胜利") +// EXTEND_ACHIEVEMENT(致命精准, "杀手出刀全数命中,无一次落空") +// EXTEND_ACHIEVEMENT(风平浪静, "全程未看到任何黑影,独自撤离大楼") +// EXTEND_ACHIEVEMENT(超额解救, "成功解救 4 名人质并安全撤离大楼") +// EXTEND_ACHIEVEMENT(满载而归, "成功解救 5 名人质并安全撤离大楼") +// EXTEND_ACHIEVEMENT(完美救援, "成功接到杀手弹出的所有人质并撤离大楼") \ No newline at end of file diff --git a/games/escape_building/hints.h b/games/escape_building/hints.h new file mode 100644 index 0000000..fd572a3 --- /dev/null +++ b/games/escape_building/hints.h @@ -0,0 +1,122 @@ +/* ========== 游戏文案 ========== */ +std::string GetRandomHint(std::span hints) { + return std::string(hints[rand() % hints.size()]); +} + +// 黑影 +const std::array shadow_hints = { + "电梯门缓缓开启……一道【黑影】疾冲而来,空气像被利刃划开!", + "电梯门缓缓开启……一道【黑影】闪了过来!", + "电梯门缓缓开启……一道【黑影】从走廊深处扑向门口,速度快得令人心悸!", + "电梯门像缓慢呼吸般开启……一道【黑影】一闪而过!", + "电梯门缓缓开启……【黑影】从走廊尽头疾冲而来,黑暗里隐藏着谜一样的意图。", + "电梯门缓缓开启……视线晃动,一道【黑影】迎面扑来!", + "电梯门缓缓开启……走廊尽头的【黑影】突然加速,直冲而来!", + "电梯门缓缓开启……【黑影】在灯光下拉长,随即冲进你的视线。", +}; + +// 烟雾弹 +const std::array smoke_hints = { + "电梯门缓缓开启……浓烟瞬间吞没视线,什么也看不清。", + "电梯门缓缓开启……烟雾弥漫,视线被封锁。", + "电梯门缓缓开启……眼前骤起浓烟,世界瞬间模糊。", + "电梯门缓缓开启……浓烟弥漫,眼前一片模糊。", + "电梯门缓缓开启……烟雾翻滚,所有轮廓都被抹去。", + "电梯门缓缓开启……灰白色的烟雾占据了一切。", +}; + +// 无事发生 +const std::array empty_hints = { + "电梯门缓缓开启……走廊空无一人,只有死一般的寂静。", + "电梯门缓缓开启……空气凝固,却没有任何人出现。", + "电梯门缓缓开启……你屏住呼吸,却什么也没发生。", + "电梯门缓缓开启……黑暗静止不动,一切归于沉默。", + "电梯门缓缓开启……紧张蔓延开来,却没有任何动静。", + "电梯门缓缓开启……你盯着前方,却只看到空荡的走廊。", + "电梯门缓缓开启……走廊安静得令人不安。", + "电梯门缓缓开启……没有危险,也没有答案。", +}; + +// 击杀三个人质 +const std::array police_lose_hints = { + "枪声再次响起,人质倒下。这是第三个。\n你站在原地,却已经无法分辨敌我。\n任务失败。", + "第三名人质倒下。电梯内一片死寂。\n黑影不再闪现,它们已经住进你的脑海。\n任务失败。", + "又一声枪响,人质倒地。\n判断彻底崩溃,你失去了继续行动的资格。\n任务失败。", + "人质伤亡已无法挽回。\n你的选择导致了不可逆的后果。\n行动被强制终止。", + "第三名人质倒下。\n这一刻,你终于意识到自己已经无法控制局面。\n任务失败。", + "枪声在走廊里回荡。\n这是最后一根防线的崩塌。\n你已无法继续执行任务。", + "人质连续倒下。\n恐惧与误判占据了一切。\n行动宣告失败。", +}; + +// 开枪命中杀手 +const std::array shoot_killer_hints = { + "枪声炸响!子弹命中目标,黑影中弹后踉跄后退。你击中了杀手。", + "枪火撕裂黑暗。子弹准确命中正在逼近的杀手,他发出一声闷哼。", + "你果断开枪。黑影应声受创,杀手被迫停下脚步。", + "子弹击中目标。黑影剧烈晃动,杀手显然受了伤。", + "枪声回荡。这一次,你的判断是正确的——杀手中弹。", + "黑暗被枪火照亮。子弹命中杀手,他的攻势被阻止。" +}; + +// 开枪误杀人质 +const std::array shoot_hostage_hints = { + "枪声回荡。黑影倒下后,你才意识到那是一名人质。", + "你扣下扳机。人质倒地,走廊里只剩下回声。", + "子弹命中目标。短暂的沉默后,你发现自己犯下了错误。", + "枪火熄灭。倒在地上的,并不是杀手。", + "你选择了射击。这一枪,击中了不该被击中的人。", +}; + +// 开枪误杀更多人质 +const std::array shoot_more_hostage_hints = { + "你再次扣下扳机。又一名人质倒下,黑影不再消失,仿佛就在你身边。", + "枪声响起。人质倒地,你的视线开始被挥之不去的黑影占据。", + "判断再次出错。人质死亡,黑影在走廊里频繁闪现。", + "你试图控制局面,却再次失手。人质倒下,幻觉开始侵蚀你的视线。", + "枪火亮起。这一刻,你已经无法确定自己看见的是否真实。", + "人质死亡。恐惧和幻影交织在一起,你的判断愈发混乱。", +}; + +// 开枪没有命中 +const std::array shoot_empty_hints = { + "枪声回荡。子弹射入黑暗,没有命中任何目标。", + "你开枪了。子弹消失在走廊深处,什么也没有发生。", + "枪火短暂亮起。黑暗没有给予任何回应。", + "子弹击中远处的墙壁。这一枪没有改变任何局势。", + "枪声渐渐消散,什么也没有击中。但危险并未因此减少。", +}; + +// 接被杀手刺伤 +const std::array killer_stab_hints = { + "你放下了枪。寒光一闪,利刃入体!你被杀手刺伤!", + "你来不及反应。黑影贴近,利刃狠狠刺入身体。", + "距离被瞬间拉近。下一秒,疼痛袭来——杀手得手了。", + "你试图后退,但已经太迟。杀手的刀锋划破了你的防线。", +}; + +// 解救人质 +const std::array rescue_hostage_hint = { + "你伸开手臂,将对方拉回电梯。成功解救了一名人质。", + "黑影停下了脚步,那是一名惊魂未定的人质。", + "你确认了对方的身份。人质被安全带离危险区域。", + "紧张的气氛中,你做出了正确的选择。人质获救。", +}; + +// 什么都没有接到 +const std::array get_empty_hint = { + "你放下了枪。外面什么也没有,电梯内一片死寂。", + "你等待着。走廊空无一人,只有自己的呼吸声。", + "什么都没有发生。紧张却没有随之消散。", + "电梯门外一片安静。你什么也没接到。", + "你站在原地,什么也没有发生,只剩下心跳声在回荡。", +}; + +// 无子弹被杀手刺伤 +const std::array no_bullet_killer_stab_hints = { + "你尝试扣动扳机,却只听见空响——子弹已经耗尽,杀手的利刃刺入了你的身体!", +}; + +// 无子弹遇到烟雾弹 +const std::array no_bullet_smoke_hints = { + "浓烟翻滚,却没有任何人出现。", +}; diff --git a/games/escape_building/icon.png b/games/escape_building/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..24179967c4eab09499e5719139b369e35a6e05dd GIT binary patch literal 5389 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 literal 0 HcmV?d00001 diff --git a/games/escape_building/mygame.cc b/games/escape_building/mygame.cc new file mode 100644 index 0000000..ec233a6 --- /dev/null +++ b/games/escape_building/mygame.cc @@ -0,0 +1,715 @@ +// Copyright (c) 2018-present, JiaQi Yu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include +#include + +#include "hints.h" + +#include "game_framework/stage.h" +#include "game_framework/util.h" +#include "utility/html.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +class MainStage; +template using SubGameStage = StageFsm; +template using MainGameStage = StageFsm; + +const GameProperties k_properties { + .name_ = "逃脱大楼", + .developer_ = "铁蛋", + .description_ = "堕入无边地狱解救人质和杀死罪犯的传奇故事", + .shuffled_player_id_ = true, +}; +uint64_t MaxPlayerNum(const CustomOptions& options) { return 2; } +uint32_t Multiple(const CustomOptions& options) { return 1; } +const MutableGenericOptions k_default_generic_options{ + .is_formal_{false}, +}; +const std::vector k_rule_commands = {}; + +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +{ + if (generic_options_readonly.PlayerNum() != 2) { + reply() << "该游戏为双人游戏,必须为 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); + return false; + } + return true; +} + +const std::vector k_init_options_commands = { + InitOptionsCommand("独自一人开始游戏", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) + { + generic_options.bench_computers_to_player_num_ = 2; + return NewGameMode::SINGLE_USER; + }, + VoidChecker("单机")), + InitOptionsCommand("配置游戏初始楼层高度", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const uint32_t floor) { + GET_OPTION_VALUE(game_options, 楼层) = floor; + return NewGameMode::MULTIPLE_USERS; + }, + ArithChecker(10, 50, "层数")), +}; + +// Role +enum class Role { + POLICE, + KILLER +}; + +// ========== GAME STAGES ========== + +class RoundStage; +class ShootStage; + +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_hp_(Global().PlayerNum(), 2) + {} + + virtual int64_t PlayerScore(const PlayerID pid) const override { return player_scores_[pid]; } + + std::vector player_scores_; + + // 回合数 + int round_; + // 玩家信息 + std::vector player_role_; + std::vector player_hp_; + + // 玩家行动 + int64_t police_select; + bool shoot; + int64_t hostage_floor = -1; + int64_t knife_floor = -1; + + // 游戏状态 + int64_t police_floor; // 警察楼层 + int64_t bullets; // 剩余子弹 + int64_t killed_hostages = 0; // 误杀人质数 + int64_t rescued_hostages = 0; // 解救人质数 + bool smoke_used = false; // 烟雾弹已被使用 + bool smoke_trigger = false; // 下回合触发烟雾弹 + bool police_insane = false; // 精神错乱 + + struct FloorInfo { + bool hostage_used = false; // 是否已经放置过人质 + std::string police_records; // 警察本层记录 + std::string killer_records; // 杀手本层记录 + + void NewKillerRecord(const std::string record) { + if (!killer_records.empty()) killer_records += "
"; + killer_records += record; + } + void NewPoliceRecord(const std::string record) { + if (!police_records.empty()) police_records += "
"; + police_records += record; + } + void UpdatePoliceRecord(const std::string record) { + police_records += record; + } + }; + std::vector floors; + + PlayerID RoleID(const Role role) const { if (role == Role::KILLER) return 0; else return 1; } + + // 赛况表格 + std::string GetTable(const bool show) const + { + html::Table playerTable(2, 4); + playerTable.SetTableStyle("align=\"center\" cellpadding=\"2\""); + for (int pid = 0; pid < 2; pid++) { + playerTable.Get(pid, 0).SetStyle("style=\"width:40px;\"").SetContent(Global().PlayerAvatar(pid, 40)); + playerTable.Get(pid, 1).SetStyle("style=\"width:250px; text-align:left;\"").SetContent(Global().PlayerName(pid)); + playerTable.Get(pid, 2).SetStyle("style=\"width:50px;\"") + .SetContent(player_role_[pid] == Role::POLICE ? "[警察]" : "[杀手]"); + playerTable.Get(pid, 3).SetStyle("style=\"width:80px;\"") + .SetContent("血量:" + std::to_string(player_hp_[pid]) + ""); + } + + const std::string no_bullet = bullets == 0 ? " background-color: #c9c9c9;" : ""; + const std::string get_hostages = rescued_hostages >= GAME_OPTION(解救人质) ? " background-color: #d9f2e6;" : ""; + const std::string insane = killed_hostages >= 2 ? " background-color: #f8d6d6;" : ""; + html::Table extra(1, 3); + extra.SetTableStyle("style=\"margin: 12px auto;\""); + extra.Get(0, 0).SetStyle("style=\"width:140px;" + no_bullet + "\"") + .SetContent("剩余子弹:" + std::to_string(bullets) + ""); + extra.Get(0, 1).SetStyle("style=\"width:140px;" + get_hostages + "\"") + .SetContent("解救人质:" + std::to_string(rescued_hostages) + " / " + std::to_string(GAME_OPTION(解救人质))); + extra.Get(0, 2).SetStyle("style=\"width:140px;" + insane + "\"") + .SetContent("误杀人质:" + std::to_string(killed_hostages) + " / 3"); + + html::Table table(GAME_OPTION(楼层) + 1, show ? 3 : 2); + table.Get(0, 0).SetStyle("style=\"width:50px;\"").SetContent("楼层"); + table.Get(0, 1).SetStyle("style=\"width:180px;\"").SetContent("[警察]"); + if (show) table.Get(0, 2).SetStyle("style=\"width:180px;\"").SetContent("[杀手]"); + for (int i = 0; i < GAME_OPTION(楼层); i++) { + int floor = GAME_OPTION(楼层) - i; + table.Get(i + 1, 0).SetContent(std::to_string(floor)); + table.Get(i + 1, 1).SetContent(floors[floor].police_records); + if (show) table.Get(i + 1, 2).SetContent(floors[floor].killer_records); + } + return "### 第 " + std::to_string(round_) + " 回合" + playerTable.ToString() + extra.ToString() + table.ToString(); + } + + private: + CompReqErrCode Status_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + if (!is_public && player_role_[pid] == Role::KILLER) { + reply() << Markdown(GetTable(true)); + } else { + reply() << Markdown(GetTable(false)); + } + return StageErrCode::OK; + } + + void FirstStageFsm(SubStageFsmSetter setter) + { + player_role_.push_back(Role::KILLER); + player_role_.push_back(Role::POLICE); + Global().Boardcast() << "本局游戏双方玩家身份:\n" + << "杀手:" << At(RoleID(Role::KILLER)) << "\n" + << "警察:" << At(RoleID(Role::POLICE)); + + floors.resize(GAME_OPTION(楼层) + 1); + floors[GAME_OPTION(楼层)].NewPoliceRecord("[R0] 警察开始行动"); + + player_hp_[0] = GAME_OPTION(杀手血量); + player_hp_[1] = GAME_OPTION(警察血量); + police_floor = GAME_OPTION(楼层); + bullets = GAME_OPTION(子弹); + setter.Emplace(*this, ++round_); + } + + void NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) + { + // 超时或强退中途结束 + if (player_scores_[0] == -1 || player_scores_[1] == -1) { + // 终局赛况 + Global().Boardcast() << Markdown(GetTable(true)); + return; + } + // 更新游戏状态 + police_floor = police_select; + floors[police_floor].NewPoliceRecord("[R" + std::to_string(round_) + "] "); + if (hostage_floor > 0) { + floors[hostage_floor].hostage_used = true; + floors[hostage_floor].NewKillerRecord("[R" + std::to_string(round_) + "] 弹射人质"); + } + if (knife_floor > 0) { + floors[knife_floor].NewKillerRecord("[R" + std::to_string(round_) + "] 拿刀出击"); + } + // 黑影 + bool has_shadow = false; + bool encounter_hostage = hostage_floor == police_floor; + bool encounter_killer = knife_floor == police_floor; + if (police_insane) { + has_shadow = true; + } else if (encounter_hostage || encounter_killer) { + has_shadow = true; + } + // 烟雾弹 + bool has_smoke = false; + if (smoke_trigger) { + has_smoke = true; + smoke_used = true; + smoke_trigger = false; + floors[police_floor].NewKillerRecord("[R" + std::to_string(round_) + "] 释放烟雾弹"); + } + // 根据条件开始新阶段 + PlayerID police = RoleID(Role::POLICE); + std::string floor_hint = " 乘坐电梯来到了 " + std::to_string(police_floor) + " 楼,"; + if ((has_shadow || has_smoke) && bullets == 0) { + // 子弹耗尽直接结算 + if (encounter_killer) { + // 被杀手刺伤 + player_hp_[police]--; + floors[police_floor].UpdatePoliceRecord("杀手刺伤"); + Global().Boardcast() << At(police) << floor_hint << GetRandomHint(shadow_hints) << "\n" << GetRandomHint(no_bullet_killer_stab_hints); + } else if (encounter_hostage) { + // 解救人质 + rescued_hostages++; + floors[police_floor].UpdatePoliceRecord("到一名人质"); + Global().Boardcast() << At(police) << floor_hint << GetRandomHint(shadow_hints) << "\n" << GetRandomHint(rescue_hostage_hint); + } else if (has_smoke) { + // 烟雾弹但无事发生 + floors[police_floor].UpdatePoliceRecord("什么都没有到"); + Global().Boardcast() << At(police) << floor_hint << GetRandomHint(smoke_hints) << "\n" << GetRandomHint(no_bullet_smoke_hints); + } else { + // 无事发生 + floors[police_floor].UpdatePoliceRecord("什么都没有到"); + Global().Boardcast() << At(police) << floor_hint << GetRandomHint(shadow_hints) << "\n" << GetRandomHint(get_empty_hint); + } + HandleRoundOver(setter); // 回合结束 + } + else if (has_smoke) { + Global().Boardcast() << At(police) << floor_hint << GetRandomHint(smoke_hints) << "——【烟雾弹】被释放,本回合信息未知!"; + setter.Emplace(*this, round_); + } + else if (has_shadow) { + Global().Boardcast() << At(police) << floor_hint << GetRandomHint(shadow_hints); + setter.Emplace(*this, round_); + } + else { + floors[police_floor].UpdatePoliceRecord("无事发生"); + Global().Boardcast() << At(police) << floor_hint << GetRandomHint(empty_hints); + HandleRoundOver(setter); // 回合结束 + } + } + + void NextStageFsm(ShootStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) + { + HandleRoundOver(setter); // 回合结束 + } + + void HandleRoundOver(SubStageFsmSetter& setter) + { + if (!CheckGameEnd()) { + setter.Emplace(*this, ++round_); + return; + } + // 终局赛况 + Global().Boardcast() << Markdown(GetTable(true)); + } + + bool CheckGameEnd() + { + PlayerID police = RoleID(Role::POLICE); + PlayerID killer = RoleID(Role::KILLER); + // 误杀 3 名人质 + if (killed_hostages >= 3) { + player_scores_[killer] = 1; + Global().Boardcast() << At(police) << " 误杀了 3 名人质,游戏结束!"; + return true; + } + // 生命值归零 + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (player_hp_[pid] <= 0) { + player_scores_[!pid] = 1; + Global().Boardcast() << At(pid) << " 生命值归零,游戏结束!"; + return true; + } + } + // 警察抵达一楼 + if (police_floor == 1) { + if (rescued_hostages >= GAME_OPTION(解救人质)) { + player_scores_[police] = 1; + Global().Boardcast() << "已抵达一楼,游戏结束!" << At(police) << " 成功解救了 " << rescued_hostages << " 名人质,获得胜利!"; + } else { + player_scores_[killer] = 1; + Global().Boardcast() << "已抵达一楼,游戏结束!未能解救 " << GAME_OPTION(解救人质) << " 名人质,杀手 " << At(killer) << " 获得胜利!"; + } + return true; + } + return false; + } + +}; + +class RoundStage : public SubGameStage<> +{ + public: + RoundStage(MainStage& main_stage, const uint64_t round) + : StageFsm(main_stage, "第 " + std::to_string(round) + " 回合", + MakeStageCommand(*this, "[警察] 选择电梯楼层下楼", &RoundStage::PoliceSelect_, + ArithChecker(1, 50, "楼层")), + MakeStageCommand(*this, "[杀手] 选择楼层弹射人质", &RoundStage::KillerHostageSelect_, + AlterChecker({{"人质", 0}, {"1", 0}}), ArithChecker(1, 50, "楼层")), + MakeStageCommand(*this, "[杀手] 选择楼层拿刀主动出击(可选)", &RoundStage::KillerKnifeSelect_, + AlterChecker({{"拿刀", 0}, {"2", 0}}), ArithChecker(1, 50, "楼层")), + MakeStageCommand(*this, "[杀手] 启用烟雾弹(可选,每局限一次)", &RoundStage::KillerSmokeTrigger_, + AlterChecker({{"烟雾弹", 0}, {"y", 0}}), OptionalDefaultChecker(true, "确定", "取消")), + MakeStageCommand(*this, "[杀手] 确认行动,确认后无法修改", &RoundStage::KillerConfirm_, + AlterChecker({{"确认", 0}, {"0", 0}}))) + { + higher_floor = Main().police_floor - 1; + lower_floor = (Main().police_floor - GAME_OPTION(电梯) > 0) ? (Main().police_floor - GAME_OPTION(电梯)) : 1; + } + + int higher_floor; + int lower_floor; + + int t_hostage_floor = -1; + int t_knife_floor = -1; + bool t_smoke_trigger = false; + + virtual void OnStageBegin() override + { + Global().Boardcast() << Markdown(Main().GetTable(false)); + Global().Boardcast() << "本回合能够到达 " << higher_floor << "-" << lower_floor << " 楼,请双方私信裁判行动,时限 " << GAME_OPTION(时限) << " 秒,超时未行动判负" + << (higher_floor == 1 ? "(已抵达 2 楼,本回合不强制放人质)" : ""); + Global().StartTimer(GAME_OPTION(时限)); + std::string hostage_available = GetKillerHostageFloors(); + // 杀手无法安装弹射装置,强制平局 + if (Main().police_floor > 2 && hostage_available.empty()) { + Global().SetReady(0); Global().SetReady(1); + Global().Boardcast() << "[提示] 杀手已无可安装弹射装置的楼层,且当前未抵达 2 楼,满足平局特殊条件,游戏结束!"; + return; + } + Global().Tell(Main().RoleID(Role::KILLER)) << "当前位于 " << Main().police_floor << " 楼,本回合可放置弹射装置的楼层为 " << (hostage_available.empty() ? "无" : hostage_available) + << (higher_floor == 1 ? "(已抵达 2 楼,本回合不强制放人质)" : ""); + } + + private: + std::string GetKillerHostageFloors() const + { + std::string result; + for (int f = higher_floor; f >= lower_floor; f--) { + if (!Main().floors[f].hostage_used) { + if (!result.empty()) result += "、"; + result += std::to_string(f); + } + } + return result; + } + + bool CheckCommon(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const Role role) + { + if (Main().player_role_[pid] != role) { + if (Main().player_role_[pid] == Role::KILLER) { + reply() << "[错误] 您的角色是「杀手」,无法使用此行动。杀手可用指令如下:\n" + << "- 人质 X、拿刀 X、烟雾弹、确认\n" + << "详细指令说明请使用「帮助」指令"; + } else { + reply() << "[错误] 您的角色是「警察」,无法使用此行动。\n" + << "- 请发送单独的数字代表本回合要去的楼层\n" + << "详细指令说明请使用「帮助」指令"; + } + return false; + } + if (is_public) { + reply() << "[错误] 请私信裁判进行行动"; + return false; + } + if (Global().IsReady(pid)) { + reply() << "[错误] 本回合行动已确认,已无退路可言"; + return false; + } + return true; + } + + std::string GetCurrentPlan() const + { + return std::string("本回合行动计划:\n") + + "弹射装置:" + (t_hostage_floor > 0 ? std::to_string(t_hostage_floor) + " 楼" : (Main().police_floor > 2 ? "【尚未部署】" : "未部署")) + "\n" + + "拿刀楼层:" + (t_knife_floor > 0 ? std::to_string(t_knife_floor) + " 楼" : "不出动") + "\n" + + "烟雾弹:" + (t_smoke_trigger ? "将在本回合释放" : (Main().smoke_used ? "已使用" : "未启用")) + + (t_hostage_floor > 0 || Main().police_floor == 2 ? "\n\n若您已完成部署,请使用「确认」指令确认行动" : ""); + } + + AtomReqErrCode PoliceSelect_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const int64_t f) + { + if (!CheckCommon(pid, is_public, reply, Role::POLICE)) { + return StageErrCode::FAILED; + } + if (f > GAME_OPTION(楼层)) { + reply() << "[错误] 喂,整座楼都没有这么高,你要准备上天吗?"; + return StageErrCode::FAILED; + } + if (f >= Main().police_floor || f < Main().police_floor - GAME_OPTION(电梯)) { + reply() << "[错误] 下楼失败:最多下 " << GAME_OPTION(电梯) << " 层楼,且不能上楼。本回合可选楼层范围为 " << higher_floor << "-" << lower_floor; + return StageErrCode::FAILED; + } + + Main().police_select = f; + reply() << "你按下了楼层 " << f << ",指示灯亮起,电梯伴随着低鸣缓缓下行……"; + return StageErrCode::READY; + } + + AtomReqErrCode KillerHostageSelect_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const int _, const int64_t f) + { + if (!CheckCommon(pid, is_public, reply, Role::KILLER)) { + return StageErrCode::FAILED; + } + if (f > GAME_OPTION(楼层)) { + reply() << "[错误] 喂,整座楼都没有这么高,你要准备上天吗?"; + return StageErrCode::FAILED; + } + if (f >= Main().police_floor || f < Main().police_floor - GAME_OPTION(电梯)) { + reply() << "[错误] 安装失败:弹射装置只能安装在警察能够到达的楼层。本回合可选楼层范围为 " << higher_floor << "-" << lower_floor; + return StageErrCode::FAILED; + } + if (Main().floors[f].hostage_used) { + reply() << "[错误] 安装失败:" << f << " 楼的弹射装置已经报废,无法再次安装"; + return StageErrCode::FAILED; + } + + t_hostage_floor = f; + bool warning = false; + if (t_knife_floor == f) { + t_knife_floor = -1; + warning = true; + } + reply() << (warning ? "[警告] 弹射和拿刀楼层存在冲突,已更新弹射楼层。\n\n" : "") + << "您准备在 " << f << " 楼安装弹射装置。电梯停靠后,装置将被悄然部署。\n\n" + << GetCurrentPlan(); + return StageErrCode::OK; + } + + AtomReqErrCode KillerKnifeSelect_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const int _, const int64_t f) + { + if (!CheckCommon(pid, is_public, reply, Role::KILLER)) { + return StageErrCode::FAILED; + } + if (f > GAME_OPTION(楼层)) { + reply() << "[错误] 喂,整座楼都没有这么高,你要准备上天吗?"; + return StageErrCode::FAILED; + } + if (f >= Main().police_floor || f < Main().police_floor - GAME_OPTION(电梯)) { + reply() << "[错误] 行动失败:拿刀出击只能在警察能够到达的楼层。本回合可选楼层范围为 " << higher_floor << "-" << lower_floor; + return StageErrCode::FAILED; + } + + t_knife_floor = f; + bool warning = false; + if (t_hostage_floor == f) { + t_hostage_floor = -1; + warning = true; + } + reply() << (warning ? "[警告] 拿刀和弹射楼层存在冲突,已更新拿刀楼层。\n\n" : "") + << "你已锁定 " << f << " 楼。拿刀行动已列入本回合计划,路线正在确认中。\n\n" + << GetCurrentPlan(); + return StageErrCode::OK; + } + + AtomReqErrCode KillerSmokeTrigger_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const int _, const bool trigger) + { + if (!CheckCommon(pid, is_public, reply, Role::KILLER)) { + return StageErrCode::FAILED; + } + if (Main().smoke_used) { + reply() << "[错误] 设定失败:烟雾弹已使用完毕,本回合无法纳入行动计划"; + return StageErrCode::FAILED; + } + + t_smoke_trigger = trigger; + reply() << (trigger ? "烟雾弹已准备就绪,等待指令\n\n" : "烟雾弹指令已撤回,计划恢复静默\n\n") + << GetCurrentPlan(); + return StageErrCode::OK; + } + + AtomReqErrCode KillerConfirm_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const int _) + { + if (!CheckCommon(pid, is_public, reply, Role::KILLER)) { + return StageErrCode::FAILED; + } + if (Global().IsReady(pid)) { + reply() << "[错误] 本回合行动已确认,已无退路可言"; + return StageErrCode::FAILED; + } + if (t_hostage_floor < 0 && Main().police_floor > 2) { + reply() << "[错误] 确认失败:弹射装置尚未部署!在警察到达 2 楼前,每回合必须安装弹射装置"; + return StageErrCode::FAILED; + } + + Main().hostage_floor = t_hostage_floor; + Main().knife_floor = t_knife_floor; + Main().smoke_trigger = t_smoke_trigger; + reply() << "本回合行动已确认:\n" + << "弹射装置:" << (t_hostage_floor > 0 ? std::to_string(t_hostage_floor) + " 楼" : "未部署") << "\n" + << "拿刀楼层:" << (t_knife_floor > 0 ? std::to_string(t_knife_floor) + " 楼" : "不出动") << "\n" + << "烟雾弹:" << (t_smoke_trigger ? "将在本回合释放" : (Main().smoke_used ? "已被使用" : "未启用")); + return StageErrCode::READY; + } + + virtual CheckoutErrCode OnStageTimeout() override + { + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (!Global().IsReady(pid)) { + if (Main().player_role_[pid] == Role::KILLER) { + if (t_hostage_floor > 0 || Main().police_floor <= 2) { + Main().hostage_floor = t_hostage_floor; + Main().knife_floor = t_knife_floor; + Main().smoke_trigger = t_smoke_trigger; + Global().Tell(pid) << "您超时未行动,已自动确认行动"; + } else { + Main().player_scores_[pid] = -1; + Global().Boardcast() << "杀手 " << At(PlayerID(pid)) << " 超时判负"; + } + } else { + Main().player_scores_[pid] = -1; + Global().Boardcast() << "警察 " << At(PlayerID(pid)) << " 超时判负"; + } + } + } + return StageErrCode::CHECKOUT; + } + + virtual CheckoutErrCode OnPlayerLeave(const PlayerID pid) override + { + Main().player_scores_[pid] = -1; + Global().Boardcast() << At(PlayerID(pid)) << " 强退认输。"; + return StageErrCode::CHECKOUT; + } + + virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) override + { + if (Global().IsReady(pid)) { + return StageErrCode::OK; + } + if (Main().player_role_[pid] == Role::POLICE) { + int32_t offset = rand() % GAME_OPTION(电梯); + int select = higher_floor - offset; + Main().police_select = std::max(1, select); + } else { + if (Main().police_floor == 2) { + if (rand() % 2 == 0 && !Main().floors[1].hostage_used) { + Main().hostage_floor = 1; + } else { + Main().knife_floor = 1; + } + } else { + do { + int32_t offset = rand() % GAME_OPTION(电梯); + t_hostage_floor = higher_floor - offset; + Main().hostage_floor = std::max(1, t_hostage_floor); + } while (Main().floors[Main().hostage_floor].hostage_used); + if (rand() % 2 == 0) { + do { + int32_t offset = rand() % GAME_OPTION(电梯); + t_knife_floor = higher_floor - offset; + if (t_knife_floor < 1) break; + Main().knife_floor = t_knife_floor; + } while (Main().knife_floor == Main().hostage_floor); + } + if (rand() % 5 == 0 && !Main().smoke_used) { + Main().smoke_trigger = true; + } + } + } + return StageErrCode::READY; + } + + virtual CheckoutErrCode OnStageOver() override + { + return StageErrCode::CHECKOUT; + } +}; + +class ShootStage : public SubGameStage<> +{ + public: + ShootStage(MainStage& main_stage, const uint64_t round) + : StageFsm(main_stage, "第 " + std::to_string(round) + " 回合[开枪阶段]", + MakeStageCommand(*this, "[警察] 危机四伏,选择是否开枪!", &ShootStage::Shoot_, + AlterChecker({{"开枪", true}, {"是", true}, {"开", true}, {"不开枪", false}, {"否", false}, {"接", false}}))) + {} + + PlayerID police = Main().RoleID(Role::POLICE); + PlayerID killer = Main().RoleID(Role::KILLER); + + virtual void OnStageBegin() override + { + Global().Boardcast() << "请 " << At(police) << " 选择是否开枪!时限 60 秒,超时默认不开枪"; + Global().SetReady(killer); + Global().StartTimer(60); + } + + private: + AtomReqErrCode Shoot_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const bool shoot) + { + if (Main().player_role_[pid] == Role::KILLER) { + reply() << "[错误] 您的角色是「杀手」,无法使用此行动。"; + return StageErrCode::FAILED; + } + Main().shoot = shoot; + return StageErrCode::READY; + } + + virtual CheckoutErrCode OnStageTimeout() override + { + Main().shoot = false; + Global().Boardcast() << At(PlayerID(police)) << " 超时未选择,默认不开枪"; + HandleShootStageOver(); + return StageErrCode::CHECKOUT; + } + + virtual CheckoutErrCode OnPlayerLeave(const PlayerID pid) override + { + Main().player_scores_[pid] = -1; + Global().Boardcast() << At(PlayerID(pid)) << " 强退认输。"; + return StageErrCode::CHECKOUT; + } + + virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) override + { + if (Global().IsReady(pid)) { + return StageErrCode::OK; + } + Main().shoot = rand() % 2; + return StageErrCode::READY; + } + + virtual CheckoutErrCode OnStageOver() override + { + HandleShootStageOver(); + return StageErrCode::CHECKOUT; + } + + void HandleShootStageOver() + { + if (Main().shoot) { + Main().bullets--; + if (Main().police_floor == Main().knife_floor) { + // 开枪打中杀手 + Main().player_hp_[killer]--; + Main().floors[Main().police_floor].UpdatePoliceRecord("开枪击中杀手"); + Global().Boardcast() << GetRandomHint(shoot_killer_hints); + } else if (Main().police_floor == Main().hostage_floor) { + // 开枪误杀人质 + Main().killed_hostages++; + Main().floors[Main().police_floor].UpdatePoliceRecord("开枪误杀人质"); + if (Main().killed_hostages == 2) { + Main().police_insane = true; + Global().Boardcast() << GetRandomHint(shoot_more_hostage_hints) <<"\n(已误杀 2 名人质,精神崩塌加深,接下来的每层都将看到黑影。请注意:再次误杀人质将直接判负)"; + } else if (Main().killed_hostages == 3) { + Global().Boardcast() << GetRandomHint(police_lose_hints); + } else { + Global().Boardcast() << GetRandomHint(shoot_hostage_hints); + } + } else { + // 开枪无事发生 + Main().floors[Main().police_floor].UpdatePoliceRecord("开枪没有命中"); + Global().Boardcast() << GetRandomHint(shoot_empty_hints); + } + } else { + if (Main().police_floor == Main().knife_floor) { + // 被杀手刺伤 + Main().player_hp_[police]--; + Main().floors[Main().police_floor].UpdatePoliceRecord("杀手刺伤"); + Global().Boardcast() << GetRandomHint(killer_stab_hints); + } else if (Main().police_floor == Main().hostage_floor) { + // 解救人质 + Main().rescued_hostages++; + Main().floors[Main().police_floor].UpdatePoliceRecord("到一名人质"); + Global().Boardcast() << GetRandomHint(rescue_hostage_hint); + } else { + // 没开枪无事发生 + Main().floors[Main().police_floor].UpdatePoliceRecord("什么都没有到"); + Global().Boardcast() << GetRandomHint(get_empty_hint); + } + } + } +}; + +auto* MakeMainStage(MainStageFactory factory) { return factory.Create(); } + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + diff --git a/games/escape_building/option.cmake b/games/escape_building/option.cmake new file mode 100644 index 0000000..e69de29 diff --git a/games/escape_building/options.h b/games/escape_building/options.h new file mode 100644 index 0000000..d59181a --- /dev/null +++ b/games/escape_building/options.h @@ -0,0 +1,7 @@ +EXTEND_OPTION("初始楼层高度", 楼层, (ArithChecker(10, 50, "层数")), 20) +EXTEND_OPTION("设置杀手初始血量", 杀手血量, (ArithChecker(1, 10, "血量")), 2) +EXTEND_OPTION("设置警察初始血量", 警察血量, (ArithChecker(1, 10, "血量")), 2) +EXTEND_OPTION("设置电梯的最大下行楼层", 电梯, (ArithChecker(2, 10, "层数")), 3) +EXTEND_OPTION("警察持有的子弹数量", 子弹, (ArithChecker(1, 20, "数量")), 5) +EXTEND_OPTION("警察获胜需要解救的人质数", 解救人质, (ArithChecker(1, 5, "数量")), 2) +EXTEND_OPTION("每回合时间限制", 时限, (ArithChecker(10, 3600, "超时时间(秒)")), 180) diff --git a/games/escape_building/rule.md b/games/escape_building/rule.md new file mode 100644 index 0000000..62f878f --- /dev/null +++ b/games/escape_building/rule.md @@ -0,0 +1,23 @@ +## 逃脱大楼 + +- **游戏人数:** 2 +- **原作:** 彩虹菜鸟 + +### 游戏简介 +- 杀人犯绑架了 19 个人质,藏匿在一栋 20 楼的大楼。警方为了不打草惊蛇,派了一位警察秘密潜入大楼,救出部分人质打探大楼情况。 +- 警察拿着一把有 5 发子弹的手枪,坐电梯到达 20 楼时什么都没有发现,这时杀人犯用广播告诉警察,他要和警察玩一个生死游戏。 + +### 游戏流程 +- 两名玩家将分别扮演警察和杀手。警察和杀手每个人都只有 2 血。**血量归 0 时直接判负** +- [杀手] **在警察能够到达的楼层**内行动。每回合都需要安装**弹射装置**在本回合将人质弹向电梯。如果警察没有来这一层,人质会被电梯门创死。**警察无法得知此信息,且这一层不能再放置弹射装置** + + **每回合杀手都必须设置人质**(特殊情况:当警察到达 2 楼时,杀手不强制要求在 1 楼放人质,可以在 1 楼让自己出动,或者不出动) +- [杀手] 除了安装弹射装置,还能选择在另外两层之一拿刀(不能选择安装弹射装置的楼层),电梯打开时,杀手会拿刀冲向电梯 + + **主动拿刀为可选行动,可以放弃本回合拿刀** +- [杀手] 在电梯安装了一个**烟雾弹**,杀手可以**随时控制**让烟雾弹在下回合爆炸。下一回合电梯门打开的时候,警察将会什么都看不到。 +- [警察] 每回合需乘坐电梯下楼,一次**最多只能下 3 层**。警察**只能得知到达楼层发生的情况**,电梯门开启时,如果有人质飞过来或者杀手刺过来,裁判会在公屏提示 **“有黑影闪过来”**,但是并不知道是杀手还是人质。玩家需要选择是否开枪: + + **选择开枪:** 打的是杀手,击退杀手这一次攻击,且让杀手扣 1 血。但如果打的是人质,人质死亡。 + + **选择接:** 来的是人质则警察成功救一名人质。但来的如果是杀手,则警察扣 1 血。 +- 例如:警察在 20 楼,本回合行动范围为 17-19 楼,杀手只能再这些楼层放置弹射装置。警察去 19 楼,而杀手在 18 楼安装装置,则警察会得知无事发生。18 楼无法再放置人质,但是此时杀手依然可以在 18 楼拿刀埋伏,则警察还是会收到有黑影闪过来的提示。 +- [警察] 一共有 5 发子弹。要是误杀了 2 名人质后,警察的精神心理开始出现问题,之后到达的每层都会有 **“有黑影闪过来”** 的提示(即使无事发生)。**如果警察杀了 3 个人质,则直接失败!** +- **警察到达 1 楼时,游戏结束。** 如果警察成功解救了 2 名人质,警察获胜,否则杀手获胜。 +- **特殊情况:** 如果杀手没有可放置弹射装置的楼层且警察还未抵达 2 楼,游戏平局。 diff --git a/games/escape_building/unittest.cc b/games/escape_building/unittest.cc new file mode 100644 index 0000000..3882d75 --- /dev/null +++ b/games/escape_building/unittest.cc @@ -0,0 +1,191 @@ +// 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(2, leave_test1) +{ + START_GAME(); + + ASSERT_LEAVE(CHECKOUT, 0); + + ASSERT_SCORE(-1, 0); +} + +GAME_TEST(2, leave_test2) +{ + START_GAME(); + + ASSERT_LEAVE(CHECKOUT, 1); + + ASSERT_SCORE(0, -1); +} + +GAME_TEST(2, sample_test) +{ + START_GAME(); + + ASSERT_PRI_MSG(OK, 0, "人质 17"); + ASSERT_PRI_MSG(OK, 0, "拿刀 19"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "17"); + ASSERT_PRI_MSG(CHECKOUT, 1, "不开枪"); // [R1] 人质1/2 + + ASSERT_PRI_MSG(OK, 0, "人质 14"); + ASSERT_PRI_MSG(OK, 0, "拿刀 15"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "15"); + ASSERT_PRI_MSG(CHECKOUT, 1, "不开枪"); // [R2] 警察血量-1 + + ASSERT_PRI_MSG(OK, 0, "人质 13"); + ASSERT_PRI_MSG(OK, 0, "烟雾弹"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "12"); + ASSERT_PRI_MSG(CHECKOUT, 1, "不开枪"); // [R3] 没有接到 + + ASSERT_PRI_MSG(OK, 0, "人质 11"); + ASSERT_PRI_MSG(OK, 0, "拿刀 10"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "9"); // [R4] 无事发生 + + ASSERT_PRI_MSG(OK, 0, "人质 7"); + ASSERT_PRI_MSG(OK, 0, "拿刀 8"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "6"); // [R5] 无事发生 + + ASSERT_PRI_MSG(OK, 0, "人质 4"); + ASSERT_PRI_MSG(OK, 0, "拿刀 5"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "3"); // [R6] 无事发生 + + ASSERT_PRI_MSG(OK, 0, "人质 2"); + ASSERT_PRI_MSG(OK, 0, "拿刀 1"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "2"); + ASSERT_PRI_MSG(CHECKOUT, 1, "不开枪"); // [R7] 人质2/2 + + ASSERT_PRI_MSG(OK, 0, "拿刀 1"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "1"); + ASSERT_PRI_MSG(CHECKOUT, 1, "开枪"); // [R8] 杀手血量-1 + + ASSERT_SCORE(0, 1); +} + +GAME_TEST(2, zero_bullet_test1) +{ + ASSERT_PUB_MSG(OK, 0, "子弹 1"); + ASSERT_PUB_MSG(OK, 0, "警察血量 1"); + START_GAME(); + + ASSERT_PRI_MSG(OK, 0, "人质 19"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "19"); + ASSERT_PRI_MSG(CHECKOUT, 1, "开枪"); // 子弹归0 + + ASSERT_PRI_MSG(OK, 0, "人质 18"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "18"); // 无子弹接人质 + + ASSERT_PRI_MSG(OK, 0, "人质 16"); + ASSERT_PRI_MSG(OK, 0, "拿刀 17"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "17"); // 无子弹中刀 + + ASSERT_SCORE(1, 0); +} + +GAME_TEST(2, zero_bullet_test2) +{ + START_GAME(); + + ASSERT_PRI_MSG(OK, 0, "人质 18"); + ASSERT_PRI_MSG(OK, 0, "拿刀 17"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "19"); // [R1] 无事发生 + + ASSERT_PRI_MSG(OK, 0, "人质 17"); + ASSERT_PRI_MSG(OK, 0, "拿刀 16"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "18"); // [R2] 无事发生 + + ASSERT_PRI_MSG(OK, 0, "人质 15"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "16"); // [R3] 无事发生 + + ASSERT_PRI_MSG(OK, 0, "人质 14"); + ASSERT_PRI_MSG(OK, 0, "拿刀 13"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "13"); + ASSERT_PRI_MSG(CHECKOUT, 1, "不开枪"); // [R4] 警察血量-1 + + ASSERT_PRI_MSG(OK, 0, "人质 12"); + ASSERT_PRI_MSG(OK, 0, "烟雾弹"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "12"); + ASSERT_PRI_MSG(CHECKOUT, 1, "开枪"); // [R5] 误杀人质1/3 + + ASSERT_PRI_MSG(OK, 0, "人质 10"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "11"); // [R6] 无事发生 + + ASSERT_PRI_MSG(OK, 0, "人质 8"); + ASSERT_PRI_MSG(OK, 0, "拿刀 9"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "10"); // [R7] 无事发生 + + ASSERT_PRI_MSG(OK, 0, "人质 9"); + ASSERT_PRI_MSG(OK, 0, "拿刀 7"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "7"); + ASSERT_PRI_MSG(CHECKOUT, 1, "开枪"); // [R8] 杀手血量-1 + + ASSERT_PRI_MSG(OK, 0, "人质 5"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "5"); + ASSERT_PRI_MSG(CHECKOUT, 1, "开枪"); // [R9] 误杀人质2/3 + + ASSERT_PRI_MSG(OK, 0, "人质 3"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "4"); + ASSERT_PRI_MSG(CHECKOUT, 1, "开枪"); // [R10] 开枪未命中 + + ASSERT_PRI_MSG(OK, 0, "人质 2"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "3"); + ASSERT_PRI_MSG(CHECKOUT, 1, "开枪"); // [R11] 开枪未命中(子弹耗尽) + + ASSERT_PRI_MSG(OK, 0, "人质 1"); + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "2"); // [R12] 无子弹没接到 + + ASSERT_PRI_MSG(OK, 0, "确认"); + ASSERT_PRI_MSG(CHECKOUT, 1, "1"); // [R13] 无子弹没接到 + + ASSERT_SCORE(1, 0); +} + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + gflags::ParseCommandLineFlags(&argc, &argv, true); + return RUN_ALL_TESTS(); +} From 20e9bc8c98d8bdc7380a579c421a939e0801c39a Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Fri, 6 Feb 2026 22:53:51 -0500 Subject: [PATCH 05/12] garnet_thief: increase initial Chips to 8 & change rule in 3 players --- games/garnet_thief/mygame.cc | 4 ++-- games/garnet_thief/options.h | 2 +- games/garnet_thief/rule.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/games/garnet_thief/mygame.cc b/games/garnet_thief/mygame.cc index 857aae2..ef00d58 100644 --- a/games/garnet_thief/mygame.cc +++ b/games/garnet_thief/mygame.cc @@ -23,7 +23,7 @@ const GameProperties k_properties { .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 +uint32_t Multiple(const CustomOptions& options) { return 1; } // the default score multiple for the game, 0 for a testing game, 1 for a formal game, 2 or 3 for a long formal game const MutableGenericOptions k_default_generic_options{ .is_formal_{false}, }; @@ -415,7 +415,7 @@ string MainStage::GetStatusBoard() { } void RoundStage::calc() { - int N = Main().Alive_() / 2; + int N = Main().Alive_() > 3 ? Main().Alive_() / 2 : 2; int M_count = 0, C_count = 0, P_count = 0, B_count = 0; for (int pid = 0; pid < Global().PlayerNum(); pid++) { diff --git a/games/garnet_thief/options.h b/games/garnet_thief/options.h index 8dfb6a6..2da1e46 100644 --- a/games/garnet_thief/options.h +++ b/games/garnet_thief/options.h @@ -1,3 +1,3 @@ EXTEND_OPTION("回合数", 回合数, (ArithChecker(1, 20, "回合数")), 8) -EXTEND_OPTION("初始 Chips 数量", Chips, (ArithChecker(1, 5, "数量")), 2) +EXTEND_OPTION("初始 Chips 数量", Chips, (ArithChecker(1, 20, "数量")), 8) EXTEND_OPTION("每回合时间限制", 时限, (ArithChecker(10, 3600, "超时时间(秒)")), 180) diff --git a/games/garnet_thief/rule.md b/games/garnet_thief/rule.md index 6072416..d4a56e6 100644 --- a/games/garnet_thief/rule.md +++ b/games/garnet_thief/rule.md @@ -10,8 +10,8 @@ ### 游戏流程 - **【允许私信】** 此游戏允许在进行中和其他玩家进行私信沟通,进行合作或协商策略 + 测试指令:「<序号> <私信内容>」直接向其他玩家发送私信消息 -- 游戏开始时,每个玩家都拥有 2 个Chips,**游戏共进行 8 个回合** -- 每回合可分配的 Chips 数量为 `N = 存活玩家数 / 2(向下取整)`。 +- 游戏开始时,每个玩家都拥有 8 个Chips,**游戏共进行 8 个回合** +- 每回合可分配的 Chips 数量为 `N = 存活玩家数 / 2(向下取整)`,3 人为 2 个 - **声明阶段:** 每个玩家要先声明自己所选的身份(黑手党 / 卡特尔 / 警察 / 乞丐),然后裁判公布所有人的声明 - **提交阶段:** 公布声明后,所有玩家要真正提交自己的身份(可与声明不同),根据真实身份分配 Chips - **Chips 分配规则** From 8ec52daca7f7061ca0c4b484092fc199935bb84e Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Fri, 6 Feb 2026 23:00:01 -0500 Subject: [PATCH 06/12] naval_battle: add scout crucial mode --- games/naval_battle/board.h | 32 +++++++--- games/naval_battle/mygame.cc | 112 ++++++++++++++++++++++++++--------- games/naval_battle/options.h | 2 +- games/naval_battle/rule.md | 20 ++++--- 4 files changed, 121 insertions(+), 45 deletions(-) diff --git a/games/naval_battle/board.h b/games/naval_battle/board.h index d21860a..a00658c 100644 --- a/games/naval_battle/board.h +++ b/games/naval_battle/board.h @@ -78,8 +78,8 @@ class Board // 是否为准备回合 int prepare; - // 保存首要害坐标 - int firstX, firstY; + // 保存需要展示机头的坐标 + vector> shown_crucials; // 计算自定义形状相关参数 void CustomizeShape(const vector shape) { @@ -156,7 +156,7 @@ class Board } // 图形界面 - string Getmap(const int show_planes, const int crucial_mode) + string GetMap(const int show_planes, const int crucial_mode) { // 初始化 for(int i = 0; i <= sizeX + 2; i++) @@ -211,11 +211,14 @@ class Board { for(int i = 1; i <= sizeX; i++) { + bool is_shown_crucial = std::find(shown_crucials.begin(), shown_crucials.end(), std::make_pair(i, j)) != shown_crucials.end(); + bool hide_crucial = (!is_shown_crucial && crucial_mode > 0); + grid[i][j] += " 0) { - if (map[i][j][1] == 2 && !show_planes && (crucial_mode == 1 || (!(firstX == i && firstY == j) && crucial_mode == 2))) { + if (map[i][j][1] == 2 && !show_planes && hide_crucial) { grid[i][j] += color[map[i][j][0]][1]; } else { grid[i][j] += color[map[i][j][0]][map[i][j][1]]; @@ -236,10 +239,9 @@ class Board m = "-"; } else { if ((show_planes || map[i][j][0] > 0) && - !(((crucial_mode == 1 || (!(firstX == i && firstY == j) && crucial_mode == 2))) && mark[i][j] == 200 && - map[i][j][0] != 2 && !(map[i][j][0] == 1 && map[i][j][1] >= 3))) + (!hide_crucial || mark[i][j] != 200 || map[i][j][0] == 2 || (map[i][j][0] == 1 && map[i][j][1] >= 3))) { - if (map[i][j][1] == 2 && !show_planes && (crucial_mode == 1 || (!(firstX == i && firstY == j) && crucial_mode == 2))) { + if (map[i][j][1] == 2 && !show_planes && hide_crucial) { m = icon[this_turn[i][j]][1]; } else { m = icon[this_turn[i][j]][map[i][j][1]]; @@ -503,6 +505,22 @@ class Board return "Empty Return"; } + // 结算侦察技能 + int Scout() + { + int scout_count = 0; + for(int i = 1; i <= sizeX; i++) { + for(int j = 1; j <= sizeY; j++) { + // 如果已被打击且是机头 + if (map[i][j][0] > 0 && map[i][j][1] == 2) { + scout_count++; + shown_crucials.push_back({i, j}); + } + } + } + return scout_count; + } + // 添加飞机标记 string AddMark(string s, const int direction) { diff --git a/games/naval_battle/mygame.cc b/games/naval_battle/mygame.cc index b643f3c..f5d0c78 100644 --- a/games/naval_battle/mygame.cc +++ b/games/naval_battle/mygame.cc @@ -40,24 +40,24 @@ const std::vector k_rule_commands = {}; bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { if (generic_options_readonly.PlayerNum() != 2) { - reply() << "该游戏为双人游戏,必须为2人参加,当前玩家数为" << generic_options_readonly.PlayerNum(); + reply() << "该游戏为双人游戏,必须为 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); return false; } auto shape = GET_OPTION_VALUE(game_options, 形状); if (shape.empty()) { - reply() << "[错误] 形状参数不能为空:必须包含5个长度为5的数字串,且只能包含数字012,数字2(机头)有且仅有一个。形如:00000 00200 11111 00100 01110"; + reply() << "[错误] 形状参数不能为空:必须包含5个长度为5的数字串,且只能包含数字012。形如:00000 00200 11111 00100 01110"; return false; } if (shape[0] != "默认") { if (shape.size() != 5) { - reply() << "[错误] 形状参数数量必须为5个:必须包含5个长度为5的数字串,且只能包含数字012,数字2(机头)有且仅有一个。形如:00000 00200 11111 00100 01110"; + reply() << "[错误] 形状参数数量必须为5个:必须包含5个长度为5的数字串,且只能包含数字012。形如:00000 00200 11111 00100 01110"; return false; } int countOf2 = 0; for (const auto& row : shape) { if (row.size() != 5) { - reply() << "[错误] 形状参数每一行都必须为5个数字:必须包含5个长度为5的数字串,且只能包含数字012,数字2(机头)有且仅有一个。形如:00000 00200 11111 00100 01110"; + reply() << "[错误] 形状参数每一行都必须为5个数字:必须包含5个长度为5的数字串,且只能包含数字012。形如:00000 00200 11111 00100 01110"; return false; } if (!std::all_of(row.begin(), row.end(), [](char c) { return c == '0' || c == '1' || c == '2'; })) { @@ -95,7 +95,7 @@ const std::vector k_init_options_commands = { "【单机BOSS挑战快捷配置】
" "[第1项] 挑战的BOSS类型:仅支持 [0-3],输入其他为随机
" "[第2项] 重叠:[0] 为不允许,[大于0] 均为允许
" - "[第3项] 要害:[0] 为有要害,[1] 为无要害,[大于1] 均为首要害
" + "[第3项] 要害:[0] 有要害,[1] 无要害,[2] 首要害,[3] 技能
" "[第4项] 连发次数:范围 [1-10],其他输入均为默认3
" "[第5项] 侦察区域大小:范围 [0-30],其他输入均为默认随机
" "[第6项] 进攻时限:范围 [30-3600],其他输入均为默认120
" @@ -110,7 +110,7 @@ const std::vector k_init_options_commands = { if (is_single) { if (options.size() >= 1) GET_OPTION_VALUE(game_options, BOSS挑战) = options[0] >= 0 && options[0] <= 3 ? options[0] : 100; if (options.size() >= 2) GET_OPTION_VALUE(game_options, 重叠) = options[1] == 0 ? false : true; - if (options.size() >= 3) GET_OPTION_VALUE(game_options, 要害) = options[2] >= 0 && options[2] <= 2 ? options[2] : 2; + if (options.size() >= 3) GET_OPTION_VALUE(game_options, 要害) = options[2] >= 0 && options[2] <= 3 ? options[2] : 0; if (options.size() >= 4) GET_OPTION_VALUE(game_options, 连发) = options[3] >= 1 && options[3] <= 10 ? options[3] : GET_OPTION_VALUE(game_options, 连发); if (options.size() >= 5) GET_OPTION_VALUE(game_options, 侦察) = options[4] >= 0 && options[4] <= 30 ? options[4] : GET_OPTION_VALUE(game_options, 侦察); if (options.size() >= 6) GET_OPTION_VALUE(game_options, 进攻时限) = options[5] >= 30 && options[5] <= 3600 ? options[5] : GET_OPTION_VALUE(game_options, 进攻时限); @@ -132,7 +132,7 @@ const std::vector k_init_options_commands = { if (options.size() >= 1) GET_OPTION_VALUE(game_options, 边长) = options[0] >= 8 && options[0] <= 15 ? options[0] : 10; if (options.size() >= 2) GET_OPTION_VALUE(game_options, 飞机) = options[1] >= 1 && options[1] <= 8 ? options[1] : 3; if (options.size() >= 3) GET_OPTION_VALUE(game_options, 重叠) = options[2] == 0 ? false : true; - if (options.size() >= 4) GET_OPTION_VALUE(game_options, 要害) = options[3] >= 0 && options[3] <= 2 ? options[3] : 2; + if (options.size() >= 4) GET_OPTION_VALUE(game_options, 要害) = options[3] >= 0 && options[3] <= 3 ? options[3] : 0; if (options.size() >= 5) GET_OPTION_VALUE(game_options, 连发) = options[4] >= 1 && options[4] <= 10 ? options[4] : GET_OPTION_VALUE(game_options, 连发); if (options.size() >= 6) GET_OPTION_VALUE(game_options, 侦察) = options[5] >= 0 && options[5] <= 30 ? options[5] : GET_OPTION_VALUE(game_options, 侦察); return NewGameMode::MULTIPLE_USERS; @@ -167,8 +167,11 @@ class MainStage : public MainGameStage int round_; // 回合攻击次数 int attack_count[2]; + // 是否使用“侦察”技能 + bool scout_used[2] = {false, false}; // 判定是否是超时淘汰 int timeout[2]; + // BOSS挑战 Boss boss; @@ -181,8 +184,8 @@ class MainStage : public MainGameStage string GetAllMap(const int show_0, const int show_1, const int crucial_mode) { string allmap = ""; - allmap += ""; - allmap += ""; + allmap += ""; + allmap += ""; allmap += "
" + board[0].Getmap(show_0, crucial_mode) + "" + board[1].Getmap(show_1, crucial_mode) + "" + board[0].GetMap(show_0, crucial_mode) + "" + board[1].GetMap(show_1, crucial_mode) + "
"; return allmap; } @@ -220,7 +223,6 @@ class MainStage : public MainGameStage board[pid].MapName += "的地图"; board[pid].alive = 0; board[pid].prepare = 1; - board[pid].firstX = board[pid].firstY = -1; attack_count[pid] = timeout[pid] = 0; } @@ -241,10 +243,10 @@ class MainStage : public MainGameStage sender << "[警告] 当前游戏未使用默认连发或侦察配置。\n\n"; } sender << "[本局挑战配置详情]" << - "\n- 重叠 " << (GAME_OPTION(重叠) ? "允许" : "不允许") << - "\n- 要害 " << (GAME_OPTION(要害) == 0 ? "有" : (GAME_OPTION(要害) == 1 ? "无" : "首次")) << - "\n- 连发 " << to_string(GAME_OPTION(连发)) << - "\n- 侦察 " << (GAME_OPTION(侦察) == 100 ? "随机" : to_string(GAME_OPTION(侦察))); + "\n- 重叠 " << (GAME_OPTION(重叠) ? "允许" : "不允许") << + "\n- 要害 " << (GAME_OPTION(要害) == 0 ? "有" : (GAME_OPTION(要害) == 1 ? "无" : "首次")) << + "\n- 连发 " << to_string(GAME_OPTION(连发)) << + "\n- 侦察 " << (GAME_OPTION(侦察) == 100 ? "随机" : to_string(GAME_OPTION(侦察))); if (GAME_OPTION(BOSS挑战) == 0) { board[0].sizeX = board[0].sizeY = board[1].sizeX = board[1].sizeY = 14; board[0].planeNum = 3; @@ -390,6 +392,9 @@ class PrepareStage : public SubGameStage<> if (GAME_OPTION(要害) == 2) { Global().Boardcast() << "[特殊规则] 本局仅首“要害”公开:每个玩家命中过1次机头以后,之后再次命中其他机头时,仅告知命中,不提示命中要害,且不具有额外一回合。"; } + if (GAME_OPTION(要害) == 3) { + Global().Boardcast() << "[特殊规则] 本局为?要害模式:无要害提示但每人有一次“侦察”机会,可以在回合结束后侦察所有已打击位置是否为要害,若其中有要害,则获得一个额外回合。(每局游戏限一次)"; + } // 游戏开始时展示飞机形状 if (GAME_OPTION(形状).size() != 1) { @@ -397,7 +402,7 @@ class PrepareStage : public SubGameStage<> } for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { - Global().Tell(pid) << Markdown(Main().board[pid].Getmap(1, GAME_OPTION(要害))); + Global().Tell(pid) << Markdown(Main().board[pid].GetMap(1, GAME_OPTION(要害))); Global().Tell(pid) << "请放置飞机,指令为「坐标 方向」,如:C5 上\n可通过「帮助」查看全部命令格式"; } Global().StartTimer(GAME_OPTION(放置时限)); @@ -425,7 +430,7 @@ class PrepareStage : public SubGameStage<> return StageErrCode::FAILED; } - reply() << Markdown(Main().board[pid].Getmap(1, GAME_OPTION(要害))); + reply() << Markdown(Main().board[pid].GetMap(1, GAME_OPTION(要害))); if (Main().board[pid].alive / Main().board[pid].crucial_num < Main().board[pid].planeNum) { reply() << "放置成功!您还有 " + to_string(Main().board[pid].planeNum - Main().board[pid].alive / Main().board[pid].crucial_num) + " 架飞机等待放置,请继续行动"; } else { @@ -451,7 +456,7 @@ class PrepareStage : public SubGameStage<> return StageErrCode::FAILED; } - reply() << Markdown(Main().board[pid].Getmap(1, GAME_OPTION(要害))); + reply() << Markdown(Main().board[pid].GetMap(1, GAME_OPTION(要害))); reply() << "移除成功!"; return StageErrCode::OK; } @@ -469,7 +474,7 @@ class PrepareStage : public SubGameStage<> Main().board[pid].RemoveAllPlanes(); - reply() << Markdown(Main().board[pid].Getmap(1, GAME_OPTION(要害))); + reply() << Markdown(Main().board[pid].GetMap(1, GAME_OPTION(要害))); reply() << "清空成功!"; return StageErrCode::OK; } @@ -494,7 +499,7 @@ class PrepareStage : public SubGameStage<> reply() << "请私信裁判查看当前布置的地图"; return StageErrCode::FAILED; } - reply() << Markdown(Main().board[pid].Getmap(1, GAME_OPTION(要害))); + reply() << Markdown(Main().board[pid].GetMap(1, GAME_OPTION(要害))); return StageErrCode::OK; } @@ -551,11 +556,14 @@ class AttackStage : public SubGameStage<> MakeStageCommand(*this, "移除飞机标记(移除+飞机头坐标+方向)", &AttackStage::RemoveMark_, VoidChecker("移除"), AnyArg("飞机头坐标", "C5"), AlterChecker(position_map)), MakeStageCommand(*this, "清空地图上的所有标记", &AttackStage::RemoveALLMark_, VoidChecker("清空")), + MakeStageCommand(*this, "[仅?要害模式] 发动“侦察”技能(限一次)", &AttackStage::Scout_, VoidChecker("侦察")), MakeStageCommand(*this, "发射导弹进行攻击!", &AttackStage::Attack_, AnyArg("攻击坐标", "A1"))) {} // 剩余连发次数 int repeated[2]; + // 本回合使用了侦察技能 + bool scout[2] = {false, false}; virtual void OnStageBegin() override { @@ -571,8 +579,37 @@ class AttackStage : public SubGameStage<> virtual CheckoutErrCode OnStageOver() override { + if (Main().board[0].alive > 0 && Main().board[1].alive > 0) { + // ?要害模式回合结算 + bool scout_success = false; + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (scout[pid]) { + scout[pid] = false; + int scout_count = Main().board[!pid].Scout(); + if (scout_count > 0) { + Global().Boardcast() << At(pid) << " 使用侦察技能\n" + << Markdown(Main().GetAllMap(0, 0, GAME_OPTION(要害))) << "\n" + << "成功侦察到已打击区域的 " << scout_count << " 个要害,获得额外一回合行动机会,导弹已重新填充。"; + scout_success = true; + repeated[pid] = GAME_OPTION(连发); + Global().ClearReady(pid); + } else { + Global().Boardcast() << At(pid) << " 使用侦察技能\n" + << Markdown(Main().GetAllMap(0, 0, GAME_OPTION(要害))) << "\n" + << "但是在已打击区域并未发现任何要害,未能获得额外回合。"; + } + } + } + // 侦察成功继续本回合 + if (scout_success) { + Global().StartTimer(GAME_OPTION(进攻时限)); + return StageErrCode::CONTINUE; + } + } + + // 回合结束 Global().Boardcast() << Markdown(Main().GetAllMap(0, 0, GAME_OPTION(要害))); - // 重置上回合打击位置 + // 重置本回合打击位置 for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { for(int i = 1; i <= Main().board[pid].sizeX; i++) { for(int j = 1; j <= Main().board[pid].sizeY; j++) { @@ -615,14 +652,12 @@ class AttackStage : public SubGameStage<> // 首要害坐标添加 bool first_crucial = false; - if (result == "2" && GAME_OPTION(要害) == 2 && Main().board[!pid].firstX == -1) { + if (result == "2" && GAME_OPTION(要害) == 2 && Main().board[!pid].shown_crucials.empty()) { string coordinate = str; Main().board[!pid].CheckCoordinate(coordinate); - Main().board[!pid].firstX = coordinate[0] - 'A' + 1; - Main().board[!pid].firstY = coordinate[1] - '0'; - if (coordinate.length() == 3) { - Main().board[!pid].firstY = (coordinate[1] - '0') * 10 + coordinate[2] - '0'; - } + int firstX = coordinate[0] - 'A' + 1; + int firstY = coordinate.length() == 2 ? coordinate[1] - '0' : (coordinate[1] - '0') * 10 + coordinate[2] - '0'; + Main().board[!pid].shown_crucials.push_back({firstX, firstY}); first_crucial = true; } @@ -642,7 +677,7 @@ class AttackStage : public SubGameStage<> } } else if (result == "1" || - (result == "2" && GAME_OPTION(要害) == 1) || + (result == "2" && (GAME_OPTION(要害) == 1 || GAME_OPTION(要害) == 3)) || (result == "2" && GAME_OPTION(要害) == 2 && !first_crucial)) { if (Main().board[!pid].alive == 0 && GAME_OPTION(要害) > 0) { @@ -674,10 +709,31 @@ class AttackStage : public SubGameStage<> sender << Main().boss.BOSS_SpecialPlanes(Main().board, str); return StageErrCode::READY; } - Global().Boardcast() << "[错误] 发生了不可预料的错误,请联系管理员:Error return value"; + Global().Boardcast() << "[错误] 发生了不可预料的错误,请联系管理员:未知的攻击行动返回值 " << result; return StageErrCode::FAILED; } + AtomReqErrCode Scout_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + if (GAME_OPTION(要害) != 3) { + reply() << "[错误] 本局游戏未启用侦察技能"; + return StageErrCode::FAILED; + } + if (Global().IsReady(pid)) { + reply() << "您本回合已行动完成,请等待对手操作"; + return StageErrCode::FAILED; + } + if (Main().scout_used[pid]) { + reply() << "[错误] 侦察技能已被使用,每局对战仅限一次"; + return StageErrCode::FAILED; + } + + scout[pid] = true; + Main().scout_used[pid] = true; + reply() << "您使用了侦察技能:将在本回合结束时侦察已打击的所有位置!"; + return StageErrCode::OK; + } + AtomReqErrCode AddMark_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const std::string& str, const int64_t direction) { string result = Main().board[!pid].AddMark(str, direction); diff --git a/games/naval_battle/options.h b/games/naval_battle/options.h index de7edd4..9066f4b 100644 --- a/games/naval_battle/options.h +++ b/games/naval_battle/options.h @@ -3,7 +3,7 @@ EXTEND_OPTION("进攻阶段时间限制(进攻操作每一步的时间限制 EXTEND_OPTION("设置地图大小", 边长, (ArithChecker(8, 15, "边长")), 10) EXTEND_OPTION("设置飞机数量", 飞机, (ArithChecker(1, 8, "数量")), 3) EXTEND_OPTION("是否允许飞机互相重叠", 重叠, (BoolChecker("允许", "不允许")), false) -EXTEND_OPTION("设置命中要害是否有提示", 要害, AlterChecker({{"有", 0}, {"无", 1}, {"首次", 2}}), 0) +EXTEND_OPTION("设置命中要害是否有提示", 要害, AlterChecker({{"有", 0}, {"无", 1}, {"首次", 2}, {"技能", 3}}), 0) EXTEND_OPTION("连发次数", 连发, (ArithChecker(1, 10, "次数")), 3) EXTEND_OPTION("初始随机侦察区域大小(默认为随机)", 侦察, (ArithChecker(0, 30, "面积")), 100) EXTEND_OPTION("BOSS挑战类型:【快捷配置已不再此处支持】
" diff --git a/games/naval_battle/rule.md b/games/naval_battle/rule.md index a3681ea..d4d37ef 100644 --- a/games/naval_battle/rule.md +++ b/games/naval_battle/rule.md @@ -18,22 +18,24 @@ ### 特殊规则 - 允许飞机重叠:若规则允许飞机重叠时,飞机的机身可任意数量重叠,机头不可与机身重叠 - 仅首“要害”公开:在该规则下,每个玩家命中过1次机头以后,之后再次命中其他机头时,仅告知命中,不提示命中要害,且不具有额外一回合 -- 无“要害”提示:在该规则下,命中机头时,仅告知命中,不再提示命中要害,且不具有额外一回合。 +- 无“要害”提示:在该规则下,命中机头时,仅告知命中,不再提示命中要害,且不具有额外一回合 +- ?要害模式:无要害提示但每人有一次“侦察”机会,可以在回合结束后侦察所有已打击位置是否为要害,若其中有要害,则获得一个额外回合 ### 地图图例 - - - + + + - - - + + + - - + +
 未被打击区域+未被打击机身未被打击飞机头 未被打击区域+未被打击机身未被打击飞机头
 已被打击空地+已被打击机身已被打击飞机头 已被打击空地+已被打击机身已被打击飞机头
-侦察点:空地+侦察点:机身-侦察点:空地+侦察点:机身
+ From 3d1d89550c61eb9ce37d5c3a514c8711bf135a2d Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Fri, 6 Feb 2026 23:02:54 -0500 Subject: [PATCH 07/12] numcomb: fix seed option bug & add text status command --- games/numcomb/mygame.cc | 30 +++++++++++++++++++++++------- games/numcomb/options.h | 2 +- games/opencomb/options.h | 2 +- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/games/numcomb/mygame.cc b/games/numcomb/mygame.cc index 03b62f9..aed427e 100644 --- a/games/numcomb/mygame.cc +++ b/games/numcomb/mygame.cc @@ -58,10 +58,11 @@ static const std::array, comb::k_direct_max> k_points{ struct Player { - Player(std::string resource_path) : score_(0), line_count_(0), comb_(new comb::Comb(std::move(resource_path))) {} + Player(std::string resource_path) : score_(0), line_count_(0), is_leave_(false), comb_(new comb::Comb(std::move(resource_path))) {} Player(Player&&) = default; int32_t score_; int32_t line_count_; + bool is_leave_; std::unique_ptr comb_; }; @@ -164,7 +165,8 @@ class RoundStage : public SubGameStage<> RoundStage(MainStage& main_stage, const uint64_t round, const comb::AreaCard& card) : StageFsm(main_stage, "第" + std::to_string(round) + "回合", MakeStageCommand(*this, "设置数字", &RoundStage::Set_, ArithChecker(0, 19, "数字")), - MakeStageCommand(*this, "查看本回合开始时蜂巢情况,可用于图片重发", &RoundStage::Info_, VoidChecker("赛况"))) + MakeStageCommand(*this, "查看本回合开始时蜂巢情况,可用于图片重发", &RoundStage::Info_, VoidChecker("赛况"), + OptionalDefaultChecker(true, "图片", "文字"))) , card_(card) , comb_html_(main_stage.CombHtml("## 第 " + std::to_string(round) + " 回合")) {} @@ -183,18 +185,28 @@ class RoundStage : public SubGameStage<> if (!Global().IsReady(pid)) { auto& player = Main().players_[pid]; const auto [idx, result] = player.comb_->SeqFill(card_); - auto sender = Global().Boardcast(); - sender << At(pid) << "因为超时未做选择,自动填入空位置 " << idx; if (result.point_ > 0) { - sender << ",意外收获 " << result.point_ << " 点积分"; player.score_ += result.point_; player.line_count_ += result.line_; } + if (!player.is_leave_) { + auto sender = Global().Boardcast(); + sender << At(pid) << "因为超时未做选择,自动填入空位置 " << idx; + if (result.point_ > 0) { + sender << ",意外收获 " << result.point_ << " 点积分"; + } + } } } Global().HookUnreadyPlayers(); } + virtual CheckoutErrCode OnPlayerLeave(const PlayerID pid) override + { + Main().players_[pid].is_leave_ = true; + return StageErrCode::CONTINUE; + } + virtual CheckoutErrCode OnStageTimeout() override { HandleUnreadyPlayers_(); @@ -242,9 +254,13 @@ class RoundStage : public SubGameStage<> return StageErrCode::READY; } - AtomReqErrCode Info_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + AtomReqErrCode Info_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const bool show_image) { - SendInfo(reply); + if (show_image) { + SendInfo(reply); + } else { + reply() << "本回合砖块为 " << card_.ImageName(); + } return StageErrCode::OK; } diff --git a/games/numcomb/options.h b/games/numcomb/options.h index 556c375..caefdbe 100644 --- a/games/numcomb/options.h +++ b/games/numcomb/options.h @@ -1,5 +1,5 @@ EXTEND_OPTION("每回合最长时间x秒", 局时, (ArithChecker(10, 3600, "局时(秒)")), 120) -EXTEND_OPTION("随机种子", 种子, (AnyArg("种子", "我是随便输入的一个字符串")), "") +EXTEND_OPTION("随机种子", 种子, (OptionalDefaultChecker("", "种子", "我是随便输入的一个字符串")), "") EXTEND_OPTION("回合数,即放置数字的数量", 回合数, (ArithChecker(10, 20, "回合数")), 20) EXTEND_OPTION("对于洗好的56个砖块,若前X个均不是癞子,则跳过这X个砖块(用于提升游戏中癞子出现的概率)", 跳过非癞子, (ArithChecker(0, 36, "数量")), 20) EXTEND_OPTION("最多癞子数量", 癞子, (ArithChecker(0, 2, "癞子数量")), 2) diff --git a/games/opencomb/options.h b/games/opencomb/options.h index f472454..fcb0df0 100644 --- a/games/opencomb/options.h +++ b/games/opencomb/options.h @@ -1,5 +1,5 @@ EXTEND_OPTION("每回合超时时间x秒", 局时, (ArithChecker(10, 3600, "局时(秒)")), 120) -EXTEND_OPTION("随机种子(关联卡池&道具,配置需一致)", 种子, (AnyArg("种子", "我是随便输入的一个字符串")), "") +EXTEND_OPTION("随机种子(关联卡池&道具,配置需一致)", 种子, (OptionalDefaultChecker("", "种子", "我是随便输入的一个字符串")), "") EXTEND_OPTION("游戏卡池:决定卡池中砖块的种类", 卡池, AlterChecker({{"经典", 0}, {"癞子", 1}, {"空气", 2}, {"混乱", 3}}), 0) EXTEND_OPTION("游戏模式:游戏采用的基础放置和对战规则", 模式, AlterChecker({{"传统", 0}, {"云顶", 1}}), 0) EXTEND_OPTION("开启连线额外得分(长度3及以上时获得额外分数)", 连线奖励, (BoolChecker("开启", "关闭")), true) From 28ad80475d1a2e81b6fc8d7a7d0b22b8e32ca180 Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Fri, 6 Feb 2026 23:07:10 -0500 Subject: [PATCH 08/12] six_nimmt: add strategy and expert game mode --- games/six_nimmt/mygame.cc | 176 +++++++++++++++++++++--------- games/six_nimmt/options.h | 5 +- games/six_nimmt/rule.md | 14 ++- games/six_nimmt/table.h | 218 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 351 insertions(+), 62 deletions(-) diff --git a/games/six_nimmt/mygame.cc b/games/six_nimmt/mygame.cc index 2808757..b06cea4 100644 --- a/games/six_nimmt/mygame.cc +++ b/games/six_nimmt/mygame.cc @@ -20,11 +20,11 @@ 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 + .name_ = "谁是牛头王", .developer_ = "铁蛋", .description_ = "玩家照着大小顺序摆牌,尽可能获得更少牛头的游戏", }; -uint64_t MaxPlayerNum(const CustomOptions& options) { return 10; } // 0 indicates no max-player limits +uint64_t MaxPlayerNum(const CustomOptions& options) { return 10; } uint32_t Multiple(const CustomOptions& options) { if (GET_OPTION_VALUE(options, 倍数) != vector{5, 10, 11, 55, 110}) { return 0; @@ -48,6 +48,9 @@ bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const Gener return false; } int cardLimit = generic_options_readonly.PlayerNum() * GET_OPTION_VALUE(game_options, 手牌) + GET_OPTION_VALUE(game_options, 行数); + if (GET_OPTION_VALUE(game_options, 策略)) { + GET_OPTION_VALUE(game_options, 卡牌) = cardLimit; + } if (GET_OPTION_VALUE(game_options, 卡牌) < cardLimit) { GET_OPTION_VALUE(game_options, 卡牌) = cardLimit; reply() << "[警告] 总卡牌数少于最小限制,已自动调整上限为 " << cardLimit; @@ -74,13 +77,37 @@ bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const Gener } const std::vector k_init_options_commands = { - InitOptionsCommand("独自一人开始游戏", - [] (CustomOptions& game_options, MutableGenericOptions& generic_options) - { - generic_options.bench_computers_to_player_num_ = 6; - return NewGameMode::SINGLE_USER; + InitOptionsCommand("配置游戏模式和规则变体", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const vector& modes) { + bool single_user = false; + for (int32_t mode : modes) { + switch (mode) { + case 0: + single_user = true; break; + case 1: + case 2: + case 3: + GET_OPTION_VALUE(game_options, 模式) = mode; break; + case 4: + GET_OPTION_VALUE(game_options, 大胃王) = true; break; + case 5: + GET_OPTION_VALUE(game_options, 策略) = true; break; + case 6: + GET_OPTION_VALUE(game_options, 专家) = true; break; + default:; + } + } + if (single_user) { + generic_options.bench_computers_to_player_num_ = 6; + return NewGameMode::SINGLE_USER; + } + return NewGameMode::MULTIPLE_USERS; }, - VoidChecker("单机")), + RepeatableChecker>(map{ + {"单机", 0}, + {"22分", 1}, {"33分", 2}, {"66分", 3}, + {"大胃王", 4}, {"策略", 5}, {"专家", 6}, + })), }; // ========== GAME STAGES ========== @@ -128,7 +155,10 @@ class MainStage : public MainGameStage if (GAME_OPTION(模式) == 1) Global().Boardcast() << "[提示] 本局为 22分 模式,当手牌打完时有玩家达到 22 个牛头时,游戏才会结束"; if (GAME_OPTION(模式) == 2) Global().Boardcast() << "[提示] 本局为 33分 模式,当手牌打完时有玩家达到 33 个牛头时,游戏才会结束"; if (GAME_OPTION(模式) == 3) Global().Boardcast() << "[提示] 本局为 66分 模式,当手牌打完时有玩家达到 66 个牛头时,游戏才会结束"; - if (GAME_OPTION(模式) == 4) Global().Boardcast() << "[提示] 本局为大胃王模式,每个牛头变更为加分,努力去获取更多的牛头吧!"; + if (GAME_OPTION(大胃王)) Global().Boardcast() << "[提示] 本局为大胃王模式:每个牛头变更为加分,努力去获取更多的牛头吧!"; + if (GAME_OPTION(策略)) Global().Boardcast() << "【策略变体】根据游戏人数限制使用的卡片数量,本局卡牌范围为 1~" << GAME_OPTION(卡牌); + if (GAME_OPTION(专家)) Global().Boardcast() << "【专家变体】牌会被放置于一行的最左边或者最右边更接近的位置。但只要添加了一行的第 " << GAME_OPTION(上限) << " 张牌,必须拿取这一行的牌和惩罚分数"; + for(int pid = 0; pid < Global().PlayerNum(); pid++) { Player newPlayer(pid, Global().PlayerName(pid), Global().PlayerAvatar(pid, 40)); players.push_back(newPlayer); @@ -143,8 +173,7 @@ class MainStage : public MainGameStage bool game_end = (GAME_OPTION(模式) == 0 && round_ == GAME_OPTION(手牌)) || (GAME_OPTION(模式) == 1 && table.CheckPlayerHead(22, players)) || (GAME_OPTION(模式) == 2 && table.CheckPlayerHead(33, players)) || - (GAME_OPTION(模式) == 3 && table.CheckPlayerHead(66, players)) || - (GAME_OPTION(模式) == 4 && round_ == GAME_OPTION(手牌)); + (GAME_OPTION(模式) == 3 && table.CheckPlayerHead(66, players)); if (!players[0].hand.empty() || !game_end) { if (players[0].hand.empty()) { Global().Boardcast() << "[提示] 手牌用尽但未到达游戏结束条件,将重新洗牌继续游戏!"; @@ -158,13 +187,13 @@ class MainStage : public MainGameStage setter.Emplace(*this, ++round_); return; } - if (GAME_OPTION(模式) != 4) { - Global().Boardcast() << "游戏结束!按照当前持有的牛头数结算扣分"; - } else { + if (GAME_OPTION(大胃王)) { Global().Boardcast() << "游戏结束!本局为大胃王模式,按照牛头数结算加分"; + } else { + Global().Boardcast() << "游戏结束!按照当前持有的牛头数结算扣分"; } for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { - if (GAME_OPTION(模式) == 4) { + if (GAME_OPTION(大胃王)) { player_scores_[pid] = players[pid].head; } else { player_scores_[pid] = -players[pid].head; @@ -325,21 +354,27 @@ class PlaceStage : public SubGameStage<> {} string once_BoardCast = ""; + // 存储当前等待手动操作的玩家的候选位置 + vector currentCandidates; virtual void OnStageBegin() override { Global().Boardcast() << Markdown(Main().table.GetTable(true, Main().players, Main().current_players)); - PlayerID lastpid = AutoPlaceCard(); + auto [needManual, activePid] = AutoPlaceCard(); for (int pid = 0; pid < Global().PlayerNum(); pid++) { - if (pid != lastpid) { + if (pid != activePid) { Global().SetReady(pid); } } if (once_BoardCast != "") { Global().Boardcast() << once_BoardCast << Markdown(Main().table.GetTable(true, Main().players, Main().current_players)); } - if (lastpid != -1) { - Global().Boardcast() << "请 [" << (lastpid + 1) << "号]" << Global().PlayerName(lastpid) << " 选择一行放置您的卡牌,时限 90 秒"; + if (activePid != -1) { + string hint; + if (GAME_OPTION(专家)) { + hint = Main().table.GetCandidateOptionsHint(currentCandidates, Main().players[activePid].current); + } + Global().Boardcast() << hint << "请 [" << (activePid + 1) << "号]" << Global().PlayerName(activePid) << " 选择一行放置您的卡牌,时限 90 秒"; Global().StartTimer(90); } } @@ -355,26 +390,65 @@ class PlaceStage : public SubGameStage<> reply() << "[错误] 当前并非是您的选择回合,或您本回合不需要操作"; return StageErrCode::FAILED; } - int gain_head = Main().table.PlaceCard(Main().players[pid], line - 1, Main().current_players); - Global().Boardcast() << "[" << (pid + 1) << "号]" << Global().PlayerName(pid) << " 放置卡牌于第 " + to_string(line) + " 行,吃掉了该行所有的卡牌,增加了 " + to_string(gain_head) + " 个牛头。\n" - << Markdown(Main().table.GetTable(true, Main().players, Main().current_players)); + + if (GAME_OPTION(专家)) { + // 验证选择是否在候选列表中 + if (!Main().table.ValidateLineChoice(line - 1, currentCandidates)) { + reply() << "[错误] 您只能从以下候选行中选择:\n" + << Main().table.GetCandidateOptionsHint(currentCandidates, Main().players[pid].current); + return StageErrCode::FAILED; + } + // 获取该行对应的放置选项(包含自动判定的左右位置) + Table::PlaceOption opt = Main().table.GetOptionByLine(line - 1, currentCandidates); + int gain_head = Main().table.PlaceCard(Main().players[pid], opt.line, opt.pos, Main().current_players); + + string posStr = (opt.pos == Table::LEFT) ? "左侧" : "右侧"; + Global().Boardcast() << "[" << (pid + 1) << "号]" << Global().PlayerName(pid) << " 选择第 " + to_string(line) + " 行,放置在" + posStr + << (gain_head ? ",吃掉了该行所有的卡牌,增加了 " + to_string(gain_head) + " 个牛头。" : "") << "\n" + << Markdown(Main().table.GetTable(true, Main().players, Main().current_players)); + } else { + // 基础规则:直接放置在右侧 + int gain_head = Main().table.PlaceCard(Main().players[pid], line - 1, Main().current_players); + Global().Boardcast() << "[" << (pid + 1) << "号]" << Global().PlayerName(pid) << " 放置卡牌于第 " + to_string(line) + " 行,吃掉了该行所有的卡牌,增加了 " + to_string(gain_head) + " 个牛头。\n" + << Markdown(Main().table.GetTable(true, Main().players, Main().current_players)); + } + return StageErrCode::READY; } - // 自动放置卡牌,返回下一个需要手动设置的玩家,-1为全部完成 - PlayerID AutoPlaceCard() { + // 自动放置卡牌,返回 {是否需要手动, 玩家ID} + pair AutoPlaceCard() { once_BoardCast = ""; + currentCandidates.clear(); + while(!Main().current_players.empty()) { PlayerID pid = Main().current_players[0].id; - int line = Main().table.CheckPlayerNeedPlace(pid, Main().players); - if (line != -1) { - int gain_head = Main().table.PlaceCard(Main().players[pid], line, Main().current_players); - once_BoardCast += "[" + to_string(pid + 1) + "号]将" + to_string(Main().players[pid].current) + "放置于" + to_string(line + 1) + "行" + (gain_head ? ",增加了" + to_string(gain_head) + "个牛头!" : "") + "\n"; + + if (GAME_OPTION(专家)) { + auto result = Main().table.CheckPlayerNeedPlaceExpert(pid, Main().players); + if (result.needManual) { + // 需要手动选择,保存候选列表 + currentCandidates = result.candidateOptions; + return {true, pid}; + } + // 自动放置 + int gain_head = Main().table.PlaceCard(Main().players[pid], result.autoLine, result.autoPos, Main().current_players); + string posStr = (result.autoPos == Table::LEFT) ? "[左侧]" : "右侧"; + once_BoardCast += "[" + to_string(pid + 1) + "号]将" + to_string(Main().players[pid].current) + "放置于第" + to_string(result.autoLine + 1) + "行" + posStr + + (gain_head ? ",增加了" + to_string(gain_head) + "个牛头!" : "") + "\n"; } else { - return pid; + // 基础规则 + int line = Main().table.CheckPlayerNeedPlace(pid, Main().players); + if (line == -1) { + currentCandidates.clear(); // 基础规则下可以选择任意行 + return {true, pid}; + } + int gain_head = Main().table.PlaceCard(Main().players[pid], line, Main().current_players); + once_BoardCast += "[" + to_string(pid + 1) + "号]将" + to_string(Main().players[pid].current) + "放置于第" + to_string(line + 1) + "行" + + (gain_head ? ",增加了" + to_string(gain_head) + "个牛头!" : "") + "\n"; } } - return -1; + return {false, -1}; } virtual CheckoutErrCode HandleStageOver_() @@ -385,27 +459,30 @@ class PlaceStage : public SubGameStage<> } PlayerID pid = Main().current_players[0].id; if (!Global().IsReady(pid)) { - int line = 0; - for (int i = 1; i < GAME_OPTION(行数); i++) { - if (Main().table.GetLineHead(i) < Main().table.GetLineHead(line)) { - line = i; - } - } - int gain_head = Main().table.PlaceCard(Main().players[pid], line, Main().current_players); - Global().Boardcast() << "[" << (pid + 1) << "号]" << Global().PlayerName(pid) << " 行动超时,自动放置于第 " << (line + 1) << " 行" << (gain_head ? ",增加了 " + to_string(gain_head) + " 个牛头。" : ""); + // 超时处理:选择候选位置中牛头最少的 + Table::PlaceOption bestOpt = Main().table.SelectMinHeadOption(currentCandidates); + int gain_head = Main().table.PlaceCard(Main().players[pid], bestOpt.line, bestOpt.pos, Main().current_players); + string posStr = ""; + if (GAME_OPTION(专家) && !currentCandidates.empty()) posStr = (bestOpt.pos == Table::LEFT) ? "[左侧]" : "右侧"; + Global().Boardcast() << "[" << (pid + 1) << "号]" << Global().PlayerName(pid) << " 行动超时,自动放置于第 " << (bestOpt.line + 1) << " 行" << posStr + << (gain_head ? ",增加了 " + to_string(gain_head) + " 个牛头。" : ""); Global().Hook(pid); Global().SetReady(pid); } // 继续下一个玩家行动 - PlayerID lastpid = AutoPlaceCard(); + auto [needManual, nextPid] = AutoPlaceCard(); if (once_BoardCast != "") { Global().Boardcast() << once_BoardCast << Markdown(Main().table.GetTable(true, Main().players, Main().current_players)); } else { Global().Boardcast() << Markdown(Main().table.GetTable(true, Main().players, Main().current_players)); } - if (lastpid != -1) { - Global().ClearReady(lastpid); - Global().Boardcast() << "请 [" << (lastpid + 1) << "号]" << Global().PlayerName(lastpid) << " 选择一行放置您的卡牌,时限 60 秒"; + if (nextPid != -1) { + string hint; + if (GAME_OPTION(专家)) { + hint = Main().table.GetCandidateOptionsHint(currentCandidates, Main().players[nextPid].current); + } + Global().Boardcast() << hint << "请 [" << (nextPid + 1) << "号]" << Global().PlayerName(nextPid) << " 选择一行放置您的卡牌,时限 60 秒"; + Global().ClearReady(nextPid); Global().StartTimer(60); return StageErrCode::CONTINUE; } else { @@ -433,14 +510,15 @@ class PlaceStage : public SubGameStage<> if (Global().IsReady(pid)) { return StageErrCode::OK; } - int line = 0; - for (int i = 1; i < GAME_OPTION(行数); i++) { - if (Main().table.GetLineHead(i) < Main().table.GetLineHead(line)) { - line = i; - } + // 选择牛头最少的候选位置 + Table::PlaceOption bestOpt = Main().table.SelectMinHeadOption(currentCandidates); + int gain_head = Main().table.PlaceCard(Main().players[pid], bestOpt.line, bestOpt.pos, Main().current_players); + string posStr = ""; + if (GAME_OPTION(专家) && !currentCandidates.empty()) { + posStr = (bestOpt.pos == Table::LEFT) ? "[左侧]" : "右侧"; } - int gain_head = Main().table.PlaceCard(Main().players[pid], line, Main().current_players); - Global().Boardcast() << "[" << (pid + 1) << "号]" << Global().PlayerName(pid) << " 放置卡牌于第 " << (line + 1) << " 行,吃掉了该行所有的卡牌,增加了 " << gain_head << " 个牛头。\n" + Global().Boardcast() << "[" << (pid + 1) << "号]" << Global().PlayerName(pid) << " 放置卡牌于第 " << (bestOpt.line + 1) << " 行" << posStr + << (gain_head ? ",吃掉了该行所有的卡牌,增加了 " + to_string(gain_head) + " 个牛头。" : "") << "\n" << Markdown(Main().table.GetTable(true, Main().players, Main().current_players)); return StageErrCode::READY; } diff --git a/games/six_nimmt/options.h b/games/six_nimmt/options.h index eb6b2af..50988a0 100644 --- a/games/six_nimmt/options.h +++ b/games/six_nimmt/options.h @@ -1,5 +1,8 @@ EXTEND_OPTION("出牌阶段时间限制(放置阶段固定为90秒)", 时限, (ArithChecker(30, 3600, "超时时间(秒)")), 120) -EXTEND_OPTION("游戏模式(可选:单局模式、22/33/66分模式,大胃王)", 模式, (AlterChecker({{"单局", 0}, {"22分", 1}, {"33分", 2}, {"66分", 3}, {"大胃王", 4}})), 0) +EXTEND_OPTION("游戏模式(可选:单局模式、22/33/66分模式)", 模式, (AlterChecker({{"单局", 0}, {"22分", 1}, {"33分", 2}, {"66分", 3}})), 0) +EXTEND_OPTION("策略变体:根据游戏人数限制使用的卡片数量", 策略, (BoolChecker("开启", "关闭")), false) +EXTEND_OPTION("专家变体:牌可以被添加到一行的最左边或者最右边", 专家, (BoolChecker("开启", "关闭")), false) +EXTEND_OPTION("大胃王模式:此模式每个牛头变更为加分", 大胃王, (BoolChecker("开启", "关闭")), false) EXTEND_OPTION("使用牌库的数字范围 1-?(不够时则拓展牌堆至本局需要的牌数)", 卡牌, (ArithChecker(50, 999, "上限")), 104) EXTEND_OPTION("每个玩家开局的手牌数", 手牌, (ArithChecker(5, 20, "牌数")), 10) EXTEND_OPTION("桌面上的牌阵行数", 行数, (ArithChecker(2, 8, "行数")), 4) diff --git a/games/six_nimmt/rule.md b/games/six_nimmt/rule.md index 46c7a07..8c83c1a 100644 --- a/games/six_nimmt/rule.md +++ b/games/six_nimmt/rule.md @@ -4,12 +4,12 @@ - **原作:** Wolfgang Kramer ### 游戏简介 -- 谁是牛头王是一款斗智的纸牌游戏,玩家照着大小顺序摆牌,排到第6张的玩家必须将整行牌吃掉,根据吃牌上的牛头多寡扣分,10个回合结束后牛头越少者获胜。 -- 包含数字1-104的卡牌每种一张,卡牌上的牛头数符合以下规律:5的倍数**2个牛头**;10的倍数**3个牛头**;11的倍数**5个牛头**;55的倍数**7个牛头**;其余卡牌均为**1个牛头** +- 谁是牛头王是一款斗智的纸牌游戏,玩家照着大小顺序摆牌,排到第6张的玩家必须将整行牌吃掉,**根据吃牌上的牛头多寡扣分,10个回合结束后牛头越少者获胜**。 +- 包含数字 1-104 的卡牌每种一张,卡牌上的牛头数符合以下规律:5的倍数**2个牛头**;10的倍数**3个牛头**;11的倍数**5个牛头**;55的倍数**7个牛头**;其余卡牌均为**1个牛头** ### 游戏流程 - 游戏的目标为尽可能获得**更少**的牛头,**每个牛头都会让自己失去分数** -- 游戏开始时,每个玩家发10张牌,并从剩下牌库抽出依照大小顺序4张摆在桌面,当作牌阵,剩下的牌不使用。 +- 游戏开始时,每个玩家发 10 张牌,并从剩下牌库抽出依照大小顺序 4 张摆在桌面,当作牌阵,剩下的牌不使用。 - 每回合所有玩家选择一张手牌打出。根据打出的牌,依照顺序从小到大轮流放置在桌面上4张牌阵中 - 在牌阵中放置时遵循以下规则: + 每一列牌阵必须**从小到大**,无须连续 @@ -25,3 +25,11 @@ - 【分数模式】包含22分、33分、66分模式。此模式当玩家手牌打完时,只有当有玩家达到对应牛头个数时游戏才会结束,**否则重新洗牌继续游戏** - 【大胃王模式】此模式每个牛头**变更为加分**,努力去获取更多的牛头吧! - 【自定义模式】如果设定的游戏总牌数超过104,则拓展牌堆至本局需要的牌数(自定义卡牌范围时可能不影响)。**注意:110的倍数为10个牛头** + +### 规则变体 +- **【策略变体】** + + 根据游戏人数限制使用的卡片数量,只使用 (玩家数 × 10) + 4 张卡牌。*(例如:在三人游戏中仅使用卡牌1-34)* +- **【专家变体】** + + 牌可以被添加到一行的**最左边或者最右边**,并且牌的数值顺序总是保持在右边上升在左边下降。 + + 如果玩家出的牌可以放在某一列的左边,也能放在另一列的右边,必须选择数字最接近的地方放,相差相同则需要手动选择放置位置。 + + **无论谁添加了一行的第六张牌,无论放在左边或者右边,必须拿取这一行的牌和惩罚分数。** diff --git a/games/six_nimmt/table.h b/games/six_nimmt/table.h index 26d37e5..56fd18f 100644 --- a/games/six_nimmt/table.h +++ b/games/six_nimmt/table.h @@ -31,6 +31,24 @@ class Player class Table { public: + enum PlacePosition { LEFT, RIGHT }; + + // 单个放置选项 + struct PlaceOption { + int line; + PlacePosition pos; + int distance; + }; + + // 放置结果结构 + struct PlaceResult { + bool needManual; // 是否需要手动选择 + int autoLine; // 自动放置的行号 + PlacePosition autoPos; // 自动放置的位置 + vector candidateOptions; // 需要手动选择时的候选位置列表 + }; + + // 游戏文件夹 string ResourceDir; @@ -178,7 +196,7 @@ class Table return playerTable.ToString(); } - // 检测玩家是否需要手动操作 + // 检测玩家是否需要手动操作(基础规则) int CheckPlayerNeedPlace(const PlayerID pid, const vector players) const { int card = players[pid].current; @@ -197,27 +215,209 @@ class Table } } if (!automatic) return -1; - else return maxIndex; + return maxIndex; } - // 玩家放置卡牌 - int PlaceCard(Player &player, const int line, vector ¤t_players) + // 根据卡牌和行号判断应该放在左边还是右边 + PlacePosition DeterminePosition(const int card, const int line) const + { + int first = tableStatus[line][0]; + int last = tableStatus[line][tableStatus[line].size() - 1]; + bool canRight = (card > last); + bool canLeft = (card < first); + // 优先右边(如果两边都可以放,说明这行只有一张牌,按基础规则放右边) + if (canRight) return RIGHT; + if (canLeft) return LEFT; + // 理论上不应该到这里 + return RIGHT; + } + + // 检测玩家是否需要手动操作(专家变体) + PlaceResult CheckPlayerNeedPlaceExpert(const PlayerID pid, const vector players) const + { + PlaceResult result; + result.needManual = false; + result.autoLine = -1; + int card = players[pid].current; + + vector allValidOptions; + for (int i = 0; i < lineNum; i++) { + int first = tableStatus[i][0]; + int last = tableStatus[i][tableStatus[i].size() - 1]; + if (card > last) { + PlaceOption opt; + opt.line = i; + opt.pos = RIGHT; + opt.distance = card - last; + allValidOptions.push_back(opt); + } + if (card < first) { + PlaceOption opt; + opt.line = i; + opt.pos = LEFT; + opt.distance = first - card; + allValidOptions.push_back(opt); + } + } + + // 没有合法位置,需要手动选择 + if (allValidOptions.empty()) { + result.needManual = true; + result.candidateOptions.clear(); + return result; + } + + // 距离最小的候选位置 + int minDistance = INT_MAX; + for (const auto& opt : allValidOptions) { + if (opt.distance < minDistance) { + minDistance = opt.distance; + } + } + for (const auto& opt : allValidOptions) { + if (opt.distance == minDistance) { + result.candidateOptions.push_back(opt); + } + } + // 单一位置,自动放置 + if (result.candidateOptions.size() == 1) { + result.needManual = false; + result.autoLine = result.candidateOptions[0].line; + result.autoPos = result.candidateOptions[0].pos; + return result; + } + // 多个最小距离位置,需要手动选择 + result.needManual = true; + return result; + } + + // 从候选位置中选择牛头数最少的(用于超时/AI) + PlaceOption SelectMinHeadOption(const vector& options) const + { + // 如果没有候选限制,选择牛头最少的行 + if (options.empty()) { + int minHead = INT_MAX; + int bestLine = 0; + for (int i = 0; i < lineNum; i++) { + int head = GetLineHead(i); + if (head < minHead) { + minHead = head; + bestLine = i; + } + } + PlaceOption opt; + opt.line = bestLine; + opt.pos = RIGHT; + opt.distance = 0; + return opt; + } + // 在候选位置中选择最少牛头的 + int minHead = INT_MAX; + PlaceOption bestOpt = options[0]; + for (const auto& opt : options) { + if (tableStatus[opt.line].size() >= lineLimit - 1) { + int head = GetLineHead(opt.line); + if (head < minHead) { + minHead = head; + bestOpt = opt; + } + } else { + if (0 < minHead) { + minHead = 0; + bestOpt = opt; + } + } + } + return bestOpt; + } + + // 验证玩家选择的行号是否在候选列表中 + bool ValidateLineChoice(const int line, const vector& candidates) const + { + // 如果候选列表为空,表示可以选择任意行(需要收牌的情况) + if (candidates.empty()) { + return line >= 0 && line < lineNum; + } + for (const auto& opt : candidates) { + if (opt.line == line) { + return true; + } + } + return false; + } + + // 从候选列表中找到指定行的选项 + PlaceOption GetOptionByLine(const int line, const vector& candidates) const + { + for (const auto& opt : candidates) { + if (opt.line == line) { + return opt; + } + } + // 如果是收牌情况(candidates为空),返回默认选项 + PlaceOption opt; + opt.line = line; + opt.pos = RIGHT; + opt.distance = 0; + return opt; + } + + // 生成候选位置的提示信息 + string GetCandidateOptionsHint(const vector& candidates, const int card) const + { + if (candidates.empty()) { + return "卡牌(" + to_string(card) + ")无法放置在任何位置,需要选择一行收牌\n\n"; + } + + string hint = "卡牌(" + to_string(card) + ")有以下等距离的放置选择:\n"; + for (const auto& opt : candidates) { + string posStr = (opt.pos == LEFT) ? "左侧" : "右侧"; + int targetCard = (opt.pos == LEFT) ? tableStatus[opt.line][0] : + tableStatus[opt.line][tableStatus[opt.line].size() - 1]; + hint += " 第" + to_string(opt.line + 1) + "行(" + posStr + ",与" + to_string(targetCard) + + "相距" + to_string(opt.distance) + ")\n"; + } + return hint + "\n"; + } + + // 玩家放置卡牌(全部规则) + int PlaceCard(Player &player, const int line, const PlacePosition pos, vector ¤t_players) { int card = player.current; - int current_place = tableStatus[line].size() - 1; + int current_size = tableStatus[line].size(); current_players.erase(current_players.begin()); - // 放置成功 - if (card > tableStatus[line][current_place] && current_place + 2 < lineLimit) { - tableStatus[line].push_back(card); + + bool canPlace = false; + if (pos == RIGHT) { // 右侧放置:牌要大于该行最后一张 + int last = tableStatus[line][current_size - 1]; + canPlace = (card > last); + } else { // 左侧放置:牌要小于该行第一张 + int first = tableStatus[line][0]; + canPlace = (card < first); + } + // 如果可以正常放置,且放置后不会达到上限 + if (canPlace && current_size + 1 < lineLimit) { + // 放置成功 + if (pos == RIGHT) { + tableStatus[line].push_back(card); + } else { + tableStatus[line].insert(tableStatus[line].begin(), card); + } return 0; } - // 放置失败 + // 放置失败:收走该行所有牌 int gain_head = GetLineHead(line); tableStatus[line].clear(); tableStatus[line].push_back(card); player.head += gain_head; return gain_head; } + + // 玩家放置卡牌(基础规则) + int PlaceCard(Player &player, const int line, vector ¤t_players) + { + return PlaceCard(player, line, RIGHT, current_players); + } int GetLineHead(const int line) const { From 7ffc9e24785604ade935f90b35d3e3014a9ef27e Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Fri, 6 Feb 2026 23:13:52 -0500 Subject: [PATCH 09/12] long_night: new game modes, blocks, achievements & refactor code structure --- games/long_night/achievements.h | 9 + games/long_night/board.h | 822 ++++++----- games/long_night/boss.h | 220 +++ games/long_night/constants.h | 361 +++++ games/long_night/grid.h | 293 +--- games/long_night/map.h | 817 ++++++----- games/long_night/mygame.cc | 1207 +++++++++++------ games/long_night/options.h | 33 +- games/long_night/player.h | 468 +++++++ games/long_night/resource/box.png | Bin 143 -> 0 bytes games/long_night/resource/button.png | Bin 2565 -> 0 bytes games/long_night/resource/classic/bomb.png | Bin 0 -> 1950 bytes games/long_night/resource/classic/box.png | Bin 0 -> 1212 bytes games/long_night/resource/classic/button.png | Bin 0 -> 3225 bytes .../resource/{ => classic}/empty.png | Bin .../resource/{ => classic}/exit.png | Bin .../resource/{ => classic}/grass.png | Bin .../resource/{ => classic}/heat.png | Bin .../resource/{ => classic}/oneway_portal.png | Bin .../resource/{ => classic}/portal.png | Bin .../resource/classic/transparent.png | Bin 0 -> 1484 bytes .../resource/{ => classic}/trap.png | Bin .../resource/{ => classic}/unknown.png | Bin .../resource/classic/walls/corner.png | Bin 0 -> 1411 bytes .../resource/classic/walls/door_col.png | Bin 0 -> 1428 bytes .../resource/classic/walls/door_row.png | Bin 0 -> 100 bytes .../resource/classic/walls/dooropen_col.png | Bin 0 -> 98 bytes .../resource/classic/walls/dooropen_row.png | Bin 0 -> 100 bytes .../resource/classic/walls/empty_col.png | Bin 0 -> 96 bytes .../resource/classic/walls/empty_row.png | Bin 0 -> 97 bytes .../resource/classic/walls/unknown_col.png | Bin 0 -> 103 bytes .../resource/classic/walls/unknown_row.png | Bin 0 -> 111 bytes .../resource/classic/walls/wall_col.png | Bin 0 -> 95 bytes .../resource/classic/walls/wall_row.png | Bin 0 -> 97 bytes .../resource/{ => classic}/water.png | Bin games/long_night/resource/retro/bomb.png | Bin 0 -> 2214 bytes games/long_night/resource/retro/box.png | Bin 0 -> 4282 bytes games/long_night/resource/retro/button.png | Bin 0 -> 3098 bytes games/long_night/resource/retro/empty.png | Bin 0 -> 2599 bytes games/long_night/resource/retro/exit.png | Bin 0 -> 5826 bytes games/long_night/resource/retro/grass.png | Bin 0 -> 3231 bytes games/long_night/resource/retro/heat.png | Bin 0 -> 4340 bytes .../resource/retro/oneway_portal.png | Bin 0 -> 94326 bytes games/long_night/resource/retro/portal.png | Bin 0 -> 4217 bytes .../long_night/resource/retro/transparent.png | Bin 0 -> 1484 bytes games/long_night/resource/retro/trap.png | Bin 0 -> 4471 bytes games/long_night/resource/retro/unknown.png | Bin 0 -> 141 bytes .../resource/retro/walls/corner.png | Bin 0 -> 1626 bytes .../resource/retro/walls/door_col.png | Bin 0 -> 2122 bytes .../resource/retro/walls/door_row.png | Bin 0 -> 2396 bytes .../resource/retro/walls/dooropen_col.png | Bin 0 -> 3158 bytes .../resource/retro/walls/dooropen_row.png | Bin 0 -> 2717 bytes .../resource/retro/walls/empty_col.png | Bin 0 -> 2212 bytes .../resource/retro/walls/empty_row.png | Bin 0 -> 2441 bytes .../resource/retro/walls/unknown_col.png | Bin 0 -> 103 bytes .../resource/retro/walls/unknown_row.png | Bin 0 -> 111 bytes .../resource/retro/walls/wall_col.png | Bin 0 -> 1462 bytes .../resource/retro/walls/wall_row.png | Bin 0 -> 1917 bytes games/long_night/resource/retro/water.png | Bin 0 -> 7414 bytes games/long_night/rule.md | 18 +- 60 files changed, 2925 insertions(+), 1323 deletions(-) create mode 100644 games/long_night/boss.h create mode 100644 games/long_night/constants.h create mode 100644 games/long_night/player.h delete mode 100644 games/long_night/resource/box.png delete mode 100644 games/long_night/resource/button.png create mode 100644 games/long_night/resource/classic/bomb.png create mode 100644 games/long_night/resource/classic/box.png create mode 100644 games/long_night/resource/classic/button.png rename games/long_night/resource/{ => classic}/empty.png (100%) rename games/long_night/resource/{ => classic}/exit.png (100%) rename games/long_night/resource/{ => classic}/grass.png (100%) rename games/long_night/resource/{ => classic}/heat.png (100%) rename games/long_night/resource/{ => classic}/oneway_portal.png (100%) rename games/long_night/resource/{ => classic}/portal.png (100%) create mode 100644 games/long_night/resource/classic/transparent.png rename games/long_night/resource/{ => classic}/trap.png (100%) rename games/long_night/resource/{ => classic}/unknown.png (100%) create mode 100644 games/long_night/resource/classic/walls/corner.png create mode 100644 games/long_night/resource/classic/walls/door_col.png create mode 100644 games/long_night/resource/classic/walls/door_row.png create mode 100644 games/long_night/resource/classic/walls/dooropen_col.png create mode 100644 games/long_night/resource/classic/walls/dooropen_row.png create mode 100644 games/long_night/resource/classic/walls/empty_col.png create mode 100644 games/long_night/resource/classic/walls/empty_row.png create mode 100644 games/long_night/resource/classic/walls/unknown_col.png create mode 100644 games/long_night/resource/classic/walls/unknown_row.png create mode 100644 games/long_night/resource/classic/walls/wall_col.png create mode 100644 games/long_night/resource/classic/walls/wall_row.png rename games/long_night/resource/{ => classic}/water.png (100%) create mode 100644 games/long_night/resource/retro/bomb.png create mode 100644 games/long_night/resource/retro/box.png create mode 100644 games/long_night/resource/retro/button.png create mode 100644 games/long_night/resource/retro/empty.png create mode 100644 games/long_night/resource/retro/exit.png create mode 100644 games/long_night/resource/retro/grass.png create mode 100644 games/long_night/resource/retro/heat.png create mode 100644 games/long_night/resource/retro/oneway_portal.png create mode 100644 games/long_night/resource/retro/portal.png create mode 100644 games/long_night/resource/retro/transparent.png create mode 100644 games/long_night/resource/retro/trap.png create mode 100644 games/long_night/resource/retro/unknown.png create mode 100644 games/long_night/resource/retro/walls/corner.png create mode 100644 games/long_night/resource/retro/walls/door_col.png create mode 100644 games/long_night/resource/retro/walls/door_row.png create mode 100644 games/long_night/resource/retro/walls/dooropen_col.png create mode 100644 games/long_night/resource/retro/walls/dooropen_row.png create mode 100644 games/long_night/resource/retro/walls/empty_col.png create mode 100644 games/long_night/resource/retro/walls/empty_row.png create mode 100644 games/long_night/resource/retro/walls/unknown_col.png create mode 100644 games/long_night/resource/retro/walls/unknown_row.png create mode 100644 games/long_night/resource/retro/walls/wall_col.png create mode 100644 games/long_night/resource/retro/walls/wall_row.png create mode 100644 games/long_night/resource/retro/water.png diff --git a/games/long_night/achievements.h b/games/long_night/achievements.h index e69de29..bb3b946 100644 --- a/games/long_night/achievements.h +++ b/games/long_night/achievements.h @@ -0,0 +1,9 @@ +EXTEND_ACHIEVEMENT(悄无声息, "未发出任何声响的情况下乘坐逃生舱") +EXTEND_ACHIEVEMENT(环游世界, "探索地图中的每一个区块") +EXTEND_ACHIEVEMENT(游荡幽灵, "未发出任何声响的情况下,探索地图中的每一个区块") +EXTEND_ACHIEVEMENT(我赶时间, "在第一回合乘坐逃生舱") +EXTEND_ACHIEVEMENT(饥渴难耐, "在第一回合捕捉目标玩家") +EXTEND_ACHIEVEMENT(乒铃乓啷, "路过或触发 5 种及以上不同的地形") +EXTEND_ACHIEVEMENT(嗜杀成性, "4人及以上对局中,捕捉所有其他玩家") +EXTEND_ACHIEVEMENT(守株待兔, "在单个回合中未进行移动并成功捕捉目标玩家") +EXTEND_ACHIEVEMENT(牛头魅魔, "持续被米诺陶斯锁定,BOSS移动 4 步仍未被抓到") diff --git a/games/long_night/board.h b/games/long_night/board.h index d094895..570d59d 100644 --- a/games/long_night/board.h +++ b/games/long_night/board.h @@ -2,15 +2,21 @@ struct GetBoardOptions { bool with_player = true; bool with_content = false; + bool with_ground = true; }; class Board { public: - Board(string image_path, const int32_t mode) : image_path_(std::move(image_path)), unitMaps(mode) {} + Board(string image_path, const int32_t image_type, const int32_t mode, const vector& custom_blocks) + : image_path_(std::move(image_path)), image_type_(image_type), gamemode(mode), g(std::random_device{}()), unitMaps(mode, g, custom_blocks) {} + + // 随机引擎 + std::mt19937 g; // 图片资源文件夹 const string image_path_; + const int32_t image_type_; // 玩家 uint32_t playerNum; @@ -21,38 +27,42 @@ class Board int size = 9; // 地图 vector> grid_map; + int gamemode; int exit_num; // 逃生舱数量 string init_html_; // 区块模板 UnitMaps unitMaps; // BOSS - struct Boss { - int x = -1; - int y = -1; - int steps = -1; - PlayerID target; - string all_record; - } boss; + Boss boss{size, players}; + // 完整赛况额外信息 + string all_extra_record; // 特殊区块位置(暂不使用) // pair special_pos = {12, 12}; // 初始化地图 - void Initialize(const bool boss) + void Initialize() { - std::random_device rd; - g = std::mt19937(rd()); grid_map.resize(size); player_map.resize(size); for (int i = 0; i < size; i++) { grid_map[i].resize(size); player_map[i].resize(size); } - InitializeBlock(); // 初始化区块 + + InitializeBlocks(); // 初始化区块 FixAdjacentWalls(grid_map); // 相邻墙面修复 FixInvalidPortals(grid_map); // 封闭传送门替换为水洼 RandomizePlayers(); // 随机生成玩家 - if (boss) BossSpawn(); // 初始化BOSS - // 保存初始盘面 + + // 成就辅助:记录玩家初始位置 + for (PlayerID pid = 0; pid < playerNum; ++pid) { + players[pid].achievement.MakeStep(players[pid].x, players[pid].y); + } + } + + // 保存初始盘面 + void SaveGameStartMap() + { init_html_ = GetBoard(grid_map); } @@ -66,48 +76,52 @@ class Board for (int x = 1; x < size * 2; x = x + 2) { for (int y = 1; y < size * 2; y = y + 2) { int gridX = (x+1)/2-1, gridY = (y+1)/2-1; - if (size >= 3) { - map.Get(x, y).SetStyle("class=\"grid\" " + GetGridStyle(grid_map[gridX][gridY].Type(), true)); + const Grid& grid = grid_map[gridX][gridY]; + if (options.with_ground) { + map.Get(x, y).SetStyle("class=\"grid\" " + GetGridStyle(grid.Type(), grid.Attach(), true)); } else { - map.Get(x, y).SetStyle("class=\"grid\""); + map.Get(x, y).SetStyle("class=\"grid\" " + GetGridStyle(GridType::EMPTY, AttachType::EMPTY, true)); } if (options.with_content) { - string content = grid_map[gridX][gridY].GetContent().first; + string content = grid.GetContent().first; if (!content.empty()) { - map.Get(x, y).SetContent(HTML_SIZE_FONT_HEADER(4) "" + content + "" HTML_FONT_TAIL); + map.Get(x, y).SetContent(GetColoredMark(content, grid.Type(), grid.Attach())); } } else if (options.with_player) { - string content = ""; - if (boss.x == gridX && boss.y == gridY) { - content += HTML_COLOR_FONT_HEADER(red) "★" HTML_FONT_TAIL; + string player_marks; + if (boss.Enable() && boss.x == gridX && boss.y == gridY) { + player_marks += boss.GetBossIcon(); } for (auto pid: player_map[gridX][gridY]) { - content += num[pid]; - if (content.length() % 2 == 0) { - content += "
"; + player_marks += num[pid]; + if (player_marks.length() % 2 == 0) { + player_marks += "
"; } } - if (!content.empty()) { - map.Get(x, y).SetContent(HTML_SIZE_FONT_HEADER(4) + content + HTML_FONT_TAIL); + // 根据[地形/附着]改变玩家标记样式 + if (!player_marks.empty()) { + map.Get(x, y).SetContent(GetColoredMark(player_marks, grid.Type(), grid.Attach())); } } + // 调试:测试地形文字颜色 + // map.Get(x, y).SetContent(GetColoredMark(num[0], grid.Type(), grid.Attach())); } } // 纵向围墙 for (int x = 1; x < size * 2; x = x + 2) { for (int y = 0; y < size * 2 - 1; y = y + 2) { int gridX = (x+1)/2-1, gridY = y/2; - map.Get(x, y).SetStyle("class=\"wall-col\" style=\"font-size:8px; line-height:8px; padding:0; margin:0;\"") - .SetColor(GetWallColor(grid_map[gridX][gridY].GetWall())); + Wall wall = grid_map[gridX][gridY].GetWall(); + map.Get(x, y).SetStyle("class=\"wall-col\" style=\"" + GetWallStyle(wall, "col") + "\""); if (options.with_content) { - string content = GetWallContent(grid_map, gridX, gridY, Direct::LEFT); + string content = Grid::GetWallContent(grid_map, gridX, gridY, Direct::LEFT); if (!content.empty()) map.Get(x, y).SetContent(content); } } - map.Get(x, size*2).SetStyle("class=\"wall-col\" style=\"font-size:8px; line-height:8px; padding:0; margin:0;\"") - .SetColor(GetWallColor(grid_map[(x+1)/2-1][size-1].GetWall())); + Wall wall = grid_map[(x+1)/2-1][size-1].GetWall(); + map.Get(x, size*2).SetStyle("class=\"wall-col\" style=\"" + GetWallStyle(wall, "col") + "\""); if (options.with_content) { - string content = GetWallContent(grid_map, (x+1)/2-1, size-1, Direct::RIGHT); + string content = Grid::GetWallContent(grid_map, (x+1)/2-1, size-1, Direct::RIGHT); if (!content.empty()) map.Get(x, size*2).SetContent(content); } } @@ -115,49 +129,30 @@ class Board for (int y = 1; y < size * 2; y = y + 2) { for (int x = 0; x < size * 2 - 1; x = x + 2) { int gridX = x/2, gridY = (y+1)/2-1; - map.Get(x, y).SetStyle("class=\"wall-row\" style=\"font-size:8px; line-height:8px; padding:0; margin:0;\"") - .SetColor(GetWallColor(grid_map[gridX][gridY].GetWall())); + Wall wall = grid_map[gridX][gridY].GetWall(); + map.Get(x, y).SetStyle("class=\"wall-row\" style=\"" + GetWallStyle(wall, "row") + "\""); if (options.with_content) { - string content = GetWallContent(grid_map, gridX, gridY, Direct::UP); + string content = Grid::GetWallContent(grid_map, gridX, gridY, Direct::UP); if (!content.empty()) map.Get(x, y).SetContent(content); } } - map.Get(size*2, y).SetStyle("class=\"wall-row\" style=\"font-size:8px; line-height:8px; padding:0; margin:0;\"") - .SetColor(GetWallColor(grid_map[size-1][(y+1)/2-1].GetWall())); + Wall wall = grid_map[size-1][(y+1)/2-1].GetWall(); + map.Get(size*2, y).SetStyle("class=\"wall-row\" style=\"" + GetWallStyle(wall, "row") + "\""); if (options.with_content) { - string content = GetWallContent(grid_map, size-1, (y+1)/2-1, Direct::DOWN); + string content = Grid::GetWallContent(grid_map, size-1, (y+1)/2-1, Direct::DOWN); if (!content.empty()) map.Get(size*2, y).SetContent(content); } } // 角落方块 for (int x = 0; x < size * 2 + 1; x = x + 2) { for (int y = 0; y < size * 2 + 1; y = y + 2) { - map.Get(x, y).SetStyle("class=\"corner\"").SetColor("black"); - } - } - return style + map.ToString(); - } - - // 获取墙壁上的提示文本 - string GetWallContent(const vector>& grid_map, const int x, const int y, const Direct direction) const - { - const int d = static_cast(direction); - const int od = static_cast(opposite(direction)); - const vector vec1 = grid_map[x][y].GetContent().second; - if (d >= 0 && d < (int)vec1.size() && !vec1[d].empty()) { - return vec1[d]; - } - int nx = x + k_DX_Direct[d]; - int ny = y + k_DY_Direct[d]; - if (0 <= nx && nx < grid_map.size() && 0 <= ny && ny < grid_map.size()) { - const vector vec2 = grid_map[nx][ny].GetContent().second; - if (od >= 0 && od < (int)vec2.size() && !vec2[od].empty()) { - return vec2[od]; + map.Get(x, y).SetStyle("class=\"corner\" style=\"background-image: url('" + ToFileUrl(image_path_ + GetImageTypeFolder() + "walls/corner.png") + "');\""); } } - return ""; + return GetBoardStyle() + map.ToString(); } + // 获取终局对比盘面 string GetFinalBoard() const { html::Table finalTable(2, 2); @@ -166,52 +161,15 @@ class Board finalTable.Get(0, 1).SetContent(HTML_SIZE_FONT_HEADER(5) "终局地图" HTML_FONT_TAIL); finalTable.Get(1, 0).SetStyle("style=\"padding: 10px 20px 10px 10px\"").SetContent(init_html_); finalTable.Get(1, 1).SetStyle("style=\"padding: 10px 10px 10px 20px\"").SetContent(GetBoard(grid_map)); - return GetPlayerTable(-1) + style + finalTable.ToString(); - } - - // 获取玩家信息 - string GetPlayerTable(const int round) const - { - html::Table playerTable(playerNum + (boss.steps >= 0), 6); - playerTable.SetTableStyle("align=\"center\" cellpadding=\"2\""); - for (int pid = 0; pid < playerNum; pid++) { - playerTable.Get(pid, 0).SetStyle("style=\"width:60px; text-align:right;\"").SetContent(to_string(pid) + "号:"); - playerTable.Get(pid, 1).SetStyle("style=\"width:40px;\"").SetContent(players[pid].avatar); - playerTable.Get(pid, 2).SetStyle("style=\"width:250px; text-align:left;\"").SetContent(players[pid].name); - if (players[pid].out == 2) { - playerTable.MergeRight(pid, 3, 3); - playerTable.Get(pid, 3).SetStyle("style=\"width:120px;\"").SetColor("#FFEBA3").SetContent("【逃生舱撤离】"); - } else if (players[pid].out == 1) { - playerTable.MergeRight(pid, 3, 3); - playerTable.Get(pid, 3).SetStyle("style=\"width:120px;\"").SetColor("#E5E5E5").SetContent("【已出局】"); - } else if (players[pid].target == 100) { - playerTable.MergeRight(pid, 3, 3); - playerTable.Get(pid, 3).SetStyle("style=\"width:150px;\"").SetContent("【单机模式】
寻找唯一的逃生舱!"); - } else if (players[pid].out == 0) { - playerTable.Get(pid, 3).SetStyle("style=\"width:40px;\"").SetContent("捕捉
目标"); - playerTable.Get(pid, 4).SetStyle("style=\"width:40px;\"").SetContent("[" + to_string(players[pid].target) + "号]"); - playerTable.Get(pid, 5).SetStyle("style=\"width:40px;\"").SetContent(players[players[pid].target].avatar); - } else { - playerTable.MergeRight(pid, 3, 3); - playerTable.Get(pid, 3).SetStyle("style=\"width:120px;\"").SetColor("#FFA07A").SetContent("[玩家状态错误]"); - } - } - if (boss.steps >= 0) { - playerTable.Get(playerNum, 0).SetStyle("style=\"width:60px;\"").SetContent(""); - playerTable.MergeRight(playerNum, 1, 2); - playerTable.Get(playerNum, 1).SetStyle("style=\"width:290px;\"").SetColor("lavender").SetContent("[BOSS] 米诺陶斯"); - playerTable.Get(playerNum, 3).SetStyle("style=\"width:40px;\"").SetContent("捕捉
目标"); - playerTable.Get(playerNum, 4).SetStyle("style=\"width:40px;\"").SetContent("[" + to_string(boss.target) + "号]"); - playerTable.Get(playerNum, 5).SetStyle("style=\"width:40px;\"").SetContent(players[boss.target].avatar); - } - return (round > 0 ? "### 第 " + to_string(round) + " 回合" : "") + playerTable.ToString(); + return GetPlayerTable(-1) + GetBoardStyle() + finalTable.ToString(); } // 全部区块信息展示 - string GetAllBlocksInfo(const int special, const int32_t test_mode = 0) const + string GetAllBlocksInfo(const int special, const bool bomb_mode, const int32_t test_mode = 0) const { - const vector& maps = test_mode == 0 ? unitMaps.maps : (test_mode == 2 ? unitMaps.rotation_maps : unitMaps.all_maps); - const vector& exits = test_mode == 0 ? unitMaps.exits : (test_mode == 2 ? unitMaps.rotation_exits : unitMaps.all_exits); + int col_num = (test_mode == 1 || test_mode == 3) ? 8 : 4; + const vector& maps = test_mode == 0 ? unitMaps.maps : (test_mode == 2 ? unitMaps.twist_maps : unitMaps.all_maps); + const vector& exits = test_mode == 0 ? unitMaps.exits : (test_mode == 2 ? unitMaps.twist_exits : unitMaps.all_exits); const vector& special_maps = unitMaps.special_maps; const string title = (test_mode == 0) ? "" @@ -220,85 +178,107 @@ class Board : (HTML_SIZE_FONT_HEADER(6) "《漫漫长夜》狂野+疯狂模式全部区块" HTML_FONT_TAIL); bool has_special = (test_mode == 3); for (const auto& map : maps) { - if (map.type == GridType::SPECIAL) { + if (map.is_special) { has_special = true; } } - int line_num = (ceil(maps.size() / 4.0) + ceil(exits.size() / 4.0)) * 2 + + has_special * (ceil(special_maps.size() / 4.0) * 2 + 1); - html::Table blocks(line_num, 4); - blocks.SetTableStyle("align=\"center\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-size: 25px;\""); + int line_num = (ceil(maps.size() / (double) col_num) + ceil(exits.size() / (double) col_num)) * 2 + + has_special * (ceil(special_maps.size() / (double) col_num) * 2 + 1); + html::Table blocks(line_num, col_num); + blocks.SetTableStyle("align=\"center\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-size: 25px; line-height: 1.2;\""); int row = 0; for (int i = 0; i < maps.size(); i++) { - blocks.Get(row, i % 4).SetStyle("style=\"padding: 25px 25px 0px 25px\"").SetContent(GetSingleBlock(0, maps[i].id, special)); + blocks.Get(row, i % col_num).SetStyle("style=\"padding: 25px 25px 0 25px\"").SetContent(GetSingleBlock(0, maps[i].id, special)); // 特殊地图不显示id - blocks.Get(row + 1, i % 4).SetContent(HTML_COLOR_FONT_HEADER(red) "" + (maps[i].id[0] == 'S' ? "???" : maps[i].id) + "" HTML_FONT_TAIL); - if ((i + 1) % 4 == 0 || i == maps.size() - 1) row += 2; + if (maps[i].is_special && gamemode >= 0) { + blocks.Get(row + 1, i % col_num).SetContent(HTML_COLOR_FONT_HEADER(red) HTML_SIZE_FONT_HEADER(6) "???" HTML_FONT_TAIL HTML_FONT_TAIL); + } else { + blocks.Get(row + 1, i % col_num).SetContent(HTML_COLOR_FONT_HEADER(red) "" + maps[i].id + "
" + HTML_COLOR_FONT_HEADER(#990000) HTML_SIZE_FONT_HEADER(4) + maps[i].title + HTML_FONT_TAIL HTML_FONT_TAIL "
" HTML_FONT_TAIL); + } + if ((i + 1) % col_num == 0 || i == maps.size() - 1) row += 2; } for (int i = 0; i < exits.size(); i++) { - blocks.Get(row, i % 4).SetStyle("style=\"padding: 20px 25px 5px 25px\"").SetContent(GetSingleBlock(1, exits[i].id, special)); - blocks.Get(row + 1, i % 4).SetContent(HTML_COLOR_FONT_HEADER(red) "EXIT " + exits[i].id + "" HTML_FONT_TAIL); - if ((i + 1) % 4 == 0 || i == exits.size() - 1) row += 2; + blocks.Get(row, i % col_num).SetStyle("style=\"padding: 20px 25px 0 25px\"").SetContent(GetSingleBlock(1, exits[i].id, special)); + blocks.Get(row + 1, i % col_num).SetContent(HTML_COLOR_FONT_HEADER(red) "EXIT " + exits[i].id + "
" + HTML_COLOR_FONT_HEADER(#990000) HTML_SIZE_FONT_HEADER(4) + exits[i].title + HTML_FONT_TAIL HTML_FONT_TAIL "
" HTML_FONT_TAIL); + if ((i + 1) % col_num == 0 || i == exits.size() - 1) row += 2; } - if (has_special) { - blocks.MergeRight(row, 0, 4); + if (has_special && gamemode >= 0) { + blocks.MergeRight(row, 0, col_num); blocks.Get(row++, 0).SetStyle("style=\"padding: 20px 25px 5px 25px\"") .SetContent(HTML_SIZE_FONT_HEADER(6) "特殊区块列表
" HTML_FONT_TAIL HTML_SIZE_FONT_HEADER(5) "(多传送门按照图中字母代号传送)" HTML_FONT_TAIL "
"); for (int i = 0; i < special_maps.size(); i++) { - blocks.Get(row, i % 4).SetStyle("style=\"padding: 20px 25px 5px 25px\"") + blocks.Get(row, i % col_num).SetStyle("style=\"padding: 20px 25px 0 25px\"") .SetContent(GetBoard(unitMaps.FindBlockById(special_maps[i].id, false, special == 3), GetBoardOptions{.with_content = true})); - blocks.Get(row + 1, i % 4).SetContent(HTML_COLOR_FONT_HEADER(red) "" + special_maps[i].id + "" HTML_FONT_TAIL); - if ((i + 1) % 4 == 0 || i == special_maps.size() - 1) row += 2; + blocks.Get(row + 1, i % col_num).SetContent(HTML_COLOR_FONT_HEADER(red) "" + special_maps[i].id + "
" + HTML_COLOR_FONT_HEADER(#990000) HTML_SIZE_FONT_HEADER(4) + special_maps[i].title + HTML_FONT_TAIL HTML_FONT_TAIL "
" HTML_FONT_TAIL); + if ((i + 1) % col_num == 0 || i == special_maps.size() - 1) row += 2; } } - const string wall_svg = "" - "" - "" - "" - ""; - auto GenerateWallSvg = [](const std::string& color) -> std::string { - return "" - "" - ""; - }; + const vector> all_walls_info = { - { Wall::DOOR, "【门】初始为关闭状态,关闭时视为墙壁。当关联的按钮被按下时会切换开关状态
如果门发生过变化,在回合结束会进行提示。关闭的门在私信墙壁信息会显示为**普通墙壁**" }, + { Wall::DOOR, "【门】初始关闭状态的门,关闭时视为墙壁。当关联的按钮被按下时会切换开关状态
如果门发生过变化,在回合结束会进行提示。关闭的门在私信墙壁信息会显示为**普通墙壁**" }, + { Wall::DOOROPEN, "【门 (开)】初始打开状态的门,玩家可移动穿过打开的门。在私信墙壁信息会显示为**无墙壁**" }, + }; + const vector> all_attachs_info = { + { AttachType::BUTTON, "【按钮】玩家进入时会触发区块内按钮相关事件。(出生不算)
进入按钮格**没有任何信息提示**,且仅在进入时才会触发按钮" }, + { AttachType::BOMB, "【炸弹】玩家**经过并尝试离开**会引爆炸弹,使玩家**立即出局并-100分**
在炸弹上**结束行动**可拆除炸弹,放置后直接停止不会拆除。**炸弹不能放置在其他附着类型上**" }, + { AttachType::BOX, "【箱子】玩家相邻箱子且向箱子移动时,箱子可被推动。(不会出生在箱子内)
**箱子不可移动到本区块外,其后方有玩家、墙壁或其他附着时,均不可被推动**。
若箱子不可被推动,则视为**撞墙**,且**箱子本身不会显示为墙壁**。" }, }; const vector> all_grids_info = { - { GridType::BUTTON, "【按钮】玩家进入时会触发区块内按钮相关事件。(出生不算)
进入按钮格**没有任何信息提示**,且仅在进入时才会触发按钮" }, { GridType::GRASS, "【树丛】玩家进入时会发出让其他人听见的**沙沙声**。(出生不算)" }, { GridType::WATER, "【水洼】玩家进入时会发出让其他人听见的**啪啪声**。(出生不算)" }, { GridType::PORTAL, "【传送门】玩家进入时会发出其他人听见的**啪啪声**。(出生不算)
进入后,再任意2次移动后就会传送至同区块另1个传送门。
进入后,玩家视作进入亚空间,上述2次移动都在亚空间内。" }, { GridType::ONEWAYPORTAL, "【传送门出口】玩家进入时会发出其他人听见的**啪啪声**。(出生不算)
传送门的单向出口,进入时不会触发传送(必须从入口进入才会传送至此处)
**玩家在进入同一区块的传送门入口时,传送门会转换方向,入口和出口交换位置**" }, { GridType::TRAP, "【陷阱】陷阱隐藏在树丛中:被奇数次进入时,会发出让其他人听见的**沙沙声**(出生不算)
被偶数次进入时,不发出声响,并**强制玩家停止**(出生不算)" }, - { GridType::HEAT, "【热源】进入热源周围8格时,将**私信**受到热浪提示。(出生不算)
当进入热源时,将**私信**收到高温烫伤提示(不会出生在热源内)
在整局游戏中,**当第 2 次或更多次进入热源时,会被强制停止行动**" }, - { GridType::BOX, "【箱子】玩家相邻箱子且向箱子移动时,箱子可被推动。(不会出生在箱子内)
**箱子不可移动到本区块外**。若箱子不可推动,则撞墙,箱子本身不会显示为墙。" }, - { GridType::EXIT, "【逃生舱】逃生者使用后,**会消失**。默认逃生舱数=人数的一半" }, + { GridType::HEAT, "【热源】进入热源周围8格时,将**私信**收到热浪提示。(只有移动时才能感受到热浪)
当进入热源时,将**私信**收到高温烫伤提示(不会出生在热源内)
在整局游戏中,**当第 2 次或更多次进入热源时,会被强制停止行动**" }, + { GridType::EXIT, "【逃生舱】逃生者使用后,**会消失**。" + (test_mode == 0 ? ("本局逃生舱数量为 **" + to_string(exit_num) + "** 个。") : "") }, + }; + + auto GenerateWallStyle = [&](Wall wall, const string& direction) { + return "
"; }; vector all_maps_in_game; all_maps_in_game.insert(all_maps_in_game.end(), maps.begin(), maps.end()); all_maps_in_game.insert(all_maps_in_game.end(), exits.begin(), exits.end()); - if (has_special) all_maps_in_game.insert(all_maps_in_game.end(), special_maps.begin(), special_maps.end()); + if (has_special && gamemode >= 0) all_maps_in_game.insert(all_maps_in_game.end(), special_maps.begin(), special_maps.end()); - html::Table legend(all_grids_info.size() + 1, 2); + int legend_max_size = all_walls_info.size() + all_attachs_info.size() + all_grids_info.size() + 1; + html::Table legend(legend_max_size, 2); legend.SetTableStyle("cellpadding=\"5\" cellspacing=\"0\" style=\"font-size: 22px;\""); - for (int i = 0; i < all_grids_info.size() + 1; i++) { + for (int i = 0; i < legend_max_size; i++) { legend.Get(i, 1).SetStyle("style=\"text-align: left;\""); } - legend.Get(0, 0).SetContent(wall_svg); - legend.Get(0, 1).SetContent("【墙壁】黑色为墙壁,如图例,玩家不可从上下通过,可以从左右通过"); + const string wall_html = + "
" + + GenerateWallStyle(Wall::NORMAL, "row") + + GetGridStyle(GridType::EMPTY, AttachType::EMPTY, false) + + GenerateWallStyle(Wall::NORMAL, "row") + + "
"; + legend.Get(0, 0).SetContent(wall_html); + legend.Get(0, 1).SetContent("【墙壁】如图例,上下为墙壁,玩家不可从上下通过,可以从左右通过"); row = 1; for (const auto& wall_info: all_walls_info) { if (UnitMaps::MapContainWallType(all_maps_in_game, wall_info.first)) { - legend.Get(row, 0).SetContent(GenerateWallSvg(GetWallColor(wall_info.first))); + legend.Get(row, 0).SetContent(GenerateWallStyle(wall_info.first, "row")); legend.Get(row, 1).SetContent(wall_info.second); row++; } } + for (const auto& attach_info: all_attachs_info) { + if (UnitMaps::MapContainAttachType(all_maps_in_game, attach_info.first) || (attach_info.first == AttachType::BOMB && bomb_mode)) { + legend.Get(row, 0).SetContent(GetGridStyle(GridType::EMPTY, attach_info.first, false)); + legend.Get(row, 1).SetContent(attach_info.second); + row++; + } + } for (const auto& grid_info: all_grids_info) { if (UnitMaps::MapContainGridType(all_maps_in_game, grid_info.first)) { if (grid_info.first == GridType::GRASS && special == 3) continue; - legend.Get(row, 0).SetContent(GetGridStyle(grid_info.first, false)); + legend.Get(row, 0).SetContent(GetGridStyle(grid_info.first, AttachType::EMPTY, false)); legend.Get(row, 1).SetContent(grid_info.second); row++; } @@ -309,8 +289,14 @@ class Board string GetSingleBlock(const int type, const string& id, const int special) const { - // 特殊地图不显示预览 - if (id[0] == 'S') return ""; + // 特殊地图不显示预览(自定义模式除外) + if (id[0] == 'S' && gamemode >= 0) { + string size = to_string(GRID_SIZE * 3 + WALL_SIZE * 4) + "px"; + return "
" + "特殊区块
"; + } vector> grid; if (type == 0) { grid = unitMaps.FindBlockById(id, false, special == 3); @@ -320,30 +306,165 @@ class Board return GetBoard(grid, GetBoardOptions{.with_content = true}); } - string GetAllRecord() const + // 获取玩家信息 + string GetPlayerTable(const int round) const { - string all_record; + html::Table playerTable(playerNum + boss.Enable(), 6); + playerTable.SetTableStyle("align=\"center\" cellpadding=\"2\""); for (int pid = 0; pid < playerNum; pid++) { - all_record += "[" + to_string(pid) + "号]" + players[pid].name + ""; - all_record += players[pid].all_record + "
"; - if (pid < playerNum - 1) all_record += "
"; + playerTable.Get(pid, 0).SetStyle("style=\"width:60px; text-align:right;\"").SetContent(to_string(pid) + "号:"); + playerTable.Get(pid, 1).SetStyle("style=\"width:40px;\"").SetContent(players[pid].avatar); + playerTable.Get(pid, 2).SetStyle("style=\"width:250px; text-align:left;\"").SetContent(players[pid].name); + if (players[pid].out == 2) { + playerTable.MergeRight(pid, 3, 3); + playerTable.Get(pid, 3).SetStyle("style=\"width:120px;\"").SetColor("#FFEBA3").SetContent("【逃生舱撤离】"); + } else if (players[pid].out == 1) { + playerTable.MergeRight(pid, 3, 3); + playerTable.Get(pid, 3).SetStyle("style=\"width:120px;\"").SetColor("#E5E5E5").SetContent("【已出局】"); + } else if (players[pid].target == 100) { + playerTable.MergeRight(pid, 3, 3); + playerTable.Get(pid, 3).SetStyle("style=\"width:130px;\"").SetContent("【单机模式】
寻找唯一逃生舱!"); + } else if (players[pid].out == 0) { + playerTable.Get(pid, 3).SetStyle("style=\"width:40px;\"").SetContent("捕捉
目标"); + playerTable.Get(pid, 4).SetStyle("style=\"width:40px;\"").SetContent("[" + to_string(players[pid].target) + "号]"); + playerTable.Get(pid, 5).SetStyle("style=\"width:40px;\"").SetContent(players[players[pid].target].avatar); + } else { + playerTable.MergeRight(pid, 3, 3); + playerTable.Get(pid, 3).SetStyle("style=\"width:120px;\"").SetColor("#FFA07A").SetContent("[玩家状态错误]"); + } } - if (boss.steps >= 0) { - all_record += "
[BOSS] 米诺陶斯"; - all_record += boss.all_record + "
"; + if (boss.Enable()) { + playerTable.Get(playerNum, 0).SetStyle("style=\"width:60px;\"").SetContent("" + boss.GetBossIcon() + ""); + playerTable.MergeRight(playerNum, 1, 2); + playerTable.Get(playerNum, 1).SetStyle("style=\"width:290px;\"").SetColor("lavender").SetContent("[BOSS] " + boss.GetBossName()); + playerTable.Get(playerNum, 3).SetStyle("style=\"width:40px;\"").SetContent("追击
目标"); + playerTable.Get(playerNum, 4).SetStyle("style=\"width:40px;\"").SetContent("[" + to_string(boss.target) + "号]"); + playerTable.Get(playerNum, 5).SetStyle("style=\"width:40px;\"").SetContent(players[boss.target].avatar); } - return clean_markdown(all_record); + return (round > 0 ? "### 第 " + to_string(round) + " 回合" : "") + playerTable.ToString(); + } + + string GetPlayerString() const + { + string players_string; + for (int pid = 0; pid < playerNum; pid++) { + if (pid > 0) players_string += "\n"; + players_string += "[" + to_string(pid) + "号]" + players[pid].name; + if (players[pid].out == 2) players_string += "【逃生舱撤离】"; + else if (players[pid].out == 1) players_string += "【已出局】"; + else if (players[pid].target == 100) players_string += "【单机模式】"; + else if (players[pid].out == 0) players_string += "\n目标→ [" + to_string(players[pid].target) + "号]" + players[players[pid].target].name; + else players_string += "[玩家状态错误]"; + } + if (boss.Enable()) { + players_string += "\n[BOSS] " + boss.GetBossName() + "\n"; + players_string += "目标→ [" + to_string(boss.target) + "号]" + players[boss.target].name; + } + return players_string; + } + + // 完整赛况 + string GetAllRecordHtml(const int query_pid) const + { + const char* style = R"( + +)"; + string record_html; + record_html += "
"; + // 玩家记录 + for (int pid = 0; pid < playerNum; pid++) { + record_html += "
"; + + record_html += "
"; + record_html += "[" + to_string(pid) + "号]" + esc_html(players[pid].name) + ""; + record_html += "
"; + + record_html += "
"; + record_html += players[pid].GetAllMoveRecord(query_pid == pid ? -1 : query_pid); + record_html += "
"; + + record_html += "
"; + } + // BOSS 记录 + if (boss.Enable()) { + record_html += "
"; + record_html += "
[BOSS] " + boss.GetBossName() + " " + boss.GetBossIcon() + "
"; + record_html += "
"; + record_html += boss.GetBossRecord(query_pid); + record_html += "
"; + record_html += "
"; + } + // 额外信息 + if (!all_extra_record.empty()) { + record_html += "
"; + record_html += "
[额外信息]
"; + record_html += "
"; + record_html += all_extra_record; + record_html += "
"; + record_html += "
"; + } + record_html += "
"; + return style + record_html; } - static string clean_markdown(const string& input) + string GetAllRecordString(const int query_pid) const { - 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 record_string; + // 玩家记录 + for (int pid = 0; pid < playerNum; pid++) { + record_string += "[" + to_string(pid) + "号]" + players[pid].name; + if (players[pid].out == 1) record_string += "【已出局】"; + if (players[pid].out == 2) record_string += "【逃生舱撤离】"; + record_string += "\n" + players[pid].GetAllMoveRecord(query_pid == pid ? -1 : query_pid, false) + "\n"; + } + // BOSS 记录 + if (boss.Enable()) { + record_string += "[BOSS] " + boss.GetBossName() + " " + boss.GetBossIcon(); + record_string += boss.GetBossRecord(query_pid, false) + "\n"; + } + // 额外信息 + if (!all_extra_record.empty()) { + record_string += "[额外信息]"; + record_string += replace_br_with_line(all_extra_record) + "\n"; + } + return record_string; } string GetAllScore() const @@ -383,26 +504,37 @@ class Board int nx = (cx + k_DX_Direct[d] + size) % size; int ny = (cy + k_DY_Direct[d] + size) % size; - bool wall = false; - switch (direction) { - case Direct::UP: wall = grid_map[cx][cy].GetWall() != Wall::EMPTY; break; - case Direct::DOWN: wall = grid_map[cx][cy].GetWall() != Wall::EMPTY; break; - case Direct::LEFT: wall = grid_map[cx][cy].GetWall() != Wall::EMPTY; break; - case Direct::RIGHT: wall = grid_map[cx][cy].GetWall() != Wall::EMPTY; break; - default: return false; - } - // 非撞墙尝试移动箱子 - if (!wall && grid_map[nx][ny].Type() == GridType::BOX && players[pid].subspace < 0) { - wall = !BoxMove(nx, ny, k_DX_Direct[d], k_DY_Direct[d]); + bool hit_wall = false; + if (players[pid].subspace < 0) { + switch (direction) { + case Direct::UP: hit_wall = !grid_map[cx][cy].CanPass(); break; + case Direct::DOWN: hit_wall = !grid_map[cx][cy].CanPass(); break; + case Direct::LEFT: hit_wall = !grid_map[cx][cy].CanPass(); break; + case Direct::RIGHT: hit_wall = !grid_map[cx][cy].CanPass(); break; + default: return false; + } + // 非撞墙,尝试移动箱子 + if (!hit_wall && grid_map[nx][ny].Attach() == AttachType::BOX) { + bool box_success = BoxMove(nx, ny, k_DX_Direct[d], k_DY_Direct[d]); + if (box_success) players[pid].achievement.visitAttach(AttachType::BOX); // 成就[乒铃乓啷]辅助 + hit_wall = !box_success; + } + // 撞墙 + if (hit_wall) { + if (!hide) players[pid].NewStepRecord(direction, "撞"); + return false; + } } // 轨迹记录 - if (wall && players[pid].subspace < 0) { - if (!hide) players[pid].NewStepRecord(direction, "撞"); - return false; - } if (!hide) players[pid].NewStepRecord(direction); - if (players[pid].subspace > 0) { + // 非撞墙,炸弹触发,实际不移动 + if (players[pid].bomb_trigger) { + return true; + } + + // 亚空间,实际不移动 + if (players[pid].InSubspace()) { players[pid].subspace--; return true; } @@ -413,6 +545,9 @@ class Board players[pid].y = ny; player_map[nx][ny].push_back(pid); + // 成就记录 + players[pid].achievement.MakeStep(nx, ny); + // 探索分 auto& explore = players[pid].score.explore_map[nx][ny]; if (explore == 0) { @@ -434,13 +569,13 @@ class Board int b_nx = (b_cx + dx + size) % size; int b_ny = (b_cy + dy + size) % size; if (!player_map[b_nx][b_ny].empty()) return false; - if (grid_map[b_nx][b_ny].Type() != GridType::EMPTY) return false; + if (grid_map[b_nx][b_ny].Attach() != AttachType::EMPTY) return false; bool wall = false; - if (dx == -1 && dy == 0) wall = grid_map[b_cx][b_cy].GetWall() != Wall::EMPTY; - else if (dx == 1 && dy == 0) wall = grid_map[b_cx][b_cy].GetWall() != Wall::EMPTY; - else if (dx == 0 && dy == -1) wall = grid_map[b_cx][b_cy].GetWall() != Wall::EMPTY; - else if (dx == 0 && dy == 1) wall = grid_map[b_cx][b_cy].GetWall() != Wall::EMPTY; + if (dx == -1 && dy == 0) wall = !grid_map[b_cx][b_cy].CanPass(); + else if (dx == 1 && dy == 0) wall = !grid_map[b_cx][b_cy].CanPass(); + else if (dx == 0 && dy == -1) wall = !grid_map[b_cx][b_cy].CanPass(); + else if (dx == 0 && dy == 1) wall = !grid_map[b_cx][b_cy].CanPass(); if (wall) return false; bool sameBlock = false; @@ -454,8 +589,8 @@ class Board } if (!sameBlock) return false; - grid_map[b_cx][b_cy].SetType(GridType::EMPTY); - grid_map[b_nx][b_ny].SetType(GridType::BOX); + grid_map[b_cx][b_cy].SetAttach(AttachType::EMPTY); + grid_map[b_nx][b_ny].SetAttach(AttachType::BOX); return true; } @@ -469,7 +604,38 @@ class Board if (it != oldCell.end()) oldCell.erase(it); } - // 地区声响(0无 1沙沙 2啪啪) + // 解析多步行动:将输入字符串解析为方向数组 + static string parseDirections(const string &input, vector &out) + { + // 按 key 长度降序,优先匹配中文 + vector keys; + for (auto& p : direction_map) keys.push_back(p.first); + sort(keys.begin(), keys.end(),[](const string& a, const string& b) { + return a.size() > b.size(); + }); + vector tmp; + size_t i = 0; + while (i < input.size()) { + bool matched = false; + for (const auto& k : keys) { + if (i + k.size() <= input.size() && + input.compare(i, k.size(), k) == 0) { + tmp.push_back(direction_map.at(k)); + i += k.size(); + matched = true; + break; + } + } + if (!matched) { + size_t len = utf8CharLen(static_cast(input[i])); + return input.substr(i, len); // 返回非法字符 + } + } + out = std::move(tmp); + return ""; + } + + // 地区声响 Sound GetSound(const Grid& grid, const bool water_mode) const { switch(grid.Type()) { @@ -513,7 +679,7 @@ class Board // 两玩家在同一位置 if (dx == 0 && dy == 0) { - return ""; + return "同格"; } if (dx == 0) { @@ -559,24 +725,39 @@ class Board pair GetSurroundingWalls(const PlayerID pid) const { Grid grid = grid_map[players[pid].x][players[pid].y]; - if (players[pid].subspace > 0) { + if (players[pid].InSubspace()) { grid.SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); } else { grid.HideSpecialWalls(); } string info; - if (grid.GetWall() == Wall::EMPTY) info += "空"; else info += "墙"; - if (grid.GetWall() == Wall::EMPTY) info += "空"; else info += "墙"; - if (grid.GetWall() == Wall::EMPTY) info += "空"; else info += "墙"; - if (grid.GetWall() == Wall::EMPTY) info += "空"; else info += "墙"; + if (grid.CanPass()) info += "空"; else info += "墙"; + if (grid.CanPass()) info += "空"; else info += "墙"; + if (grid.CanPass()) info += "空"; else info += "墙"; + if (grid.CanPass()) info += "空"; else info += "墙"; + return make_pair(info, GetBoard({{grid}}, GetBoardOptions{.with_player = false, .with_ground = false})); + } + + // [BOSS-邦邦] 周围墙壁信息 + pair GetBangBangSurroundingWalls(const int x, const int y) const + { + Grid grid = grid_map[x][y]; + grid.SetType(GridType::EMPTY).SetAttach(AttachType::BOMB); + grid.HideSpecialWalls(); + string info; + if (grid.CanPass()) info += "空"; else info += "墙"; + if (grid.CanPass()) info += "空"; else info += "墙"; + if (grid.CanPass()) info += "空"; else info += "墙"; + if (grid.CanPass()) info += "空"; else info += "墙"; return make_pair(info, GetBoard({{grid}}, GetBoardOptions{.with_player = false})); } // 玩家随机传送 void TeleportPlayer(const PlayerID pid) { - players[pid].subspace = -1; - players[pid].inHeatZone = false; + players[pid].subspace = -1; // 移除亚空间状态 + players[pid].inHeatZone = false; // 移除热浪区域状态 + players[pid].bomb_trigger = false; // 移除炸弹触发状态 vector> candidates = FindLargestConnectedArea(); // 禁止坐标 @@ -588,7 +769,7 @@ class Board } } } - if (boss.steps >= 0) { + if (boss.Enable()) { forbiddenSources.push_back({boss.x, boss.y}); } for (const auto& player: players) { @@ -614,14 +795,13 @@ class Board std::back_inserter(finalCandidates), [&](auto const& pos) { auto [x, y] = pos; - return !forbidden[x][y] && (boss.steps <= 0 || ManhattanDistance(x, y, boss.x, boss.y) > boss.steps); + return !forbidden[x][y] && (!boss.Enable() || ManhattanDistance(x, y, boss.x, boss.y, size) > boss.steps); } ); - // 无有效区域,不传送 + // 保险方案:无有效区域,直接在最大联通区域内随机 if (finalCandidates.empty()) { - cout << "[debug warning] 传送失败,无有效候选位置" << endl; - return; + finalCandidates = candidates; } std::shuffle(finalCandidates.begin(), finalCandidates.end(), g); @@ -690,142 +870,35 @@ class Board return count; } - // BOSS生成和锁定目标 - void BossSpawn() - { - for (int attempt = 0; attempt < 500; attempt++) { - int x = rand() % size; - int y = rand() % size; - bool nearPlayer = false; - for (int i = 0; i < playerNum && !nearPlayer; i++) { - for (int dx = -1; dx <= 1 && !nearPlayer; dx++) { - for (int dy = -1; dy <= 1 && !nearPlayer; dy++) { - int nx = (players[i].x + dx + size) % size; - int ny = (players[i].y + dy + size) % size; - if (nx == x && ny == y) - nearPlayer = true; - } - } - } - boss.x = x; - boss.y = y; - boss.steps = 0; - if (!nearPlayer) break; - } - // 初始锁定最近的玩家作为目标 - int bestDist = INT_MAX; - vector candidates; - for (int i = 0; i < playerNum; i++) { - int d = ManhattanDistance(boss.x, boss.y, players[i].x, players[i].y); - if (d < bestDist) { - bestDist = d; - candidates.clear(); - candidates.push_back(i); - } else if (d == bestDist) { - candidates.push_back(i); - } - } - boss.target = candidates[rand() % candidates.size()]; - } - - // BOSS更换目标(未更换返回true) - bool BossChangeTarget(const bool reset) - { - int curTarget = boss.target; - int curDist = ManhattanDistance(boss.x, boss.y, players[boss.target].x, players[boss.target].y); - if (reset || players[boss.target].out > 0) curDist = INT_MAX; - for (auto& player : players) { - if (player.out > 0) continue; - int d = ManhattanDistance(boss.x, boss.y, player.x, player.y); - if (d < curDist) { - boss.target = player.pid; - boss.steps = 0; - curDist = d; - } - } - return curTarget == boss.target; - } - - // BOSS移动和更换目标(抓到人返回true) - bool BossMove() - { - int targetDist = ManhattanDistance(boss.x, boss.y, players[boss.target].x, players[boss.target].y); - boss.steps++; - // 步数足够直接走到目标位置 - if (boss.steps >= targetDist) { - boss.x = players[boss.target].x; - boss.y = players[boss.target].y; - boss.steps = 0; - return true; - } - // 执行移动 - int stepsRemaining = boss.steps; - while (stepsRemaining > 0) { - targetDist = ManhattanDistance(boss.x, boss.y, players[boss.target].x, players[boss.target].y); - int tx = players[boss.target].x, ty = players[boss.target].y; - int dx = tx - boss.x, dy = ty - boss.y; - if (abs(dx) > size / 2) { dx = (dx > 0) ? dx - size : dx + size; } - if (abs(dy) > size / 2) { dy = (dy > 0) ? dy - size : dy + size; } - - // 如果|dx|≠|dy|则沿较大差值轴走一步 - if (abs(dx) != abs(dy)) { - if (abs(dx) > abs(dy)) { - boss.x = (boss.x + ((dx > 0) ? 1 : -1) + size) % size; - } else { - boss.y = (boss.y + ((dy > 0) ? 1 : -1) + size) % size; - } - } else { - // 如果已经正方形,随机选择在 x 或 y 方向上移动一步 - if (rand() % 2 == 0) - boss.x = (boss.x + ((dx > 0) ? 1 : -1) + size) % size; - else - boss.y = (boss.y + ((dy > 0) ? 1 : -1) + size) % size; - } - stepsRemaining--; - } - return false; - } - - bool IsBossNearby(const Player& player) const - { - for (int dx = -1; dx <= 1; dx++) { - for (int dy = -1; dy <= 1; dy++) { - int nx = (boss.x + dx + size) % size; - int ny = (boss.y + dy + size) % size; - if (player.x == nx && player.y == ny) { - return true; - } - } - } - return false; - } - private: // 初始化区块 - void InitializeBlock() + void InitializeBlocks() { vector maps = unitMaps.maps; vector exits = unitMaps.exits; std::shuffle(maps.begin(), maps.end(), g); std::shuffle(exits.begin(), exits.end(), g); + const int selected_map_num = gamemode >= 0 ? unitMaps.pos.size() - exit_num : maps.size(); + const int selected_exit_num = gamemode >= 0 ? exit_num : exits.size(); + vector selected; - for (int i = 0; i < unitMaps.pos.size() - exit_num; i++) { + for (int i = 0; i < selected_map_num; i++) { selected.push_back(maps[i]); } - for (int i = 0; i < exit_num; i++) { + for (int i = 0; i < selected_exit_num; i++) { selected.push_back(exits[i]); } std::shuffle(selected.begin(), selected.end(), g); for (int i = 0; i < unitMaps.pos.size(); i++) { - if (selected[i].type == GridType::EXIT) { + if (selected[i].is_exit) { unitMaps.SetExitBlock(unitMaps.pos[i].first, unitMaps.pos[i].second, grid_map, selected[i].id, true); } else { unitMaps.SetMapBlock(unitMaps.pos[i].first, unitMaps.pos[i].second, grid_map, selected[i].id, true); } // 记录特殊地图坐标(暂不使用) - // if (selected[i].type == GridType::SPECIAL) { + // if (selected[i].is_special) { // special_pos = unitMaps.pos[i]; // } } @@ -842,23 +915,24 @@ class Board int nx = (x + k_DX_Direct[d] + size) % size; int ny = (y + k_DY_Direct[d] + size) % size; - Wall w1 = grid_map[x][y].GetWallByEnum(dir); - Wall w2 = grid_map[nx][ny].GetWallByEnum(rev); + Wall w1 = grid_map[x][y].GetWall(dir); + Wall w2 = grid_map[nx][ny].GetWall(rev); Wall w = static_cast(w1) > static_cast(w2) ? w1 : w2; - if (w != w1) grid_map[x][y].SetWallByEnum(dir, w); - if (w != w2) grid_map[nx][ny].SetWallByEnum(rev, w); + if (w != w1) grid_map[x][y].SetWall(dir, w); + if (w != w2) grid_map[nx][ny].SetWall(rev, w); } } } } - // 封闭传送门替换为水洼 + // 封闭传送门自动更新传送门样式 void FixInvalidPortals(vector>& grid_map) const { for (int x = 0; x < size; x++) { for (int y = 0; y < size; y++) { if (grid_map[x][y].Type() == GridType::PORTAL && grid_map[x][y].IsFullyEnclosed()) { pair pRelPos = grid_map[x][y].PortalPos(); + grid_map[x][y].SetType(GridType::ONEWAYPORTAL); grid_map[x + pRelPos.first][y + pRelPos.second].SetType(GridType::ONEWAYPORTAL); } } @@ -870,20 +944,20 @@ class Board { int nx = (x + dx + size) % size; int ny = (y + dy + size) % size; - if (grid_map[x][y].Type() == GridType::HEAT || grid_map[x][y].Type() == GridType::BOX) { + if (grid_map[x][y].Type() == GridType::HEAT || grid_map[x][y].Attach() == AttachType::BOX) { return false; } - if (grid_map[nx][ny].Type() == GridType::HEAT || grid_map[nx][ny].Type() == GridType::BOX) { + if (grid_map[nx][ny].Type() == GridType::HEAT || grid_map[nx][ny].Attach() == AttachType::BOX) { return false; } if (dx == -1 && dy == 0) - return grid_map[x][y].GetWall() == Wall::EMPTY && grid_map[nx][ny].GetWall() == Wall::EMPTY; + return grid_map[x][y].CanPass() && grid_map[nx][ny].CanPass(); else if (dx == 1 && dy == 0) - return grid_map[x][y].GetWall() == Wall::EMPTY && grid_map[nx][ny].GetWall() == Wall::EMPTY; + return grid_map[x][y].CanPass() && grid_map[nx][ny].CanPass(); else if (dx == 0 && dy == -1) - return grid_map[x][y].GetWall() == Wall::EMPTY && grid_map[nx][ny].GetWall() == Wall::EMPTY; + return grid_map[x][y].CanPass() && grid_map[nx][ny].CanPass(); else if (dx == 0 && dy == 1) - return grid_map[x][y].GetWall() == Wall::EMPTY && grid_map[nx][ny].GetWall() == Wall::EMPTY; + return grid_map[x][y].CanPass() && grid_map[nx][ny].CanPass(); return false; } @@ -1145,71 +1219,95 @@ class Board return largest; } - // 计算两点间曼哈顿距离 - int ManhattanDistance(int x1, int y1, int x2, int y2) const + string GetImageTypeFolder() const { - int dx = abs(x1 - x2); - int dy = abs(y1 - y2); - dx = std::min(dx, size - dx); - dy = std::min(dy, size - dy); - return dx + dy; + switch (image_type_) { + case 0: return "classic/"; + case 1: return "retro/"; + default: return ""; + } } - string GetGridStyle(const GridType type, const bool is_bg) const + // 获取样式 + string GetGridStyle(const GridType type, const AttachType attach, const bool is_bg) const { + string bg_image = ToFileUrl(image_path_ + GetImageTypeFolder() + GetGridImage(type)); + string attach_image = ToFileUrl(image_path_ + GetImageTypeFolder() + GetAttachImage(attach)); + if (is_bg) { - return "style=\"background-image: url('file:///" + image_path_ + GetGridImage(type) + "');\""; + return "style=\"" + "background-image: url('" + attach_image + "'), url('" + bg_image + "');" + "background-repeat: no-repeat, no-repeat;" + "background-position: center, center;" + "background-size: contain, cover;" + "\""; } else { - return ""; + return "
" + "" + "" + "
"; } } - string GetGridImage(const GridType type) const + string GetWallStyle(const Wall wall, const string& direction) const { - switch (type) { - case GridType::EMPTY: return "empty.png"; - case GridType::GRASS: return "grass.png"; - case GridType::WATER: return "water.png"; - case GridType::ONEWAYPORTAL: return "oneway_portal.png"; - case GridType::PORTAL: return "portal.png"; - case GridType::EXIT: return "exit.png"; - case GridType::TRAP: return "trap.png"; - case GridType::HEAT: return "heat.png"; - case GridType::BOX: return "box.png"; - case GridType::BUTTON: return "button.png"; - default: return "unknown.png"; - } + const string folder = GetImageTypeFolder() + "walls/"; + string wall_image = ToFileUrl(image_path_ + folder + GetWallImage(wall, direction)); + return "background-image: url('" + wall_image + "');"; } - string GetWallColor(const Wall wall) const + static string GetColoredMark(const string& content, const GridType type, const AttachType attach) { - switch (wall) { - case Wall::EMPTY: return "#FFFFFF"; - case Wall::NORMAL: return "#000000"; - case Wall::DOOR: return "#EA68A2"; - default: return "#D9D9D9"; + string color; + + switch (type) { + case GridType::PORTAL: color = "#FFFF00"; break; + case GridType::ONEWAYPORTAL: color = "#FFF8E7"; break; + // case GridType::HEAT: color = "#00FFFF"; break; + default:; } + switch (attach) { + case AttachType::BOMB: color = "#FFFF00"; break; + default:; + } + + if (color.empty()) return "" + content + ""; + return "" + content + ""; } - std::mt19937 g; - inline static constexpr const char* style = R"( + static string GetBoardStyle() + { + return R"( )"; + } }; diff --git a/games/long_night/boss.h b/games/long_night/boss.h new file mode 100644 index 0000000..b24ff3f --- /dev/null +++ b/games/long_night/boss.h @@ -0,0 +1,220 @@ + +enum class BossType { + NONE, + MINOTAUR, + BANGBANG, +}; + +// BOSS暴君 +// 1、每回合开始时,boss将公开本回合需要玩家遵守的“铁律”。若有玩家不遵守,则成为被boss追逐的目标。 +// 2、若不遵守玩家为复数,则选择距离最近的。若已有追逐目标时产生新的不遵守玩家,则转移目标。 +// 3、boss的速度初始为7,随回合+1。 +// 4、第一回合的铁律随机池: +// ①禁足:移动小于等于1步。 +// ②活动:移动大于等于1步。 +// 5、除第一回合的铁律随机池: +// ①噤声:不得发出声音。 +// ②仁爱:不得杀生。 +// ③牢笼:不得逃生(等于没有规则,违反此条规则,boss会无能狂怒)。 +// ④活动:移动大于等于2-5步(随回合增长)。 +// ⑤禁足:移动小于等于2-5步(随机) +// 6、默认规则:暴君可被玩家点杀,暴君在下回合开始时复活,并且以弑君者为追捕目标。若玩家弑君并逃生或存活最后可获得成就“弑君者” + +class Boss +{ + public: + Boss(int& size, vector& players) : size(size), players(players) {} + + // BOSS信息 + BossType type = BossType::NONE; + int x, y; + int steps = -1; + PlayerID target = 0; + + // 辅助成员变量 + int& size; // 地图大小 + vector& players; // Board玩家引用 + + // BOSS行为信息 + struct BossMove { + string content; + Sound sound; + vector propagation; // 声音向其他所有玩家的传播方向(私信完整赛况) + + BossMove(string content, Sound sound) : content(content), sound(sound) {} + }; + vector boss_all_record; + + void NewRecord(const string& content, const Sound sound = Sound::NONE) { boss_all_record.push_back({content, sound}); } + void UpdateContentRecord(const string& content) { if (!boss_all_record.empty()) boss_all_record.back().content = content; } + void UpdateSoundRecord(const Sound sound) { if (!boss_all_record.empty()) boss_all_record.back().sound = sound; } + void AddSoundPropagation(const string& direct_str) { if (!boss_all_record.empty()) boss_all_record.back().propagation.push_back(direct_str); } + // 添加BOSS开局记录 + void InitBossStartRecord() + { + if (Is(BossType::MINOTAUR)) NewRecord("【开局】初始锁定玩家为 [" + to_string(target) + "号]", Sound::BOSS); + if (Is(BossType::BANGBANG)) NewRecord("【开局】初始锁定玩家为 [" + to_string(target) + "号]"); + } + + string GetBossName() const + { + if (Is(BossType::MINOTAUR)) return "米诺陶斯"; + if (Is(BossType::BANGBANG)) return "邦邦"; + return "[错误BOSS]"; + } + + string GetBossIcon() const + { + if (Is(BossType::MINOTAUR)) return "👹"; + if (Is(BossType::BANGBANG)) return "💣"; + return "⚠️"; + } + + string GetBossStartInfo() const + { + switch (type) { + case BossType::MINOTAUR: return "米诺陶斯 现身于地图中,会在回合结束时追击最近的玩家。BOSS发出震耳欲聋的巨响!请所有玩家留意BOSS开局所在的方位!"; + case BossType::BANGBANG: return "邦邦 带着[炸弹]现身于地图中,会在回合结束时追击最近玩家,并在结束位置放置[炸弹]。玩家经过并离开会炸飞并出局!"; + default: return "[错误BOSS]"; + } + } + + string GetBossRecord(const int query_pid, const bool is_html = true) const + { + string result; + + for (const auto& mv : boss_all_record) { + string sound_d; + if (mv.sound != Sound::NONE && query_pid != -1) { + if (query_pid < mv.propagation.size()) { + sound_d = "[" + mv.propagation[query_pid] + "]"; + } else { + sound_d = "{越界异常[pid=" + to_string(query_pid) + "]}"; + } + } + + result += is_html ? "
" : "\n"; + + if (mv.sound == Sound::BOSS) { // [巨响] + result += mv.content + "(巨响" + sound_d + ")"; + } else { + result += mv.content; + } + } + + return result; + } + + // BOSS初始化 + void BossInitialize(const int type_num) + { + this->type = ToBossType(type_num); + if (Is(BossType::MINOTAUR)) this->steps = 0; + if (Is(BossType::BANGBANG)) this->steps = rand() % 3 + 3; + BossSpawn(); + BossChangeTarget(true); + } + + // BOSS生成 + void BossSpawn() + { + // 不生成在玩家周围8格 + for (int attempt = 0; attempt < 500; attempt++) { + int x = rand() % size; + int y = rand() % size; + bool nearPlayer = false; + for (int i = 0; i < players.size() && !nearPlayer; i++) { + for (int dx = -1; dx <= 1 && !nearPlayer; dx++) { + for (int dy = -1; dy <= 1 && !nearPlayer; dy++) { + int nx = (players[i].x + dx + size) % size; + int ny = (players[i].y + dy + size) % size; + if (nx == x && ny == y) + nearPlayer = true; + } + } + } + this->x = x; + this->y = y; + if (!nearPlayer) break; + } + } + + // BOSS更换目标(更换返回true) + bool BossChangeTarget(const bool reset) + { + int curTarget = this->target; + int curDist = ManhattanDistance(this->x, this->y, players[this->target].x, players[this->target].y, size); + if (reset || players[this->target].out > 0) curDist = INT_MAX; + for (auto& player : players) { + if (player.out > 0) continue; + int d = ManhattanDistance(this->x, this->y, player.x, player.y, size); + if (Is(BossType::BANGBANG) && d == 0) continue; // [邦邦]位置相同需强制更换目标 + if (d < curDist) { + this->target = player.pid; + if (Is(BossType::MINOTAUR)) this->steps = 0; // [米诺陶斯]需要重置步数 + curDist = d; + } + } + return curTarget != this->target; + } + + // BOSS移动和更换目标(抓到人返回true) + bool BossMove() + { + int targetDist = ManhattanDistance(this->x, this->y, players[this->target].x, players[this->target].y, size); + if (Is(BossType::MINOTAUR)) this->steps++; // [米诺陶斯]需要增加步数 + // 步数足够直接走到目标位置 + if (this->steps >= targetDist) { + this->x = players[this->target].x; + this->y = players[this->target].y; + if (Is(BossType::MINOTAUR)) this->steps = 0; // [米诺陶斯]需要重置步数 + return true; + } + // 执行移动 + int stepsRemaining = this->steps; + while (stepsRemaining > 0) { + targetDist = ManhattanDistance(this->x, this->y, players[this->target].x, players[this->target].y, size); + int tx = players[this->target].x, ty = players[this->target].y; + int dx = tx - this->x, dy = ty - this->y; + if (abs(dx) > size / 2) { dx = (dx > 0) ? dx - size : dx + size; } + if (abs(dy) > size / 2) { dy = (dy > 0) ? dy - size : dy + size; } + + // 如果|dx|≠|dy|则沿较大差值轴走一步 + if (abs(dx) != abs(dy)) { + if (abs(dx) > abs(dy)) { + this->x = (this->x + ((dx > 0) ? 1 : -1) + size) % size; + } else { + this->y = (this->y + ((dy > 0) ? 1 : -1) + size) % size; + } + } else { + // 如果已经正方形,随机选择在 x 或 y 方向上移动一步 + if (rand() % 2 == 0) + this->x = (this->x + ((dx > 0) ? 1 : -1) + size) % size; + else + this->y = (this->y + ((dy > 0) ? 1 : -1) + size) % size; + } + stepsRemaining--; + } + return false; + } + + bool IsBossNearby(const Player& player) const + { + for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + int nx = (this->x + dx + size) % size; + int ny = (this->y + dy + size) % size; + if (player.x == nx && player.y == ny) { + return true; + } + } + } + return false; + } + + bool Enable() const { return this->type != BossType::NONE; } + bool Is(BossType type) const { return this->type == type; } + + private: + static BossType ToBossType(int value) { return static_cast(value); } +}; diff --git a/games/long_night/constants.h b/games/long_night/constants.h new file mode 100644 index 0000000..5112b32 --- /dev/null +++ b/games/long_night/constants.h @@ -0,0 +1,361 @@ + +// 方向 +enum class Direct { + UP, + DOWN, + LEFT, + RIGHT, +}; + +// 声响 +enum class Sound { + NONE, + SHASHA, + PAPA, + BOSS, +}; + +// 地形 +enum class GridType { + EMPTY, + GRASS, + WATER, + PORTAL, + EXIT, + TRAP, + HEAT, + ONEWAYPORTAL, +}; + +// 附着 +enum class AttachType { + EMPTY, + BUTTON, + BOMB, + BOX, +}; + +// 墙壁:优先级从低到高,连通性越好优先级理应越高 +enum class Wall { + EMPTY, + NORMAL, + DOOR, + DOOROPEN, +}; + +const int GRID_SIZE = 50; +const int WALL_SIZE = 10; + +const map direction_map = { + {"上", Direct::UP}, {"U", Direct::UP}, {"s", Direct::UP}, + {"下", Direct::DOWN}, {"D", Direct::DOWN}, {"x", Direct::DOWN}, + {"左", Direct::LEFT}, {"L", Direct::LEFT}, {"z", Direct::LEFT}, + {"右", Direct::RIGHT}, {"R", Direct::RIGHT}, {"y", Direct::RIGHT}, +}; + +const string dir_cn[4] = {"上", "下", "左", "右"}; + +int k_DX_Direct[4] = {-1, 1, 0, 0}; +int k_DY_Direct[4] = {0, 0, -1, 1}; + +const string num[10] = {"⓪", "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨"}; + +const int HIDE_LIMIT = 4; + +const int EXTRATIMECRAD_COUNT = 3; +const int EXTRATIMECRAD_TIME = 60; + +// 图片名称 +string GetGridImage(const GridType type) +{ + switch (type) { + case GridType::EMPTY: return "empty.png"; + case GridType::GRASS: return "grass.png"; + case GridType::WATER: return "water.png"; + case GridType::ONEWAYPORTAL: return "oneway_portal.png"; + case GridType::PORTAL: return "portal.png"; + case GridType::EXIT: return "exit.png"; + case GridType::TRAP: return "trap.png"; + case GridType::HEAT: return "heat.png"; + default: return "unknown.png"; + } +} + +string GetAttachImage(const AttachType type) +{ + switch (type) { + case AttachType::EMPTY: return "transparent.png"; + case AttachType::BUTTON: return "button.png"; + case AttachType::BOMB: return "bomb.png"; + case AttachType::BOX: return "box.png"; + default: return "unknown.png"; + } +} + +string GetWallImage(const Wall wall, const string& direction) +{ + switch (wall) { + case Wall::EMPTY: return "empty_" + direction + ".png"; + case Wall::NORMAL: return "wall_" + direction + ".png"; + case Wall::DOOROPEN: return "dooropen_" + direction + ".png"; + case Wall::DOOR: return "door_" + direction + ".png"; + default: return "unknown_" + direction + ".png"; + } +} + +// 相反方向 +Direct opposite(Direct dir) +{ + switch (dir) { + case Direct::UP: return Direct::DOWN; + case Direct::DOWN: return Direct::UP; + case Direct::LEFT: return Direct::RIGHT; + case Direct::RIGHT: return Direct::LEFT; + } + throw std::invalid_argument("Invalid direction"); +} + +// 计算两点间曼哈顿距离 +int ManhattanDistance(int x1, int y1, int x2, int y2, int size) +{ + int dx = abs(x1 - x2); + int dy = abs(y1 - y2); + dx = std::min(dx, size - dx); + dy = std::min(dy, size - dy); + return dx + dy; +} + +// UTF-8字符长度 +size_t utf8CharLen(unsigned char c) +{ + if ((c & 0x80) == 0x00) return 1; // 0xxxxxxx + if ((c & 0xE0) == 0xC0) return 2; // 110xxxxx + if ((c & 0xF0) == 0xE0) return 3; // 1110xxxx + if ((c & 0xF8) == 0xF0) return 4; // 11110xxx + return 1; // 非法 UTF-8 +} + +// 替换html换行为文本换行 +string replace_br_with_line(string s) +{ + const string from = "
"; + const string to = "\n"; + size_t pos = 0; + while ((pos = s.find(from, pos)) != string::npos) { + s.replace(pos, from.length(), to); + pos += to.length(); + } + return s; +} + +// 转义 html/markdown +string esc_html(const string& input) +{ + string result = input; + result = regex_replace(result, regex("&"), "&"); + result = regex_replace(result, regex("<"), "<"); + result = regex_replace(result, regex(">"), ">"); + result = regex_replace(result, regex("\""), """); + result = regex_replace(result, regex("'"), "'"); + return result; +} + +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; +} + +// 根据系统转换文件URL +string ToFileUrl(const string& path) +{ + string url = path; + // 兼容 Windows 反斜杠 + replace(url.begin(), url.end(), '\\', '/'); + + #ifdef _WIN32 + // 如果路径开头是盘符,保证 file:///C:/... 格式 + if (url.size() >= 2 && url[1] == ':') { + url = "file:///" + url; + } else { + url = "file://" + url; + } + #else + url = "file://" + url; + #endif + + return url; +} + +/* ========== 游戏文案 ========== */ +string GetRandomHint(span hints) +{ + return string(hints[rand() % hints.size()]); +} + +// 撞墙提示 +const array wall_hints = { + "砰!你狠狠地撞在了一堵墙上!", // 铁蛋 + "一阵剧痛传来,你撞上了一堵墙,看来这里走不通。", + "黑暗中,你的身体撞击了一面粗糙的墙壁。", + "你听到一声沉闷的回响——是墙壁挡住了你的去路。", + "墙壁在你面前横亘,仿佛无情地阻挠着前行的路。", + "墙壁冷漠地矗立在你面前,拒绝让你通过。", + "前方一堵高墙挡住了你的去路。", + "你试图继续前进,但一堵墙挡住了你的去路。", + "砰!你撞上了一堵墙,幸好没人看到。", + "你决定挑战墙壁,结果墙壁胜利了。", + "看来你的“穿墙术”还没练成。", + "你试图与墙壁讲道理,但它完全不想搭理你。", + "你向前冲去,然后优雅地和墙壁来了个亲密接触。", + "你试图用意念穿过墙壁,然而它比你的意念还坚定。", + "黑暗中,你的手掌摸到一堵砖墙上,你期待的邂逅没有发生。", // 大萝卜姬 + "噢是墙壁,你情不自禁地把耳朵贴了上去,你很失望。", + "是一面光滑湿软的墙!快趁机误导一下对手!信我的邪,你发出了只能自己听见的啪啪声。", + "你的手掌按在冰冷的墙面上,它仿佛正在吞噬你的温度。你是南方人啊,试试舌头吧!", + "彭!靠北啦,你不看路的吗?拜托~这么大一面墙你就这样撞啊。", + "砰!你撞到了墙上。看来你数错了,9又3/4并不是那么好找的。", + "你似乎走到了墙面上?快醒醒,这里并不是匹诺康尼,别做白日梦了。", // 三月七 + "这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。", + "你申请对墙壁过一个说服,dm拒绝了你。", + "你尝试使用闪现过墙,但可惜墙体的厚度超出了你的预期。", + "墙壁温柔的注视着你,不再言语。", + "仿生铁蛋bot会梦到电子萝卜吗?至少墙壁不会给你答案。", + "你在平原上走着,突然迎面遇到一堵墙,这墙向上无限高,向下无限深,向左无限远,向右无限远,这墙是什么?\n当然不可能有这样的墙,无论材质是什么,都会因为无限大的重力坍缩的。这只是那些神经兮兮的糟老头子的臆想罢了。\n不过,你确实撞在了一堵墙上。", // H3PO4 + "黑暗中突然出现的墙壁,像是命运在说:换个方向试试?", // 月影 + "你试图用脸测量墙壁的硬度,恭喜获得物理系荣誉学位!", + "砰!你与墙壁进行了深入交流,结论是它比你想象的更固执。", + "砰!脑门和墙壁的亲密接触,证明了你对探索的执着!", // 小葵 +}; +// 第一步撞墙提示 +const array firststep_wall_hints = { + "这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。", // 三月七 + "你知道吗,撞击同一面墙114514次即可将其撞倒!", + "看来有人认为自己在玩多层迷宫……别想了,这是永久墙壁。", + "俗话说不撞南墙不回头,但有人撞了南墙也不回头。", +}; +// 树丛提示 +const array grass_hints = { + "你踏入一片树丛,枯叶和树枝在脚下沙沙作响。", // 铁蛋 + "你一脚踏入了一片树丛,树叶发出了沙沙声,仿佛某种回应。", + "树叶微微颤动,沙沙声仿佛在轻声低语。", + "脚下传出“沙沙”的声音……你希望这只是树叶,而不是别的东西。", + "你踏入一片树丛,沙沙声在寂静的黑暗里显得格外刺耳。", + "枝叶在你身上扫过,沙沙声中,它刮破了你的蚕丝薄衫和渔网袜。", // 大萝卜姬 + "你突然跌进了一片黑暗的树丛,诡异的沙沙声勾起了你不好的回忆。", + "随着一阵沙沙声,密集的枝杈无情地划开了你的衣物和皮肤。", + "你路过一片树丛,出现了野生的妙蛙种子!快使用大师球!", // 三月七 + "你踩到了一根萝卜……等等,树林里怎么会有萝卜?", + "沙沙,沙沙,这片丛林的背后,会不会住着小红帽?", + "沙沙,你踏入了危险的树丛。这里要是藏着一个老六,可就遭了……", // Hyacinth +}; +// 啪啪提示 +const array papa_hints = { + "你向前动了动,下半身传来了啪啪声。", // 大萝卜姬 + "啪啪!常在迷宫走,哪有不湿鞋?是的,你踩到了某些液体。", + "啪啪!你问踩的是什么液体?也许是我为那情人留下的泪。", + "啪啪!冰冷的液体渗入了你的鞋里。", + "啪!尽管你动作已经很轻,但还是发出了很大的声音。啪!你决定不管了。", + "啪啪!是谁那么不讲公德!随地……", + "啪啪!冰冷的涟漪在你的鞋边回荡,你想起了那个陪着铁蛋看冰块的下午。", + "啪啪!突如其来的声音让你的动作瞬间冻结;被打破的静谧仿佛被劈开的水,迅速地恢复了无声。你的敏锐好像没有得到回应。", + "啪嗒!你好像踩到了地雷?低头看一看,还好,只是一些液体。", // 三月七 + "啪!你似乎有什么东西掉了进去,可惜这里并没有河神。", +}; +// 陷阱触发提示 +const array trap_hints = { + "深阱垂空百尺方,足悬铁索断人肠。", // 齐齐 + "犹似魂断垓下道,恨满胸中万古刀。", + "你想起守株待兔的故事,只是此刻,你成为了那只兔子。", // 纤光 + "为坠落的人类命名:_________", + "恭喜🎉被特斯拉捕获,电击调教一回合", // 特斯拉 +}; +// 热浪提示 +const array heat_wave_hints = { + "你感受到了迎面扑来的热浪,炽热的空气仿佛要将你吞没", // 铁蛋 + "你感受到周围弥漫着炽热的气息!", + "!!请注意!!局部出现厄尔尼诺现象,气温异常升高,请注意做好防中暑措施。", // 纤光 + "你感到难以忍受的炎热,“要是周围有个水池就好了…”", + "在这座冰冷的迷宫里,你感到一阵久违的温暖,周围似乎有明亮的光源,吸引你一探究竟…?", +}; +// 热源进入提示 +const array heat_core_hints = { + "你的脚被高温烫伤了,刺痛让你不由得倒吸一口凉气,然而周围的空气同样炙热无比", // 铁蛋 + "然而,光源并不总象征着安全,烈焰利用人对光明的向往,试图再次吞噬一个失落的灵魂。", // 纤光 + "oops!检测到核心温度急剧上升,即将超过阈值…准备启动自毁程序…", + "你相信自己的铜头铁臂可以击败一切,却不知眼前的岩浆能轻易融化所有金属。", +}; +// 热源公开提示 +const array heat_active_hints = { + "你被滚滚热浪淹没了...周围的一切都在高温中扭曲变形,只剩下火焰的深渊。", // 铁蛋 + "哦不!你落入了巨人萝卜的火锅池里,这下你只好成为萝卜的夜宵了。", // 纤光 + "你失败了!\nSteve试图在岩浆中游泳。", +}; +// 炸弹爆炸提示 +const array bomb_hints = { + "轰——你脚下的土地猛地炸裂,烈焰与烟尘把你吞没,眼前一片黑暗。", // 铁蛋 + "轰隆!炸弹瞬间炸响,你被高高弹起,如同流星坠入未知深渊。", + "炽热的冲击波把你掀起,四周的碎石与烟尘如同流星雨,你在空中划出最后的弧线。", + "耳边传来震耳欲聋的巨响,火光吞噬了一切,你的身影消失在滚滚浓烟之中。", + "世界在强光中褪色成负片,最后涌入意识的不是疼痛,而是童年某个夏日的蝉鸣。", + "好消息:你确实飞起来了。坏消息:是以分子扩散的形式。", + "这可能是你最亮眼的时刻——字面意义上的。", + "你刚刚完成了一次无需火箭推进的轨道发射。遗憾的是,没有返回舱。", +}; +// 逃生舱提示 +const array exit_hints = { + "你坐进了逃生舱,在启动的轰鸣声中,你想起了那句话:“不要忘了,这个世界穿透一切高墙的东西,它就在我们的内心深处,他们无法达到,也接触不到,那就是希望。”", // 大萝卜姬 + "躺在逃生舱内,平日并不虔诚的你颤巍巍地画着十字,双手合十,嘴里念念有词。诸如什么真主阿拉耶稣基督释迦牟利急急如律令之类。前窗仿佛响应了你的号召,一阵白色闪光迅速笼罩了你。正当你诧异得到了哪位神仙的庇佑时,眼前浮现出两个大字。一个振奋人心的声音在你耳边响起:“原神,启动!”", + "你忘记躺了多久,你只记得这里很温暖、舒适、令人安心。直到一股力量把你从舱内抽离;强光穿透了你稚嫩的眼皮;你哭了,你向世界宣告着你的降临。也许你只是此刻降生的其中之一,但在她眼里,你就是她的唯一。", + "是的。夜色再浓,也挡不住黎明的到来,就像再大的困难也挡不住我们的前进。黑暗即将过去,曙光就在前头!", + "“我们城市崇尚和平,人人都善良和谐自觉拥护一切美好。是因为我们坚信堵不如疏。每年的今天我们都会举办 [长夜节]。在这一天我们可以放下所有原则,释放心中的恶意并且不被追究责任。”今年优胜的你当然可以如此说道。", + "“恭喜逃生!请选择:①清空记忆并返回现实②复活所有选手重新逃杀”\n“②”你斩钉截铁道。\n无论还要重来多少次,我只要那个有你的未来。", + "你和同伴睁开眼睛,熟练且迅速地摘下入耳式共享梦境机。\n“我觉得我们应该给足暗示了吧”同伴迅速地收拾着周围散落的仪器。\n“给的足足的,关键是他会认为是自己这样想的。他一定会给我们升至少两个职级。”你立马摘下床上目标的梦境机,与同伴一起安静又麻利地从窗户离开了卧室。", + "你出生后 时间就已所剩无几\n在妈妈离世之后 我不知对你倾注了多少的爱呢\n但是你的微笑让爸爸备受鼓舞呀(^_^)\n其实要是能一起走就好了 但没能做到\n希望你能忘记一切继续前行 你一定可以做到的", // 纤光 + "当逃生舱门缓缓关闭,伴随着沉闷的启动声,黑暗迷宫逐渐远去。依靠在逃生舱内冷冽的仪表光芒中,你仿佛听见遥远星辰的低语:“未来,总为勇者留下一缕希望。”", // 铁蛋 + "舱门缓缓关闭,逃生舱的指示灯一一亮起,冰冷的金属包裹着你,但比起外面的黑暗,这里却意外地令人安心。你知道,一切都已经结束,或许,也是一切的开始。", + "你说的对,但是《漫漫长夜》是由大萝卜姬自主研发的一款全新大逃杀游戏。游戏发生在一个被称作“黑暗迷宫”的地图,在这里,被铁蛋选中的人将被授予“树丛”,导引“沙沙”之力。你将扮演一位名为“狩猎者”的神秘角色,在自由的旅行中邂逅性格各异、能力独特的墙壁,和它们一起阻拦对手,找到隐匿的“逃生者”——同时,逐步发掘“逃生舱”的真相。", // 三月七 + "进入逃生舱后,随着几下逐渐变弱的震动,周围的环境随之稳定下来。也就在这时,你眼前闪过一道白光,似乎是这使得你进入了一个全新的环境,伴随着的还有来自外部的一阵欢呼声:“太好了!成功抓住宝可梦了!”", // faust + "一阵失重后,舱门终于打开。随着刺眼的白光,在指缝间你看见几个面目可憎的巨人在围观你。很快地你被巨大的餐叉粗暴地刺穿;顾不及对痛觉反应,你便殒命在血盘大口之中。健硕、坚定、智慧、乐观,这些优秀的品质在他们嘴里同样珍贵。", // 大萝卜姬 + "当逃生舱的舱门关闭时,你才发现手中的钥匙根本不属于这里。系统提示音冰冷地重复着:身份验证失败。原来从一开始,你就只是这个迷宫的装饰品而已。", // 月影 + "逃生舱启动的瞬间,你突然想起那个古老的预言:'逃出迷宫的人将获得永生,但代价是永远孤独'。舱体剧烈震动起来,不知是故障还是某种警告...", + "舱内显示屏突然亮起:'恭喜您成为第1024位逃生者!作为奖励,系统将向您展示迷宫的真相...'画面切换的瞬间,你看到了无数个一模一样的逃生舱,里面坐着无数个一模一样的你。", + "当逃生舱启动时,你突然明白:黑暗不是终点,而是黎明前的温柔。舱内温度逐渐升高,那不是故障,而是新生的心跳。", + "逃生舱启动的轰鸣声渐渐平息,取而代之的是轻柔的摇篮曲。透过舷窗,你看见繁星组成的银河缓缓流动——原来迷宫的出口,一直连接着整片宇宙。", + "当舱门完全关闭的瞬间,你听见系统轻声说:'恭喜,这是第1024次模拟。根据数据,你这次终于选择相信自己了。' 周围突然亮起温暖的阳光,原来真正的逃生舱,一直都在你心里。", + "逃生舱的显示屏突然亮起一行字:'记住,黑暗只是光明的候车室。' 随着这句话,整个舱体开始散发出柔和的金色光芒,照亮了通往新世界的道路。", +}; +// 捕捉提示 +const array catch_hints = { + "星光黯淡,你们的相遇,是命中注定,亦是命终注定。", // 纤光 + "你化作一道黑影,在血月之下,无情地终结了又一条生命。", + "你想触碰一切的真相,但在对方空洞无神的双眼中,你没能找到答案。", + "你叹了口气,在黑暗森林里,你不得不这样做。", + "我说我杀人不眨眼,你问我眼睛干不干?永别了", // 克里斯丁 + "感谢你为了我自愿放弃逃生资格", +}; +// 同格树丛声响提示 +const array grass_sound_hints = { + "你听见有人进入了你的小树丛,沙沙声很近很近;一股接一股热气扑向了你的耳朵;呼…哈……呼…哈……他好像很累的样子。积极地想,他也许没有察觉到你", // 大萝卜姬 + "你听见有人进入了你所在的树丛,他从旁边匆匆走过,没有发现你。阴暗的想法在你心里成长起来,是让他帮你探路,还是直接干掉。甚至运气好的话,抢在前面牛了他的逃生舱……(额外探索分只有1分并逃生一事在漫漫长夜中亦有记载)", // Hyacinth +}; +// 同格啪啪声响提示 +const array papa_sound_hints = { + "“啪!”你汗毛直立,有人来了。。幸好,你并没有站在中间,多疑多虑的性格给了你久违的回报。你小心翼翼地蹲了下来,尽力减少接触概率。只是黑暗中你低估了脚下液体的深度。“等他走远,再把内裤脱了吧。。”你暗暗地想。", // 大萝卜姬 + "啪!啪!看来是有人来了。两个人,狭小的隔间,不间断地啪啪声……'淫秽的人!'你的脑海回想起了她的声音。是啊。我承认,我确实有点想她了。", +}; +// 无逃生舱最后生还 +const array withoutE_win_hints = { + "我睁开了双眼,眼前的一切既熟悉又陌生。看来这次终于是我赢了。我用力地端详着周遭的一切,试图捕捉错过的几日时光的任何蛛丝马迹。“我真希望他们彻底离开了 ......”说完,我在床脚拿起了本该在枕边的剃须刀;“看来上次赢的是萝卜。”我下意识地抹了抹嘴唇。在指尖晕开的口红证实了我的猜测。我笑了。", // 大萝卜姬 + "你醒啦?现在已经是第二天了哦。\n明媚的阳光照进迷宫,耳旁传来小鸟的叫声,一切美好的不太真实,唯有眼前冰冷的血迹,无声的诉说着昨晚的那场噩梦,而有些人,永远留在了那场梦中。\n可你,真的从中逃出来了吗?\n“地形参数设置完毕,新的循环正在重启……”", // 纤光 +}; +// 有逃生舱但死斗取胜 +const array withE_win_hints = { + "“已经没事啦~”她温柔地从背后把你抱住,轻轻抚摸着头。“你很勇敢,这一步太不容易了。”你转过身,紧紧埋进她柔软的身体里放肆哭泣。“一切都结束了。不用再害怕了。”她低下头凑近你的耳边,“咱们回家叭…”", // 大萝卜姬 + "多年以后,在家族背景下你在事业上取得了巨大成就。大家把你的性情大变归功于当年失踪逃生的经历。“之前那个玩世不恭的我已经死了。”你每次都这样认真回答大家。至于细节的提问嘛,失忆这个理由你很喜欢。", +}; diff --git a/games/long_night/grid.h b/games/long_night/grid.h index 4174498..3c9569e 100644 --- a/games/long_night/grid.h +++ b/games/long_night/grid.h @@ -1,218 +1,14 @@ -enum class Direct { - UP, - DOWN, - LEFT, - RIGHT, -}; - -enum class Sound { - NONE, - SHASHA, - PAPA, - BOSS, -}; - -enum class Wall { - EMPTY, - NORMAL, - DOOR, -}; - -enum class GridType { - SPECIAL, - EMPTY, - GRASS, - WATER, - PORTAL, - EXIT, - TRAP, - HEAT, - BOX, - BUTTON, - ONEWAYPORTAL, -}; - -const map direction_map = { - {"上", Direct::UP}, {"U", Direct::UP}, {"s", Direct::UP}, - {"下", Direct::DOWN}, {"D", Direct::DOWN}, {"x", Direct::DOWN}, - {"左", Direct::LEFT}, {"L", Direct::LEFT}, {"z", Direct::LEFT}, - {"右", Direct::RIGHT}, {"R", Direct::RIGHT}, {"y", Direct::RIGHT}, -}; - -static constexpr int k_DX_Direct[4] = {-1, 1, 0, 0}; -static constexpr int k_DY_Direct[4] = {0, 0, -1, 1}; - -const string num[10] = {"⓪", "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨"}; - -const int hide_limit = 4; - -const char* score_rule = R"( - 【抓人分】
-抓人+100,被抓-100
- 【逃生分】
-第1/2/3/4个逃生+150/200/250/250
- 【探索分】
-每探索一个自己未探索的格子+1
-每探索一个所有玩家未探索的格子额外+1
-【退出惩罚】强制退出-300分
)"; - -Direct opposite(Direct dir) -{ - switch (dir) { - case Direct::UP: return Direct::DOWN; - case Direct::DOWN: return Direct::UP; - case Direct::LEFT: return Direct::RIGHT; - case Direct::RIGHT: return Direct::LEFT; - } - throw std::invalid_argument("Invalid direction"); -} - - -class Score -{ - public: - Score(const int size) - { - explore_map.resize(size); - for (int i = 0; i < size; i++) { - for (int j = 0; j < size; j++) { - explore_map[i].push_back(0); - } - } - } - // 抓人分 - int catch_score = 0; - // 逃生分 - static constexpr const int exit_order[4] = {150, 200, 250, 250}; - int exit_score = 0; - // 探索分 - vector> explore_map; - // 退出惩罚 - int quit_score = 0; - - pair ExploreCount() const - { - int count1 = 0, count2 = 0; - for (const auto &row : explore_map) { - count1 += count(row.begin(), row.end(), 1); - count2 += count(row.begin(), row.end(), 2); - } - return make_pair(count1, count2); - } - - static string ScoreInfo() { return score_rule; } - - int FinalScore() const { return catch_score + exit_score + ExploreScore() + quit_score; } - - private: - int ExploreScore() const - { - auto [c1, c2] = ExploreCount(); - return c2 * 2 + c1 * 1; - } -}; - - -class Player +class Grid { public: - Player(const PlayerID pid, const string &name, const string &avatar, const int size) - : pid(pid), name(name), avatar(avatar), score(size) {} - - // 玩家信息 - const PlayerID pid; // 玩家ID - const string name; // 玩家名字 - const string avatar; // 玩家头像 - // 出局(1被抓 2出口) - int out = 0; - // 当前坐标 - int x, y; - // 抓捕目标 - PlayerID target; - // 移动相关 - int subspace = -1; // 亚空间剩余步数 - string 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; + // 按钮触发位置关联 + struct ButtonTarget { + int dx; + int dy; + std::optional dir; }; - 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; - } -}; - - -class Grid -{ - public: void PortalTeleport(Player& player) const { player.x += portalRelPos.first; @@ -226,8 +22,8 @@ class Grid { Wall& target = wall[static_cast(dir)]; switch (target) { - case Wall::DOOR: target = Wall::EMPTY; break; - case Wall::EMPTY: target = Wall::DOOR; break; + case Wall::DOOR: target = Wall::DOOROPEN; break; + case Wall::DOOROPEN: target = Wall::DOOR; break; default:; } } @@ -235,10 +31,18 @@ class Grid void HideSpecialWalls() { for (int i = 0; i < 4; i++) - if (wall[i] != Wall::EMPTY) wall[i] = Wall::NORMAL; + if (CanPass(i)) + wall[i] = Wall::EMPTY; + else + wall[i] = Wall::NORMAL; } - + template + bool CanPass() const { return CanPass(static_cast(direct)); } + bool CanPass(const int wall_id) const + { + return wall[wall_id] == Wall::EMPTY || wall[wall_id] == Wall::DOOROPEN; + } bool IsFullyEnclosed() const { return wall[0] == Wall::NORMAL && wall[1] == Wall::NORMAL && wall[2] == Wall::NORMAL && wall[3] == Wall::NORMAL; @@ -250,7 +54,7 @@ class Grid template void SetWall(const Wall new_wall) { wall[static_cast(direct)] = new_wall; } - void SetWallByEnum(Direct direct, const Wall new_wall) { wall[static_cast(direct)] = new_wall; } + void SetWall(Direct direct, const Wall new_wall) { wall[static_cast(direct)] = new_wall; } Grid& SetWall(const Wall up, const Wall down, const Wall left, const Wall right) { wall[0] = up; @@ -261,48 +65,63 @@ class Grid } Grid& SetType(const GridType type) { - this->type = type; + this->grid = type; + return *this; + } + Grid& SetAttach(const AttachType type) + { + this->attach = type; return *this; } Grid& SetPortal(const int relPosX, const int relPosY) { this->portalRelPos = {relPosX, relPosY}; return *this; } - Grid& SetButton(const int relPosX, const int relPosY, const optional dir = nullopt) + Grid& SetButton(const vector& button_targets) { - this->buttonRelPos = {relPosX, relPosY, dir}; + this->buttonTargetPos = button_targets; return *this; } void SetGrowable(const bool growable) { this->growable = growable; } - void SetContent(const string& content, const string& color = "") - { - if (color.empty()) { - this->content.first = content; - } else { - this->content.first = "" + content + ""; - } - } + void SetContent(const string& content) { this->content.first = content; } void SetWallContent(const vector& wall_content) { this->content.second = wall_content; } template Wall GetWall() const { return wall[static_cast(direct)]; } - Wall GetWallByEnum(Direct direct) const { return wall[static_cast(direct)]; } - GridType Type() const { return type; } + Wall GetWall(Direct direct) const { return wall[static_cast(direct)]; } + GridType Type() const { return grid; } + AttachType Attach() const { return attach; } pair PortalPos() const { return portalRelPos; } bool TrapStatus() const { return trap; } bool CanGrow() const { return growable; } pair> GetContent() const { return content; } - // 按钮触发位置关联 - struct ButtonTarget { - int dx; - int dy; - std::optional dir; - }; - ButtonTarget ButtonTargetPos() const { return buttonRelPos; } + vector ButtonTargetPos() const { return buttonTargetPos; } + + // 获取墙壁上的提示文本 + static string GetWallContent(const vector>& grid_map, const int x, const int y, const Direct direction) + { + const int d = static_cast(direction); + const int od = static_cast(opposite(direction)); + const vector vec1 = grid_map[x][y].GetContent().second; + if (d >= 0 && d < (int)vec1.size() && !vec1[d].empty()) { + return vec1[d]; + } + int nx = x + k_DX_Direct[d]; + int ny = y + k_DY_Direct[d]; + if (0 <= nx && nx < grid_map.size() && 0 <= ny && ny < grid_map.size()) { + const vector vec2 = grid_map[nx][ny].GetContent().second; + if (od >= 0 && od < (int)vec2.size() && !vec2[od].empty()) { + return vec2[od]; + } + } + return ""; + } private: // 区块类型 - GridType type = GridType::EMPTY; + GridType grid = GridType::EMPTY; + // 附着类型 + AttachType attach = AttachType::EMPTY; // 四周墙面(上/下/左/右) Wall wall[4] = { Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY }; @@ -314,7 +133,7 @@ class Grid // 传送门相对位置(PORTAL) pair portalRelPos = {0, 0}; // 按钮触发位置(BUTTON) - ButtonTarget buttonRelPos = {0, 0, nullopt}; + vector buttonTargetPos; // 陷阱状态(TRAP) bool trap = true; diff --git a/games/long_night/map.h b/games/long_night/map.h index e8b8dfd..3870102 100644 --- a/games/long_night/map.h +++ b/games/long_night/map.h @@ -1,158 +1,8 @@ // 感谢 BC 提供的区块初始化思路 class UnitMaps { public: - // 撞墙提示 - static constexpr const array wall_hints = { - "砰!你狠狠地撞在了一堵墙上!", // 铁蛋 - "一阵剧痛传来,你撞上了一堵墙,看来这里走不通。", - "黑暗中,你的身体撞击了一面粗糙的墙壁。", - "你听到一声沉闷的回响——是墙壁挡住了你的去路。", - "墙壁在你面前横亘,仿佛无情地阻挠着前行的路。", - "墙壁冷漠地矗立在你面前,拒绝让你通过。", - "前方一堵高墙挡住了你的去路。", - "你试图继续前进,但一堵墙挡住了你的去路。", - "砰!你撞上了一堵墙,幸好没人看到。", - "你决定挑战墙壁,结果墙壁胜利了。", - "看来你的“穿墙术”还没练成。", - "你试图与墙壁讲道理,但它完全不想搭理你。", - "你向前冲去,然后优雅地和墙壁来了个亲密接触。", - "你试图用意念穿过墙壁,然而它比你的意念还坚定。", - "黑暗中,你的手掌摸到一堵砖墙上,你期待的邂逅没有发生。", // 大萝卜姬 - "噢是墙壁,你情不自禁地把耳朵贴了上去,你很失望。", - "是一面光滑湿软的墙!快趁机误导一下对手!信我的邪,你发出了只能自己听见的啪啪声。", - "你的手掌按在冰冷的墙面上,它仿佛正在吞噬你的温度。你是南方人啊,试试舌头吧!", - "彭!靠北啦,你不看路的吗?拜托~这么大一面墙你就这样撞啊。", - "砰!你撞到了墙上。看来你数错了,9又3/4并不是那么好找的。", - "你似乎走到了墙面上?快醒醒,这里并不是匹诺康尼,别做白日梦了。", // 三月七 - "这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。", - "你申请对墙壁过一个说服,dm拒绝了你。", - "你尝试使用闪现过墙,但可惜墙体的厚度超出了你的预期。", - "墙壁温柔的注视着你,不再言语。", - "仿生铁蛋bot会梦到电子萝卜吗?至少墙壁不会给你答案。", - "你在平原上走着,突然迎面遇到一堵墙,这墙向上无限高,向下无限深,向左无限远,向右无限远,这墙是什么?\n当然不可能有这样的墙,无论材质是什么,都会因为无限大的重力坍缩的。这只是那些神经兮兮的糟老头子的臆想罢了。\n不过,你确实撞在了一堵墙上。", // H3PO4 - "黑暗中突然出现的墙壁,像是命运在说:换个方向试试?", // 月影 - "你试图用脸测量墙壁的硬度,恭喜获得物理系荣誉学位!", - "砰!你与墙壁进行了深入交流,结论是它比你想象的更固执。", - "砰!脑门和墙壁的亲密接触,证明了你对探索的执着!", // 小葵 - }; - // 第一步撞墙提示 - static constexpr const array firststep_wall_hints = { - "这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。", // 三月七 - "你知道吗,撞击同一面墙114514次即可将其撞倒!", - "看来有人认为自己在玩多层迷宫……别想了,这是永久墙壁。", - "俗话说不撞南墙不回头,但有人撞了南墙也不回头。", - }; - // 树丛提示 - static constexpr const array grass_hints = { - "你踏入一片树丛,枯叶和树枝在脚下沙沙作响。", // 铁蛋 - "你一脚踏入了一片树丛,树叶发出了沙沙声,仿佛某种回应。", - "树叶微微颤动,沙沙声仿佛在轻声低语。", - "脚下传出“沙沙”的声音……你希望这只是树叶,而不是别的东西。", - "你踏入一片树丛,沙沙声在寂静的黑暗里显得格外刺耳。", - "枝叶在你身上扫过,沙沙声中,它刮破了你的蚕丝薄衫和渔网袜。", // 大萝卜姬 - "你突然跌进了一片黑暗的树丛,诡异的沙沙声勾起了你不好的回忆。", - "随着一阵沙沙声,密集的枝杈无情地划开了你的衣物和皮肤。", - "你路过一片树丛,出现了野生的妙蛙种子!快使用大师球!", // 三月七 - "你踩到了一根萝卜……等等,树林里怎么会有萝卜?", - "沙沙,沙沙,这片丛林的背后,会不会住着小红帽?", - "沙沙,你踏入了危险的树丛。这里要是藏着一个老六,可就遭了……", // Hyacinth - }; - // 啪啪提示 - static constexpr const array papa_hints = { - "你向前动了动,下半身传来了啪啪声。", // 大萝卜姬 - "啪啪!常在迷宫走,哪有不湿鞋?是的,你踩到了某些液体。", - "啪啪!你问踩的是什么液体?也许是我为那情人留下的泪。", - "啪啪!冰冷的液体渗入了你的鞋里。", - "啪!尽管你动作已经很轻,但还是发出了很大的声音。啪!你决定不管了。", - "啪啪!是谁那么不讲公德!随地……", - "啪啪!冰冷的涟漪在你的鞋边回荡,你想起了那个陪着铁蛋看冰块的下午。", - "啪啪!突如其来的声音让你的动作瞬间冻结;被打破的静谧仿佛被劈开的水,迅速地恢复了无声。你的敏锐好像没有得到回应。", - "啪嗒!你好像踩到了地雷?低头看一看,还好,只是一些液体。", // 三月七 - "啪!你似乎有什么东西掉了进去,可惜这里并没有河神。", - }; - // 陷阱触发提示 - static constexpr const array trap_hints = { - "深阱垂空百尺方,足悬铁索断人肠。", // 齐齐 - "犹似魂断垓下道,恨满胸中万古刀。", - "你想起守株待兔的故事,只是此刻,你成为了那只兔子。", // 纤光 - "为坠落的人类命名:_________", - "恭喜🎉被特斯拉捕获,电击调教一回合", // 特斯拉 - }; - // 热浪提示 - static constexpr const array heat_wave_hints = { - "你感受到了迎面扑来的热浪,炽热的空气仿佛要将你吞没", // 铁蛋 - "你感受到周围弥漫着炽热的气息!", - "!!请注意!!局部出现厄尔尼诺现象,气温异常升高,请注意做好防中暑措施。", // 纤光 - "你感到难以忍受的炎热,“要是周围有个水池就好了…”", - "在这座冰冷的迷宫里,你感到一阵久违的温暖,周围似乎有明亮的光源,吸引你一探究竟…?", - }; - // 热源进入提示 - static constexpr const array heat_core_hints = { - "你的脚被高温烫伤了,刺痛让你不由得倒吸一口凉气,然而周围的空气同样炙热无比", // 铁蛋 - "然而,光源并不总象征着安全,烈焰利用人对光明的向往,试图再次吞噬一个失落的灵魂。", // 纤光 - "oops!检测到核心温度急剧上升,即将超过阈值…准备启动自毁程序…", - "你相信自己的铜头铁臂可以击败一切,却不知眼前的岩浆能轻易融化所有金属。", - }; - // 热源公开提示 - static constexpr const array heat_active_hints = { - "你被滚滚热浪淹没了...周围的一切都在高温中扭曲变形,只剩下火焰的深渊。", // 铁蛋 - "哦不!你落入了巨人萝卜的火锅池里,这下你只好成为萝卜的夜宵了。", // 纤光 - "你失败了!\nSteve试图在岩浆中游泳。", - }; - // 逃生舱提示 - static constexpr const array exit_hints = { - "你坐进了逃生舱,在启动的轰鸣声中,你想起了那句话:“不要忘了,这个世界穿透一切高墙的东西,它就在我们的内心深处,他们无法达到,也接触不到,那就是希望。”", // 大萝卜姬 - "躺在逃生舱内,平日并不虔诚的你颤巍巍地画着十字,双手合十,嘴里念念有词。诸如什么真主阿拉耶稣基督释迦牟利急急如律令之类。前窗仿佛响应了你的号召,一阵白色闪光迅速笼罩了你。正当你诧异得到了哪位神仙的庇佑时,眼前浮现出两个大字。一个振奋人心的声音在你耳边响起:“原神,启动!”", - "你忘记躺了多久,你只记得这里很温暖、舒适、令人安心。直到一股力量把你从舱内抽离;强光穿透了你稚嫩的眼皮;你哭了,你向世界宣告着你的降临。也许你只是此刻降生的其中之一,但在她眼里,你就是她的唯一。", - "是的。夜色再浓,也挡不住黎明的到来,就像再大的困难也挡不住我们的前进。黑暗即将过去,曙光就在前头!", - "你出生后 时间就已所剩无几\n在妈妈离世之后 我不知对你倾注了多少的爱呢\n但是你的微笑让爸爸备受鼓舞呀(^_^)\n其实要是能一起走就好了 但没能做到\n希望你能忘记一切继续前行 你一定可以做到的", // 纤光 - "当逃生舱门缓缓关闭,伴随着沉闷的启动声,黑暗迷宫逐渐远去。依靠在逃生舱内冷冽的仪表光芒中,你仿佛听见遥远星辰的低语:“未来,总为勇者留下一缕希望。”", // 铁蛋 - "舱门缓缓关闭,逃生舱的指示灯一一亮起,冰冷的金属包裹着你,但比起外面的黑暗,这里却意外地令人安心。你知道,一切都已经结束,或许,也是一切的开始。", - "你说的对,但是《漫漫长夜》是由大萝卜姬自主研发的一款全新大逃杀游戏。游戏发生在一个被称作“黑暗迷宫”的地图,在这里,被铁蛋选中的人将被授予“树丛”,导引“沙沙”之力。你将扮演一位名为“狩猎者”的神秘角色,在自由的旅行中邂逅性格各异、能力独特的墙壁,和它们一起阻拦对手,找到隐匿的“逃生者”——同时,逐步发掘“逃生舱”的真相。", // 三月七 - "进入逃生舱后,随着几下逐渐变弱的震动,周围的环境随之稳定下来。也就在这时,你眼前闪过一道白光,似乎是这使得你进入了一个全新的环境,伴随着的还有来自外部的一阵欢呼声:“太好了!成功抓住宝可梦了!”", // faust - "一阵失重后,舱门终于打开。随着刺眼的白光,在指缝间你看见几个面目可憎的巨人在围观你。很快地你被巨大的餐叉粗暴地刺穿;顾不及对痛觉反应,你便殒命在血盘大口之中。健硕、坚定、智慧、乐观,这些优秀的品质在他们嘴里同样珍贵。", // 大萝卜姬 - "当逃生舱的舱门关闭时,你才发现手中的钥匙根本不属于这里。系统提示音冰冷地重复着:身份验证失败。原来从一开始,你就只是这个迷宫的装饰品而已。", // 月影 - "逃生舱启动的瞬间,你突然想起那个古老的预言:'逃出迷宫的人将获得永生,但代价是永远孤独'。舱体剧烈震动起来,不知是故障还是某种警告...", - "舱内显示屏突然亮起:'恭喜您成为第1024位逃生者!作为奖励,系统将向您展示迷宫的真相...'画面切换的瞬间,你看到了无数个一模一样的逃生舱,里面坐着无数个一模一样的你。", - "当逃生舱启动时,你突然明白:黑暗不是终点,而是黎明前的温柔。舱内温度逐渐升高,那不是故障,而是新生的心跳。", - "逃生舱启动的轰鸣声渐渐平息,取而代之的是轻柔的摇篮曲。透过舷窗,你看见繁星组成的银河缓缓流动——原来迷宫的出口,一直连接着整片宇宙。", - "当舱门完全关闭的瞬间,你听见系统轻声说:'恭喜,这是第1024次模拟。根据数据,你这次终于选择相信自己了。' 周围突然亮起温暖的阳光,原来真正的逃生舱,一直都在你心里。", - "逃生舱的显示屏突然亮起一行字:'记住,黑暗只是光明的候车室。' 随着这句话,整个舱体开始散发出柔和的金色光芒,照亮了通往新世界的道路。", - }; - // 捕捉提示 - static constexpr const array catch_hints = { - "星光黯淡,你们的相遇,是命中注定,亦是命终注定。", // 纤光 - "你化作一道黑影,在血月之下,无情地终结了又一条生命。", - "你想触碰一切的真相,但在对方空洞无神的双眼中,你没能找到答案。", - "你叹了口气,在黑暗森林里,你不得不这样做。", - "我说我杀人不眨眼,你问我眼睛干不干?永别了", // 克里斯丁 - "感谢你为了我自愿放弃逃生资格", - }; - // 同格树丛声响提示 - static constexpr const array grass_sound_hints = { - "你听见有人进入了你的小树丛,沙沙声很近很近;一股接一股热气扑向了你的耳朵;呼…哈……呼…哈……他好像很累的样子。积极地想,他也许没有察觉到你", // 大萝卜姬 - "你听见有人进入了你所在的树丛,他从旁边匆匆走过,没有发现你。阴暗的想法在你心里成长起来,是让他帮你探路,还是直接干掉。甚至运气好的话,抢在前面牛了他的逃生舱……(额外探索分只有1分并逃生一事在漫漫长夜中亦有记载)", // Hyacinth - }; - // 同格啪啪声响提示 - static constexpr const array papa_sound_hints = { - "“啪!”你汗毛直立,有人来了。。幸好,你并没有站在中间,多疑多虑的性格给了你久违的回报。你小心翼翼地蹲了下来,尽力减少接触概率。只是黑暗中你低估了脚下液体的深度。“等他走远,再把内裤脱了吧。。”你暗暗地想。", // 大萝卜姬 - "啪!啪!看来是有人来了。两个人,狭小的隔间,不间断地啪啪声……'淫秽的人!'你的脑海回想起了她的声音。是啊。我承认,我确实有点想她了。", - }; - // 无逃生舱最后生还 - static constexpr const array withoutE_win_hints = { - "我睁开了双眼,眼前的一切既熟悉又陌生。看来这次终于是我赢了。我用力地端详着周遭的一切,试图捕捉错过的几日时光的任何蛛丝马迹。“我真希望他们彻底离开了 ......”说完,我在床脚拿起了本该在枕边的剃须刀;“看来上次赢的是萝卜。”我下意识地抹了抹嘴唇。在指尖晕开的口红证实了我的猜测。我笑了。", // 大萝卜姬 - "你醒啦?现在已经是第二天了哦。\n明媚的阳光照进迷宫,耳旁传来小鸟的叫声,一切美好的不太真实,唯有眼前冰冷的血迹,无声的诉说着昨晚的那场噩梦,而有些人,永远留在了那场梦中。\n可你,真的从中逃出来了吗?\n“地形参数设置完毕,新的循环正在重启……”", // 纤光 - }; - // 有逃生舱但死斗取胜 - static constexpr const array withE_win_hints = { - "“已经没事啦~”她温柔地从背后把你抱住,轻轻抚摸着头。“你很勇敢,这一步太不容易了。”你转过身,紧紧埋进她柔软的身体里放肆哭泣。“一切都结束了。不用再害怕了。”她低下头凑近你的耳边,“咱们回家叭…”", // 大萝卜姬 - "多年以后,在家族背景下你在事业上取得了巨大成就。大家把你的性情大变归功于当年失踪逃生的经历。“之前那个玩世不恭的我已经死了。”你每次都这样认真回答大家。至于细节的提问嘛,失忆这个理由你很喜欢。", - }; - - static string RandomHint(std::span hints) - { - return string(hints[rand() % hints.size()]); - } + const int k_map_num = 12; + const int k_exit_num = 4; vector> pos = { {0, 0}, {0, 3}, {0, 6}, @@ -163,107 +13,147 @@ class UnitMaps { struct Map { vector> block; - GridType type; string id; + string title; + GridType grid; + AttachType attach; + bool is_exit = false; + bool is_special = false; + + Map(vector> b, string i, string t, GridType g, AttachType a = AttachType::EMPTY, bool s = false) + : block(std::move(b)), id(std::move(i)), title(std::move(t)), grid(g), attach(a), is_special(s) {} + Map(vector> b, string i, string t, bool e, GridType g, AttachType a = AttachType::EMPTY) + : block(std::move(b)), id(std::move(i)), title(std::move(t)), is_exit(e), grid(g), attach(a) {} }; - const int k_map_num = 12; - const int k_exit_num = 4; - std::mt19937 g; vector all_maps = { - {Map1(), GridType::WATER, "1"}, - {Map2(), GridType::PORTAL, "2"}, - {Map3(), GridType::GRASS, "3"}, - {Map4(), GridType::GRASS, "4"}, - {Map5(), GridType::WATER, "5"}, - {Map6(), GridType::PORTAL, "6"}, - {Map7(), GridType::GRASS, "7"}, - {Map8(), GridType::GRASS, "8"}, - {Map9(), GridType::EMPTY, "9"}, - {Map10(), GridType::EMPTY, "10"}, - {Map11(), GridType::GRASS, "11"}, - {Map12(), GridType::GRASS, "12"}, - {Map13(), GridType::PORTAL, "13"}, - {Map14(), GridType::PORTAL, "14"}, - {Map15(), GridType::PORTAL, "15"}, - {Map16(), GridType::PORTAL, "16"}, - {Map17(), GridType::WATER, "17"}, - {Map18(), GridType::WATER, "18"}, - {Map19(), GridType::EMPTY, "19"}, - {Map20(), GridType::EMPTY, "20"}, - {Map21(), GridType::EMPTY, "21"}, - {Map22(), GridType::EMPTY, "22"}, - {Map23(), GridType::TRAP, "23"}, - {Map24(), GridType::TRAP, "24"}, - {Map25(), GridType::HEAT, "25"}, - {Map26(), GridType::BOX, "26"}, - {Map27(), GridType::BOX, "27"}, - {Map28(), GridType::PORTAL, "28"}, - {Map29(), GridType::WATER, "29"}, - {Map30(), GridType::WATER, "30"}, - // {Map31(), GridType::PORTAL, "31"}, - // {Map32(), GridType::PORTAL, "32"}, - {Map33(), GridType::TRAP, "33"}, - {Map34(), GridType::BUTTON, "34"}, + {Map1(), "1", "水廊", GridType::WATER}, + {Map2(), "2", "表里空间A", GridType::PORTAL}, + {Map3(), "3", "旋转楼道A", GridType::GRASS}, + {Map4(), "4", "缠绕走廊A", GridType::GRASS}, + {Map5(), "5", "水花大厅", GridType::WATER}, + {Map6(), "6", "表里空间B", GridType::PORTAL}, + {Map7(), "7", "旋转楼道B", GridType::GRASS}, + {Map8(), "8", "缠绕走廊B", GridType::GRASS}, + {Map9(), "9", "藏书阁A", GridType::EMPTY}, + {Map10(), "10", "藏书阁B", GridType::EMPTY}, + {Map11(), "11", "横廊", GridType::GRASS}, + {Map12(), "12", "竖廊", GridType::GRASS}, + {Map13(), "13", "镜面迷宫A", GridType::PORTAL}, + {Map14(), "14", "镜面迷宫B", GridType::PORTAL}, + {Map15(), "15", "伪装亚空间", GridType::PORTAL}, + {Map16(), "16", "隐藏房间", GridType::PORTAL}, + {Map17(), "17", "植物园A", GridType::WATER}, + {Map18(), "18", "植物园B", GridType::WATER}, + {Map19(), "19", "换鞋区A", GridType::EMPTY}, + {Map20(), "20", "换鞋区B", GridType::EMPTY}, + {Map21(), "21", "转角A", GridType::EMPTY}, + {Map22(), "22", "转角B", GridType::EMPTY}, + {Map23(), "23", "捕鸟陷阱A", GridType::TRAP}, + {Map24(), "24", "捕鸟陷阱B", GridType::TRAP}, + {Map25(), "25", "岩浆井A", GridType::HEAT}, + {Map26(), "26", "岩浆井B", GridType::HEAT}, + {Map27(), "27", "仓库A", GridType::EMPTY, AttachType::BOX}, + {Map28(), "28", "仓库B", GridType::EMPTY, AttachType::BOX}, + {Map29(), "29", "湖边A", GridType::WATER}, + {Map30(), "30", "湖边B", GridType::WATER}, + {Map31(), "31", "捕兽陷阱", GridType::TRAP}, + {Map32(), "32", "空间裂隙", GridType::PORTAL}, + {Map33(), "33", "长廊", GridType::EMPTY, AttachType::BUTTON}, + {Map34(), "34", "忏悔室", GridType::EMPTY, AttachType::BUTTON}, + {Map35(), "35", "旋转门A", GridType::EMPTY, AttachType::BUTTON}, + {Map36(), "36", "旋转门B", GridType::EMPTY, AttachType::BUTTON}, + {Map37(), "37", "会客厅", GridType::GRASS, AttachType::BUTTON}, + {Map38(), "38", "公共厕所", GridType::WATER, AttachType::BUTTON}, + {Map39(), "39", "卫生间A", GridType::TRAP, AttachType::BUTTON}, + {Map40(), "40", "卫生间B", GridType::TRAP, AttachType::BUTTON}, + {Map41(), "41", "红石迷宫A", GridType::GRASS, AttachType::BUTTON}, + {Map42(), "42", "红石迷宫B", GridType::GRASS, AttachType::BUTTON}, + // {Map31(), "31", "未命名", GridType::PORTAL}, + // {Map32(), "32", "未命名", GridType::PORTAL}, }; vector all_exits = { - {Exit1(), GridType::EXIT, "1"}, - {Exit2(), GridType::EXIT, "2"}, - {Exit3(), GridType::EXIT, "3"}, - {Exit4(), GridType::EXIT, "4"}, - {Exit5(), GridType::EXIT, "5"}, - {Exit6(), GridType::EXIT, "6"}, - {Exit7(), GridType::EXIT, "7"}, - {Exit8(), GridType::EXIT, "8"}, - {Exit9(), GridType::EXIT, "9"}, - {Exit10(), GridType::EXIT, "10"}, + {Exit1(), "1", "逃生长廊A", true, GridType::EMPTY}, + {Exit2(), "2", "逃生长廊B", true, GridType::EMPTY}, + {Exit3(), "3", "逃生长廊C", true, GridType::EMPTY}, + {Exit4(), "4", "逃生长廊D", true, GridType::EMPTY}, + {Exit5(), "5", "快速逃生通道A", true, GridType::PORTAL}, + {Exit6(), "6", "快速逃生通道B", true, GridType::PORTAL}, + {Exit7(), "7", "亚空间逃生舱A", true, GridType::PORTAL}, + {Exit8(), "8", "亚空间逃生舱B", true, GridType::PORTAL}, + {Exit9(), "9", "逃生地道A", true, GridType::TRAP}, + {Exit10(), "10", "逃生地道B", true, GridType::TRAP}, + {Exit11(), "11", "隐蔽逃生通道A", true, GridType::EMPTY, AttachType::BUTTON}, + {Exit12(), "12", "隐蔽逃生通道B", true, GridType::EMPTY, AttachType::BUTTON}, }; vector special_maps = { - {SMap1(), GridType::SPECIAL, "S1"}, - {SMap2(), GridType::SPECIAL, "S2"}, - {SMap3(), GridType::SPECIAL, "S3"}, - {SMap4(), GridType::SPECIAL, "S4"}, + {SMap1(), "S1", "实验场", GridType::HEAT, AttachType::EMPTY, true}, + {SMap2(), "S2", "原子空间", GridType::PORTAL, AttachType::EMPTY, true}, + {SMap3(), "S3", "原子阱", GridType::PORTAL, AttachType::EMPTY, true}, + {SMap4(), "S4", "黑洞", GridType::PORTAL, AttachType::EMPTY, true}, }; - const vector rotation_maps_id = { - "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", - "21", "22", "23", "24", "26", - "29", "30", "33", "34" + const vector twist_maps_id = { + "2", "3", "4", "6", "7", "8", "11", "12", + "17", "18", "21", "22", "23", "24", "25", "26", + "37", "38", "39", "40", }; - const vector rotation_exits_id = {"1", "2", "3", "4"}; - vector rotation_maps; - vector rotation_exits; + const vector twist_exits_id = { + "1", "2", "3", "4", + "9", "10" + }; + vector twist_maps; + vector twist_exits; vector maps; vector exits; - UnitMaps(const int32_t mode) + UnitMaps() = default; + + UnitMaps(const int32_t mode, std::mt19937& gen, const vector& custom_blocks): g(std::make_unique(gen)) { - std::random_device rd; - g = std::mt19937(rd()); if (mode == 0) { + // 标准 maps.insert(maps.end(), all_maps.begin(), all_maps.begin() + k_map_num); exits.insert(exits.end(), all_exits.begin(), all_exits.begin() + k_exit_num); } else if (mode == 1) { - std::sample(all_maps.begin(), all_maps.end(), std::back_inserter(maps), k_map_num, g); + // 狂野 + std::sample(all_maps.begin(), all_maps.end(), std::back_inserter(maps), k_map_num, *g); SampleExits(all_exits, k_exit_num / 2); } else if (mode == 2) { - for (const auto &m : all_maps) { - if (std::find(rotation_maps_id.begin(), rotation_maps_id.end(), m.id) != rotation_maps_id.end()) { - rotation_maps.push_back(m); + // 幻变 + for (const auto& m : all_maps) { + if (std::find(twist_maps_id.begin(), twist_maps_id.end(), m.id) != twist_maps_id.end()) { + twist_maps.push_back(m); } } - for (const auto &m : all_exits) { - if (std::find(rotation_exits_id.begin(), rotation_exits_id.end(), m.id) != rotation_exits_id.end()) { - rotation_exits.push_back(m); + for (const auto& m : all_exits) { + if (std::find(twist_exits_id.begin(), twist_exits_id.end(), m.id) != twist_exits_id.end()) { + twist_exits.push_back(m); } } - std::sample(rotation_maps.begin(), rotation_maps.end(), std::back_inserter(maps), k_map_num, g); - SampleExits(rotation_exits, k_exit_num / 2); - } else { - std::sample(special_maps.begin(), special_maps.end(), std::back_inserter(maps), 2, g); - std::sample(all_maps.begin(), all_maps.end(), std::back_inserter(maps), k_map_num - 2, g); + std::sample(twist_maps.begin(), twist_maps.end(), std::back_inserter(maps), k_map_num, *g); + SampleExits(twist_exits, k_exit_num / 2); + } else if (mode == 3) { + // 疯狂 + std::sample(special_maps.begin(), special_maps.end(), std::back_inserter(maps), 2, *g); + std::sample(all_maps.begin(), all_maps.end(), std::back_inserter(maps), k_map_num - 2, *g); SampleExits(all_exits, k_exit_num / 2); + } else { + // 自定义 + for (const auto& block : custom_blocks) { + if (auto it = std::find_if(special_maps.begin(), special_maps.end(), [&](const Map& m) { return m.id == block; }); + it != special_maps.end()) { + maps.push_back(*it); + } + const bool is_exit = !block.empty() && block[0] == 'E'; + string search_id = is_exit ? block.substr(1) : block; + const auto& list = is_exit ? all_exits : all_maps; + if (auto it = std::find_if(list.begin(), list.end(), [&](const Map& m) { return m.id == search_id; }); + it != list.end()) { + (is_exit ? exits : maps).push_back(*it); + } + } } origin_pos = pos; } @@ -275,7 +165,7 @@ class UnitMaps { int pair_num = exits_pool.size() / 2; vector pairs(pair_num); std::iota(pairs.begin(), pairs.end(), 0); - std::shuffle(pairs.begin(), pairs.end(), g); + std::shuffle(pairs.begin(), pairs.end(), *g); pairs.resize(k_exit_pair); std::sort(pairs.begin(), pairs.end()); for (int p : pairs) { @@ -286,14 +176,28 @@ class UnitMaps { vector> FindBlockById(const string id, const bool is_exit, const bool special = false) const { + auto special_it = std::find_if(special_maps.begin(), special_maps.end(), [id](const Map& map) { return map.id == id; }); + if (special_it != special_maps.end()) return special_it->block; + const vector& search_list = is_exit ? (special ? exits : all_exits) : (special ? maps : all_maps); auto it = std::find_if(search_list.begin(), search_list.end(), [id](const Map& map) { return map.id == id; }); if (it != search_list.end()) return it->block; - auto special_it = std::find_if(special_maps.begin(), special_maps.end(), [id](const Map& map) { return map.id == id; }); - if (special_it != special_maps.end()) return special_it->block; + return InitializeMapTemplate(); } + bool IsBlockExist(const string id, const bool is_exit) const + { + auto special_it = std::find_if(special_maps.begin(), special_maps.end(), [id](const Map& map) { return map.id == id; }); + if (special_it != special_maps.end()) return true; + + const vector& search_list = is_exit ? all_exits : all_maps; + auto it = std::find_if(search_list.begin(), search_list.end(), [id](const Map& map) { return map.id == id; }); + if (it != search_list.end()) return true; + + return false; + } + static bool MapContainGridType(const vector& maps, const GridType& type) { for (const auto& map: maps) { @@ -306,6 +210,18 @@ class UnitMaps { return false; } + static bool MapContainAttachType(const vector& maps, const AttachType& type) + { + for (const auto& map: maps) { + for (int k = 0; k < 9; ++k) { + if (map.block[k / 3][k % 3].Attach() == type) { + return true; + } + } + } + return false; + } + static bool MapContainWallType(const vector& maps, const Wall& wall) { for (const auto& map: maps) { @@ -397,7 +313,7 @@ class UnitMaps { int targetGrowth = min(grassCount, static_cast(growablePositions.size())); if (targetGrowth == 0) continue; // 无法生长则跳过 - std::shuffle(growablePositions.begin(), growablePositions.end(), g); + std::shuffle(growablePositions.begin(), growablePositions.end(), *g); for (int i = 0; i < targetGrowth; i++) { int x = growablePositions[i].first; int y = growablePositions[i].second; @@ -441,7 +357,7 @@ class UnitMaps { candidates.push_back({i, j}); } } - std::shuffle(candidates.begin(), candidates.end(), g); + std::shuffle(candidates.begin(), candidates.end(), *g); vector> chosen; bool found = backtrack(0, chosen, candidates); if (found) { @@ -476,6 +392,8 @@ class UnitMaps { } private: + std::unique_ptr g; + void SetBlock(const int x, const int y, vector>& grid_map, const vector> block) const { for (int i = 0; i < block.size(); i++) { @@ -526,6 +444,7 @@ class UnitMaps { return map; } + /* ========== 常规地图 ========== */ static vector> Map1() { auto map = InitializeMapTemplate(); @@ -1039,7 +958,8 @@ class UnitMaps { static vector> Map26() { auto map = InitializeMapTemplate(); - map[1][1].SetType(GridType::BOX); + map[0][0].SetType(GridType::HEAT); + map[2][2].SetType(GridType::HEAT); return map; } @@ -1047,8 +967,7 @@ class UnitMaps { static vector> Map27() { auto map = InitializeMapTemplate(); - map[0][0].SetType(GridType::BOX); - map[2][2].SetType(GridType::BOX); + map[1][1].SetAttach(AttachType::BOX); return map; } @@ -1056,21 +975,8 @@ class UnitMaps { static vector> Map28() { auto map = InitializeMapTemplate(); - map[1][0].SetType(GridType::PORTAL).SetPortal(0, 2); - map[1][2].SetType(GridType::PORTAL).SetPortal(0, -2); - map[1][1].SetType(GridType::TRAP); - - map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - - map[1][0].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); - map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); - map[1][2].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); - - map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][0].SetAttach(AttachType::BOX); + map[2][2].SetAttach(AttachType::BOX); return map; } @@ -1083,15 +989,15 @@ class UnitMaps { map[2][2].SetType(GridType::WATER); map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); - map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); map[1][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); - map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); map[1][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); - map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); return map; @@ -1108,9 +1014,9 @@ class UnitMaps { map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); - map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); - map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); - map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); @@ -1122,21 +1028,19 @@ class UnitMaps { static vector> Map31() { auto map = InitializeMapTemplate(); - map[0][2].SetType(GridType::ONEWAYPORTAL).SetPortal(2, -2); - map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2); - map[1][1].SetType(GridType::GRASS); + map[1][1].SetType(GridType::TRAP); - map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); - map[0][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); - map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); - map[2][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); - map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); - map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); return map; } @@ -1144,21 +1048,21 @@ class UnitMaps { static vector> Map32() { auto map = InitializeMapTemplate(); - map[0][0].SetType(GridType::ONEWAYPORTAL).SetPortal(2, 2); - map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2); - map[1][1].SetType(GridType::GRASS); + map[1][0].SetType(GridType::PORTAL).SetPortal(0, 2); + map[1][2].SetType(GridType::PORTAL).SetPortal(0, -2); + map[1][1].SetType(GridType::TRAP); - map[0][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); - map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - map[1][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[1][0].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); - map[1][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + map[1][2].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); - map[2][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); return map; } @@ -1166,19 +1070,20 @@ class UnitMaps { static vector> Map33() { auto map = InitializeMapTemplate(); - map[1][1].SetType(GridType::TRAP); + map[0][2].SetAttach(AttachType::BUTTON).SetButton({{0, -2, Direct::DOWN}}).SetContent("A"); + map[2][0].SetAttach(AttachType::BUTTON).SetButton({{0, 2, Direct::UP}}).SetContent("B"); - map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[0][0].SetWall(Wall::EMPTY, Wall::DOOR, Wall::EMPTY, Wall::EMPTY).SetWallContent({"", "A", "", ""}); + map[0][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); - map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); - map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); - map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][0].SetWall(Wall::DOOR, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::NORMAL, Wall::DOOR, Wall::EMPTY, Wall::EMPTY); - map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); - map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::DOOR, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetWallContent({"B", "", "", ""}); return map; } @@ -1186,25 +1091,221 @@ class UnitMaps { static vector> Map34() { auto map = InitializeMapTemplate(); - map[0][2].SetType(GridType::BUTTON).SetButton(0, -2, Direct::DOWN).SetContent("A"); - map[2][0].SetType(GridType::BUTTON).SetButton(0, 2, Direct::UP).SetContent("B"); + map[0][1].SetAttach(AttachType::BUTTON).SetButton({{0, 0, Direct::DOWN}}).SetContent("A"); + map[1][0].SetAttach(AttachType::BUTTON).SetButton({{0, 0, Direct::RIGHT}}).SetContent("D"); + map[1][2].SetAttach(AttachType::BUTTON).SetButton({{0, 0, Direct::LEFT}}).SetContent("B"); + map[2][1].SetAttach(AttachType::BUTTON).SetButton({{0, 0, Direct::UP}}).SetContent("C"); - map[0][0].SetWall(Wall::EMPTY, Wall::DOOR, Wall::EMPTY, Wall::EMPTY).SetWallContent({"", "A", "", ""}); - map[0][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); - map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::DOOR, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); - map[1][0].SetWall(Wall::DOOR, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[1][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::DOOR); + map[1][1].SetWall(Wall::DOOR, Wall::DOOR, Wall::DOOR, Wall::DOOR).SetWallContent({"A", "C", "D", "B"}); + map[1][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOR, Wall::NORMAL); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::DOOR, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Map35() + { + auto map = InitializeMapTemplate(); + map[0][2].SetAttach(AttachType::BUTTON).SetButton({{0, -1, Direct::DOWN}, {1, -1, Direct::DOWN}}).SetContent("A"); + map[2][0].SetAttach(AttachType::BUTTON).SetButton({{-1, 0, Direct::RIGHT}, {-1, 1, Direct::RIGHT}}).SetContent("B"); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::EMPTY, Wall::DOOR, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::DOOR); + map[1][1].SetWall(Wall::DOOR, Wall::DOOR, Wall::DOOR, Wall::DOOR).SetWallContent({"A", "A", "B", "B"}); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::DOOR, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::DOOR, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Map36() + { + auto map = InitializeMapTemplate(); + map[0][2].SetAttach(AttachType::BUTTON).SetButton({{0, -1, Direct::DOWN}, {1, 0, Direct::LEFT}}).SetContent("A"); + map[2][0].SetAttach(AttachType::BUTTON).SetButton({{-1, 0, Direct::RIGHT}, {0, 1, Direct::UP}}).SetContent("B"); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::EMPTY, Wall::DOOR, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::DOOROPEN); + map[1][1].SetWall(Wall::DOOR, Wall::DOOR, Wall::DOOROPEN, Wall::DOOROPEN).SetWallContent({"A", "B", "B", "A"}); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::DOOROPEN, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::DOOR, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Map37() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::GRASS).SetAttach(AttachType::BUTTON).SetButton({ + {-1, 0, Direct::UP}, + { 0, -1, Direct::UP}, { 0, -1, Direct::DOWN}, + { 0, 1, Direct::UP}, { 0, 1, Direct::DOWN}, + { 1, 0, Direct::DOWN}, + }); + + map[0][0].SetWall(Wall::EMPTY, Wall::DOOROPEN, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::DOOR, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::DOOR, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::DOOROPEN, Wall::DOOR, Wall::NORMAL, Wall::EMPTY); map[1][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); - map[1][2].SetWall(Wall::NORMAL, Wall::DOOR, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::DOOR, Wall::DOOROPEN, Wall::EMPTY, Wall::NORMAL); - map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); - map[2][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); - map[2][2].SetWall(Wall::DOOR, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetWallContent({"B", "", "", ""}); + map[2][0].SetWall(Wall::DOOR, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::DOOROPEN, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::DOOROPEN, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Map38() + { + auto map = InitializeMapTemplate(); + map[0][0].SetType(GridType::WATER); + map[0][2].SetType(GridType::WATER); + map[1][1].SetType(GridType::GRASS).SetAttach(AttachType::BUTTON).SetButton({ + {-1, -1, Direct::DOWN}, {-1, -1, Direct::RIGHT}, + {-1, 1, Direct::DOWN}, {-1, 1, Direct::LEFT}, + { 1, -1, Direct::UP}, { 1, -1, Direct::RIGHT}, + { 1, 1, Direct::UP}, { 1, 1, Direct::LEFT}, + }); + map[2][0].SetType(GridType::WATER); + map[2][2].SetType(GridType::WATER); + + map[0][0].SetWall(Wall::NORMAL, Wall::DOOR, Wall::EMPTY, Wall::DOOROPEN); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOROPEN, Wall::DOOR); + map[0][2].SetWall(Wall::NORMAL, Wall::DOOROPEN, Wall::DOOR, Wall::EMPTY); + + map[1][0].SetWall(Wall::DOOR, Wall::DOOROPEN, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::DOOROPEN, Wall::DOOR, Wall::EMPTY, Wall::EMPTY); + + map[2][0].SetWall(Wall::DOOROPEN, Wall::NORMAL, Wall::EMPTY, Wall::DOOR); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOR, Wall::DOOROPEN); + map[2][2].SetWall(Wall::DOOR, Wall::NORMAL, Wall::DOOROPEN, Wall::EMPTY); + + return map; + } + + static vector> Map39() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::TRAP).SetAttach(AttachType::BUTTON).SetButton({ + {-1, 1, Direct::DOWN}, {-1, 1, Direct::LEFT}, + { 0, 0, Direct::UP}, { 0, 0, Direct::DOWN}, { 0, 0, Direct::LEFT}, { 0, 0, Direct::RIGHT}, + { 1, -1, Direct::UP}, { 1, -1, Direct::RIGHT}, + }); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::EMPTY, Wall::DOOR, Wall::EMPTY, Wall::DOOROPEN); + map[0][2].SetWall(Wall::NORMAL, Wall::DOOR, Wall::DOOROPEN, Wall::NORMAL); + + map[1][0].SetWall(Wall::EMPTY, Wall::DOOROPEN, Wall::EMPTY, Wall::DOOR); + map[1][1].SetWall(Wall::DOOR, Wall::DOOROPEN, Wall::DOOR, Wall::DOOROPEN); + map[1][2].SetWall(Wall::DOOR, Wall::EMPTY, Wall::DOOROPEN, Wall::EMPTY); + + map[2][0].SetWall(Wall::DOOROPEN, Wall::NORMAL, Wall::NORMAL, Wall::DOOR); + map[2][1].SetWall(Wall::DOOROPEN, Wall::EMPTY, Wall::DOOR, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Map40() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::TRAP).SetAttach(AttachType::BUTTON).SetButton({ + {-1, 1, Direct::DOWN}, {-1, 1, Direct::LEFT}, + { 0, 0, Direct::UP}, { 0, 0, Direct::DOWN}, { 0, 0, Direct::LEFT}, { 0, 0, Direct::RIGHT}, + { 1, -1, Direct::UP}, { 1, -1, Direct::RIGHT}, + }); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::EMPTY, Wall::DOOROPEN, Wall::EMPTY, Wall::DOOR); + map[0][2].SetWall(Wall::NORMAL, Wall::DOOR, Wall::DOOR, Wall::NORMAL); + + map[1][0].SetWall(Wall::EMPTY, Wall::DOOROPEN, Wall::EMPTY, Wall::DOOR); + map[1][1].SetWall(Wall::DOOROPEN, Wall::DOOR, Wall::DOOR, Wall::DOOROPEN); + map[1][2].SetWall(Wall::DOOR, Wall::EMPTY, Wall::DOOROPEN, Wall::EMPTY); + + map[2][0].SetWall(Wall::DOOROPEN, Wall::NORMAL, Wall::NORMAL, Wall::DOOROPEN); + map[2][1].SetWall(Wall::DOOR, Wall::EMPTY, Wall::DOOROPEN, Wall::EMPTY); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + static vector> Map41() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::GRASS).SetAttach(AttachType::BUTTON).SetButton({ + {-1, -1, Direct::UP}, {-1, -1, Direct::DOWN}, {-1, -1, Direct::LEFT}, {-1, -1, Direct::RIGHT}, + {-1, 1, Direct::UP}, {-1, 1, Direct::DOWN}, {-1, 1, Direct::LEFT}, {-1, 1, Direct::RIGHT}, + { 1, -1, Direct::UP}, { 1, -1, Direct::DOWN}, { 1, -1, Direct::LEFT}, { 1, -1, Direct::RIGHT}, + { 1, 1, Direct::UP}, { 1, 1, Direct::DOWN}, { 1, 1, Direct::LEFT}, { 1, 1, Direct::RIGHT}, + }); + + map[0][0].SetWall(Wall::DOOROPEN, Wall::DOOR, Wall::DOOR, Wall::DOOROPEN); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOROPEN, Wall::DOOR); + map[0][2].SetWall(Wall::DOOR, Wall::DOOROPEN, Wall::DOOR, Wall::DOOROPEN); + + map[1][0].SetWall(Wall::DOOR, Wall::DOOROPEN, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::DOOROPEN, Wall::DOOR, Wall::EMPTY, Wall::EMPTY); + + map[2][0].SetWall(Wall::DOOROPEN, Wall::DOOR, Wall::DOOROPEN, Wall::DOOR); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOR, Wall::DOOROPEN); + map[2][2].SetWall(Wall::DOOR, Wall::DOOROPEN, Wall::DOOROPEN, Wall::DOOR); + + return map; + } + + static vector> Map42() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::GRASS).SetAttach(AttachType::BUTTON).SetButton({ + {-1, -1, Direct::UP}, {-1, -1, Direct::DOWN}, {-1, -1, Direct::LEFT}, {-1, -1, Direct::RIGHT}, + {-1, 1, Direct::UP}, {-1, 1, Direct::DOWN}, {-1, 1, Direct::LEFT}, {-1, 1, Direct::RIGHT}, + { 1, -1, Direct::UP}, { 1, -1, Direct::DOWN}, { 1, -1, Direct::LEFT}, { 1, -1, Direct::RIGHT}, + { 1, 1, Direct::UP}, { 1, 1, Direct::DOWN}, { 1, 1, Direct::LEFT}, { 1, 1, Direct::RIGHT}, + }); + + map[0][0].SetWall(Wall::DOOR, Wall::DOOR, Wall::DOOROPEN, Wall::DOOROPEN); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOROPEN, Wall::DOOROPEN); + map[0][2].SetWall(Wall::DOOR, Wall::DOOR, Wall::DOOROPEN, Wall::DOOROPEN); + + map[1][0].SetWall(Wall::DOOR, Wall::DOOR, Wall::EMPTY, Wall::EMPTY); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::DOOR, Wall::DOOR, Wall::EMPTY, Wall::EMPTY); + + map[2][0].SetWall(Wall::DOOR, Wall::DOOR, Wall::DOOROPEN, Wall::DOOROPEN); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOROPEN, Wall::DOOROPEN); + map[2][2].SetWall(Wall::DOOR, Wall::DOOR, Wall::DOOROPEN, Wall::DOOROPEN); return map; } - // static vector> Map35() + // static vector> Map43() // { // auto map = InitializeMapTemplate(); // map[0][0].SetType(GridType::WATER); @@ -1227,6 +1328,52 @@ class UnitMaps { // return map; // } + // 旧版31、32(暂时废弃) + // static vector> Map31() + // { + // auto map = InitializeMapTemplate(); + // map[0][2].SetType(GridType::ONEWAYPORTAL).SetPortal(2, -2); + // map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2); + // map[1][1].SetType(GridType::GRASS); + + // map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + // map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + // map[0][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + // map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + // map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + // map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + // map[2][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + // map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + // map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + // return map; + // } + + // static vector> Map32() + // { + // auto map = InitializeMapTemplate(); + // map[0][0].SetType(GridType::ONEWAYPORTAL).SetPortal(2, 2); + // map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2); + // map[1][1].SetType(GridType::GRASS); + + // map[0][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + // map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY).SetGrowable(true); + // map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + // map[1][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + // map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + // map[1][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + // map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + // map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); + // map[2][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + // return map; + // } + + /* ========== 逃生舱 ========== */ static vector> Exit1() { auto map = InitializeMapTemplate(); @@ -1449,7 +1596,49 @@ class UnitMaps { return map; } - // 特殊地图 + static vector> Exit11() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::EXIT); + map[1][2].SetAttach(AttachType::BUTTON).SetButton({{1, -1, Direct::LEFT}}); + + map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[0][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + + map[1][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::DOOR); + map[2][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::DOOR, Wall::NORMAL); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::EMPTY); + + return map; + } + + static vector> Exit12() + { + auto map = InitializeMapTemplate(); + map[1][1].SetType(GridType::EXIT); + map[1][0].SetAttach(AttachType::BUTTON).SetButton({{-1, 1, Direct::RIGHT}}); + + map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL); + map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::NORMAL, Wall::DOOR); + map[0][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOR, Wall::EMPTY); + + map[1][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); + map[1][1].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::NORMAL); + map[1][2].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::NORMAL, Wall::EMPTY); + + map[2][0].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[2][1].SetWall(Wall::NORMAL, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); + map[2][2].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + + return map; + } + + /* ========== 特殊地图 ========== */ static vector> SMap1() { auto map = InitializeMapTemplate(); @@ -1481,11 +1670,11 @@ class UnitMaps { static vector> SMap2() { auto map = InitializeMapTemplate(); - map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2).SetContent("A", "#FFF8E7"); - map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2).SetContent("B", "#FFF8E7"); + map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2).SetContent("A"); + map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2).SetContent("B"); map[1][1].SetType(GridType::WATER); - map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2).SetContent("B", "#FFF8E7"); - map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2).SetContent("A", "#FFF8E7"); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2).SetContent("B"); + map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2).SetContent("A"); map[0][0].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); map[0][1].SetWall(Wall::NORMAL, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); @@ -1505,11 +1694,11 @@ class UnitMaps { static vector> SMap3() { auto map = InitializeMapTemplate(); - map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2).SetContent("A", "#FFF8E7"); - map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2).SetContent("B", "#FFF8E7"); + map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2).SetContent("A"); + map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2).SetContent("B"); map[1][1].SetType(GridType::TRAP); - map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2).SetContent("B", "#FFF8E7"); - map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2).SetContent("A", "#FFF8E7"); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2).SetContent("B"); + map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2).SetContent("A"); map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::EMPTY); map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::NORMAL).SetGrowable(true); @@ -1529,15 +1718,15 @@ class UnitMaps { static vector> SMap4() { auto map = InitializeMapTemplate(); - map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2).SetContent("A", "#FFF8E7"); - map[0][1].SetType(GridType::PORTAL).SetPortal(2, 0).SetContent("B", "#FFF8E7"); - map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2).SetContent("C", "#FFF8E7"); - map[1][0].SetType(GridType::PORTAL).SetPortal(0, 2).SetContent("D", "#FFF8E7"); - map[1][1].SetType(GridType::PORTAL).SetPortal(0, 0).SetContent("E(E)", "#FFF8E7"); - map[1][2].SetType(GridType::PORTAL).SetPortal(0, -2).SetContent("D", "#FFF8E7"); - map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2).SetContent("C", "#FFF8E7"); - map[2][1].SetType(GridType::PORTAL).SetPortal(-2, 0).SetContent("B", "#FFF8E7"); - map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2).SetContent("A", "#FFF8E7"); + map[0][0].SetType(GridType::PORTAL).SetPortal(2, 2).SetContent("A"); + map[0][1].SetType(GridType::PORTAL).SetPortal(2, 0).SetContent("B"); + map[0][2].SetType(GridType::PORTAL).SetPortal(2, -2).SetContent("C"); + map[1][0].SetType(GridType::PORTAL).SetPortal(0, 2).SetContent("D"); + map[1][1].SetType(GridType::PORTAL).SetPortal(0, 0).SetContent("E(E)"); + map[1][2].SetType(GridType::PORTAL).SetPortal(0, -2).SetContent("D"); + map[2][0].SetType(GridType::PORTAL).SetPortal(-2, 2).SetContent("C"); + map[2][1].SetType(GridType::PORTAL).SetPortal(-2, 0).SetContent("B"); + map[2][2].SetType(GridType::PORTAL).SetPortal(-2, -2).SetContent("A"); map[0][0].SetWall(Wall::EMPTY, Wall::NORMAL, Wall::EMPTY, Wall::NORMAL); map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::NORMAL, Wall::NORMAL); diff --git a/games/long_night/mygame.cc b/games/long_night/mygame.cc index 8c0f50d..de13f1d 100644 --- a/games/long_night/mygame.cc +++ b/games/long_night/mygame.cc @@ -6,6 +6,7 @@ #include #include #include +#include #include "game_framework/stage.h" #include "game_framework/util.h" @@ -13,8 +14,11 @@ using namespace std; +#include "constants.h" +#include "player.h" #include "grid.h" #include "map.h" +#include "boss.h" #include "board.h" namespace lgtbot { @@ -27,20 +31,124 @@ 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 + .name_ = "漫漫长夜", .developer_ = "铁蛋", .description_ = "在漆黑的迷宫中探索,根据有限的线索展开追击与逃生", .shuffled_player_id_ = true, }; uint64_t MaxPlayerNum(const CustomOptions& options) { return 8; } -uint32_t Multiple(const CustomOptions& options) { return 1; } -const MutableGenericOptions k_default_generic_options{ - .is_formal_{false}, +uint32_t Multiple(const CustomOptions& options) { + if (GET_OPTION_VALUE(options, 区块).size() != 1) + return 0; + return 1; +} +const MutableGenericOptions k_default_generic_options; + +const char* const boss_details[1] = { + R"EOF(《BOSS 规则和技能》 + 【👹米诺陶斯】 +1、随机生成在地图中,每回合最后行动,首回合锁定最近玩家为目标(公屏显示) +2、每回合移动步数递增,发现更近玩家则更换目标并重置步数。 +3、BOSS无视地形,移动结束时会发出巨响,如果玩家在其周围会听到喘息声。 +4、BOSS踩到玩家则玩家出局,玩家经过米诺陶斯不会出局。 + 【💣邦邦】 +1、所有玩家移动后,BOSS以固定速度(3-5 随机)跟着距离最近玩家移动。接触玩家后停止移动,但无法捕捉玩家;追到玩家后,会转移目标到第二近的玩家。 +2、BOSS每回合结束时下一个[炸弹],[炸弹]的四墙信息将会公开。 +3、BOSS一定每回合都移动,无视地形,转换目标将公屏显示。)EOF", +}; +const char* const rule_details[4] = { + R"EOF(《游戏规则和机制细节》 +可选参数列表,使用「#规则 漫漫长夜 <参数>」查看详情: +【地形】地形&附着&墙壁机制和特殊情况 +【传送】开局随机和随机传送机制 +【成就】成就触发判定和特殊情况)EOF", + + R"EOF(【地形&附着】 +出生在任何地形都不会触发其效果,所有效果均在移动时触发。 +【墙壁】 +相邻区块组合时,边界墙壁发生冲突则只保留优先级更高的墙壁。 +优先级:门(开) > 门 > 墙壁 > 空 +【热源相关】 +玩家出生在热浪区域,如果第1步还在热浪范围会收到提示。如果第1步进入热源,在第2步才会收到热浪提示。 +【炸弹相关】 +炸弹必须要有[进入]并[离开]两步才能引爆。炸弹下到玩家上因为没有[进入]的过程,[离开]不会引爆。同理,随机传送到炸弹上,[离开]也不会引爆。 +[拆弹]也需要[进入]并[停止行动],放置炸弹直接撞墙,因为没有[进入]的过程,不会拆弹。同理,随机传送到炸弹上,直接[停止行动]也不会拆弹。 +特殊:抓人时脚下有炸弹不会触发拆弹;进入逃生舱不会引爆/拆弹;隐匿状态一样会引爆炸弹;亚空间内下包会放置于传送门入口处)EOF", + + R"EOF(【玩家随机传送】 +传送至最大联通区域,不会传送至[逃生舱、热源周围8格]、[存活玩家周围8格]、[BOSS当前步数可到达的区域]。如果没有有效候选点,直接在最大联通区域内随机。 +【开局玩家随机】 +所有位置均在同一个最大联通区域内,按照顺序依次尝试(BFS考虑墙壁,但不考虑传送门): +- 方案1:使用非逃生舱区块,相邻玩家之间的BFS路径距离≥5;每个玩家落点到所有逃生舱的BFS路径距离≥5 +- 方案2:使用非逃生舱区块,仅要求相邻玩家之间的BFS路径距离≥5(不检查逃生舱距离) +- 方案3:直接使用非逃生舱区块随机分配(不检查距离) +- 方案4:允许使用逃生舱区块 +- 保险方案:直接从最大连通区域中随机选取位置)EOF", + + R"EOF(【成就相关】 +[乒铃乓啷]撞箱子不会计数,必须要成功推动箱子。引爆/拆弹均会计数,但是算作同一种类型。“单向传送门”和“普通传送门”视为同一种地形 +隐匿状态进入树丛等声响地形,仍可以获得[无声]相关成就。 +[守株待兔]可以直接停止也可以第1步撞墙,[谋定后动模式]走1步抓不会触发此成就。)EOF", +}; +const std::vector k_rule_commands = { + RuleCommand("查看所有 BOSS 的规则和技能", + []() { return boss_details[0]; }, + VoidChecker("BOSS")), + RuleCommand("游戏部分隐藏机制:「#规则 漫漫长夜 机制」查看可用列表帮助", + [](const int type) { return rule_details[type]; }, + AlterChecker({{"机制", 0}, {"地形", 1}, {"传送", 2}, {"成就", 3}})), }; -const std::vector k_rule_commands = {}; bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { + auto& custom_blocks = GET_OPTION_VALUE(game_options, 区块); + if (custom_blocks.empty()) { + reply() << "[错误] 区块参数为空:必须包含至少 9 个有效区块代号。形如:1 1 6 16 34 38 E1 e7 S4 s1"; + return false; + } + if (custom_blocks[0] != "默认") { + UnitMaps unitMaps; + vector vaild_blocks; + bool has_invalid = false; + for (auto& block : custom_blocks) { + bool is_valid; + std::transform(block.begin(), block.end(), block.begin(), [](unsigned char c) { return std::toupper(c); }); + if (!block.empty() && block[0] == 'E') { + is_valid = unitMaps.IsBlockExist(block.substr(1), true); + } else { + is_valid = unitMaps.IsBlockExist(block, false); + } + if (is_valid) vaild_blocks.push_back(block); else has_invalid = true; + } + if (generic_options_readonly.PlayerNum() > 6 && vaild_blocks.size() < 16) { + reply() << "[错误] 区块参数不足:当前玩家数为 " << generic_options_readonly.PlayerNum() << ",必须包含至少 16 个有效区块代号,当前数量为:" << vaild_blocks.size(); + return false; + } + if (vaild_blocks.size() < 9) { + reply() << "[错误] 区块参数不足:必须包含至少 9 个有效区块代号,当前数量为:" << vaild_blocks.size(); + return false; + } + GET_OPTION_VALUE(game_options, 模式) = -1; + std::sort(vaild_blocks.begin(), vaild_blocks.end(), [](const string& a, const string& b) { + bool a_is_digit = std::isdigit(a[0]); + bool b_is_digit = std::isdigit(b[0]); + if (a_is_digit != b_is_digit) + return !a_is_digit; + if (!a_is_digit) { + char a_letter = std::toupper(a[0]); + char b_letter = std::toupper(b[0]); + if (a_letter != b_letter) + return a_letter < b_letter; + int a_num = std::stoi(a.substr(1)); + int b_num = std::stoi(b.substr(1)); + return a_num < b_num; + } else { + return std::stoi(a) < std::stoi(b); + } + }); + custom_blocks = vaild_blocks; + } + if (GET_OPTION_VALUE(game_options, 特殊事件) == -1) { GET_OPTION_VALUE(game_options, 特殊事件) = rand() % 3 + 1; } @@ -52,42 +160,65 @@ bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const Gener } const std::vector k_init_options_commands = { - InitOptionsCommand("设定特殊事件或游戏模式", - [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const int32_t mode) + InitOptionsCommand("一键设定特殊事件或游戏模式:空格分隔,冲突配置以靠后的为准", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const vector& modes) { - switch (mode) { - case -1: - case 1: - case 2: - case 3: - GET_OPTION_VALUE(game_options, 特殊事件) = mode; break; - case 10: - case 12: - GET_OPTION_VALUE(game_options, 边长) = mode; break; - case 20: - GET_OPTION_VALUE(game_options, 大乱斗) = true; break; - case 21: - case 22: - GET_OPTION_VALUE(game_options, 隐匿) = mode - 20; break; - case 23: - GET_OPTION_VALUE(game_options, 点杀) = true; break; - case 24: - GET_OPTION_VALUE(game_options, BOSS) = true; break; - case 30: - case 31: - case 32: - case 33: - GET_OPTION_VALUE(game_options, 模式) = mode - 30; break; - case 100: - return NewGameMode::SINGLE_USER; - default:; + bool single_user = false; + for (int32_t mode : modes) { + switch (mode) { + // ===== 特殊事件 ===== + case -1: + case 1: + case 2: + case 3: + GET_OPTION_VALUE(game_options, 特殊事件) = mode; break; + // ===== 边长 ===== + case 10: + case 12: + GET_OPTION_VALUE(game_options, 边长) = mode; break; + // ===== 区块模式 ===== + case 15: + case 16: + case 17: + case 18: + GET_OPTION_VALUE(game_options, 模式) = mode - 15; break; + // ===== 游戏模式 ===== + case 20: + GET_OPTION_VALUE(game_options, 大乱斗) = true; break; + case 21: + case 22: + GET_OPTION_VALUE(game_options, 隐匿) = mode - 20; break; + case 23: + GET_OPTION_VALUE(game_options, 点杀) = true; break; + case 24: + GET_OPTION_VALUE(game_options, 谋定后动) = true; break; + case 25: + GET_OPTION_VALUE(game_options, 炸弹) = 1; break; + case 51: + case 52: + GET_OPTION_VALUE(game_options, BOSS) = mode - 50; break; + // ===== 其他配置 ===== + case 80: + GET_OPTION_VALUE(game_options, 捉捕目标) = false; break; + case 81: + GET_OPTION_VALUE(game_options, 停止私信) = true; break; + case 82: + GET_OPTION_VALUE(game_options, 纹理) = 1; break; + case 100: + single_user = true; break; + default:; + } } + if (single_user) return NewGameMode::SINGLE_USER; return NewGameMode::MULTIPLE_USERS; }, - AlterChecker({ - {"单机", 100}, {"随机", -1}, {"怠惰的园丁", 1}, {"营养过剩", 2}, {"雨天小故事", 3}, - {"10*10", 10}, {"12*12", 12}, {"大乱斗", 20}, {"回合隐匿", 21}, {"单步隐匿", 22}, {"点杀", 23}, {"BOSS", 24}, - {"标准", 30}, {"狂野", 31}, {"幻变", 32}, {"疯狂", 33} + RepeatableChecker>(map{ + {"随机", -1}, {"怠惰的园丁", 1}, {"营养过剩", 2}, {"雨天小故事", 3}, + {"10*10", 10}, {"12*12", 12}, {"标准", 15}, {"狂野", 16}, {"幻变", 17}, {"疯狂", 18}, + {"大乱斗", 20}, {"回合隐匿", 21}, {"单步隐匿", 22}, {"点杀", 23}, {"谋定后动", 24}, {"炸弹人", 25}, + {"米诺陶斯", 51}, {"邦邦", 52}, + {"上家", 80}, {"停止私信", 81}, {"复古", 82}, + {"单机", 100}, })), }; @@ -105,7 +236,7 @@ class MainStage : public MainGameStage VoidChecker("预览"), RepeatableChecker>("序号", "2 3 0 11 E1 0 E2 7 9"))) , round_(0) , player_scores_(Global().PlayerNum(), 0) - , board(Global().ResourceDir(), GAME_OPTION(模式)) + , board(Global().ResourceDir(), GAME_OPTION(纹理), GAME_OPTION(模式), GAME_OPTION(区块)) {} virtual int64_t PlayerScore(const PlayerID pid) const override { return player_scores_[pid]; } @@ -134,7 +265,7 @@ class MainStage : public MainGameStage if (GAME_OPTION(边长) > 9) { sender << "本局游戏地图为 " << GAME_OPTION(边长) << "x" << GAME_OPTION(边长) << "\n"; } - sender << Markdown(board.GetAllBlocksInfo(GAME_OPTION(特殊事件)), 1000); + sender << Markdown(board.GetAllBlocksInfo(GAME_OPTION(特殊事件), GAME_OPTION(炸弹) > 0), (GRID_SIZE + WALL_SIZE) * 16 + 40); return StageErrCode::OK; } @@ -142,13 +273,15 @@ class MainStage : public MainGameStage { vector map_str = map_string; map_str.resize(board.unitMaps.pos.size(), "0"); - reply() << Markdown(board.MapGenerate(map_str), 60 * (GAME_OPTION(边长) + 1)); + reply() << Markdown(board.MapGenerate(map_str), (GRID_SIZE + WALL_SIZE) * (GAME_OPTION(边长) + 1)); return StageErrCode::OK; } void FirstStageFsm(SubStageFsmSetter setter) { - // Global().SaveMarkdown(board.GetAllBlocksInfo(GAME_OPTION(特殊事件), GAME_OPTION(模式)), 1000); // 用于生成全部地图列表 + // 调试:用于生成全部地图列表 + // Global().SaveMarkdown(board.GetAllBlocksInfo(0, true, 2), ((GRID_SIZE + WALL_SIZE) * 16 + 40)); + // Global().SaveMarkdown(board.GetAllBlocksInfo(0, true, 3), ((GRID_SIZE + WALL_SIZE) * 16 + 40) * 2); srand((unsigned int)time(NULL)); auto sender = Global().Boardcast(); @@ -167,7 +300,7 @@ class MainStage : public MainGameStage if (board.unitMaps.RandomizeBlockPosition(GAME_OPTION(边长))) { sender << "【10*10】本局游戏地图将更改为 " << GAME_OPTION(边长) << "x" << GAME_OPTION(边长) << " 大地图。9个区块在大地图随机排列,区块不会重叠。没有区块覆盖的地图空隙将变成普通道路。\n"; } else { - sender << "[错误] 生成10*10地图时发生错误:未能成功随机布局,游戏无法正常开始!"; + sender << "[未知错误] 生成10*10地图时发生错误:未能成功随机布局,游戏无法正常开始!"; return; } } @@ -179,26 +312,30 @@ class MainStage : public MainGameStage if (GAME_OPTION(点杀)) { // 点杀 sender << "【点杀模式】捕捉改为仅在回合结束时触发,路过不会触发捕捉\n"; } - if (GAME_OPTION(隐匿) == 1) { // 隐匿 + if (GAME_OPTION(隐匿) == 1) { // 隐匿 sender << "【隐匿模式】回合隐匿:隐匿效果持续1回合,**仅可使用1次**,隐匿后当回合的行动转为私聊进行,不会发出声响,不会触发捕捉。\n"; } else if (GAME_OPTION(隐匿) == 2) { - sender << "【隐匿模式】单步隐匿:隐匿效果仅作用于下一步,**可使用" << hide_limit << "次**,隐匿后在私聊行动一步,不会发出声响,不会触发捕捉。\n"; + sender << "【隐匿模式】单步隐匿:隐匿效果仅作用于下一步,**可使用" << HIDE_LIMIT << "次**,隐匿后在私聊行动一步,不会发出声响,不会触发捕捉。\n"; } - if (GAME_OPTION(大乱斗)) { + if (GAME_OPTION(大乱斗)) { // 大乱斗 sender << "【大乱斗模式】所有的逃生舱改为随机传送!但仍会统计逃生分\n"; } + if (GAME_OPTION(炸弹) > 0) { // 炸弹人 + sender << "【炸弹人模式】玩家可在公屏安置炸弹,经过并离开会爆炸立即出局并-100分。在炸弹上结束行动可拆除炸弹\n"; + } board.size = GAME_OPTION(边长); // 初始化玩家 board.playerNum = Global().PlayerNum(); board.players.reserve(board.playerNum); for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { - board.players.emplace_back(pid, Global().PlayerName(pid), Global().PlayerAvatar(pid, 40), board.size); - if (GAME_OPTION(隐匿) == 1) { // 隐匿 + board.players.emplace_back(pid, Global().PlayerName(pid), Global().PlayerAvatar(pid, 40), board.size, board.unitMaps.pos); + if (GAME_OPTION(隐匿) == 1) { // 隐匿 board.players[pid].hide_remaining = 1; } else if (GAME_OPTION(隐匿) == 2) { - board.players[pid].hide_remaining = hide_limit; + board.players[pid].hide_remaining = HIDE_LIMIT; } + if (GAME_OPTION(炸弹) > 0) board.players[pid].bomb = GAME_OPTION(炸弹); // 炸弹人 } board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 出口数 @@ -212,28 +349,37 @@ class MainStage : public MainGameStage // 单机模式 if (Global().PlayerNum() == 1) board.players[0].target = 100; // 初始化地图 - board.Initialize(GAME_OPTION(BOSS)); + board.Initialize(); + board.exit_num = board.ExitCount(); - if (GAME_OPTION(BOSS)) { - board.boss.all_record = "
【开局】初始锁定玩家为 [" + to_string(board.boss.target) + "号](巨响)"; - sender << "【BOSS】米诺陶斯现身于地图中,会在回合结束时追击最近的玩家。BOSS发出震耳欲聋的巨响!请所有玩家留意BOSS开局所在的方位!\n"; - sender << "当前BOSS锁定的玩家为 " << At(board.boss.target) << "\n"; + if (GAME_OPTION(BOSS) > 0) { + board.boss.BossInitialize(GAME_OPTION(BOSS)); // 初始化BOSS + board.boss.InitBossStartRecord(); + sender << "【BOSS】" + board.boss.GetBossStartInfo() + "\n"; + sender << "当前 BOSS 锁定的玩家为 " << At(board.boss.target) << "\n"; } + board.SaveGameStartMap(); // 保存初始盘面 + + int mode = GAME_OPTION(模式); sender << "[提示] 本局游戏人数为 " << board.playerNum << " 人,逃生舱数量为 " << board.exit_num << " 个。请留意私信发送的开局墙壁信息"; // 开局私信墙壁信息 for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { auto [info, md] = board.GetSurroundingWalls(pid); board.players[pid].private_record = "【开局】\n您所在位置的四周墙壁信息,按照 上下左右 顺序分别是:\n" + info; - Global().Tell(pid) << board.players[pid].private_record << "\n" << Markdown(md, 110); + Global().Tell(pid) << board.players[pid].private_record << "\n" << Markdown(md, (GRID_SIZE + WALL_SIZE * 2) + 40); } - string mode_str[4] = {"标准", "狂野", "幻变", "疯狂"}; - sender << "\n\n【区块模式】" + mode_str[GAME_OPTION(模式)]; - if (GAME_OPTION(模式) > 0) { - sender << ":本局可能出现的区块种类详见下图所示:\n"; - sender << Markdown(board.GetAllBlocksInfo(GAME_OPTION(特殊事件)), 1000); + if (mode >= 0) { + string mode_str[4] = {"标准", "狂野", "幻变", "疯狂"}; + sender << "\n\n【区块模式】" + mode_str[mode] + "\n"; + } else { + sender << "\n\n【区块模式】自定义\n"; + } + if (mode != 0) { + sender << "本局可能出现的区块详见下图所示:\n"; + sender << Markdown(board.GetAllBlocksInfo(GAME_OPTION(特殊事件), GAME_OPTION(炸弹) > 0), (GRID_SIZE + WALL_SIZE) * 16 + 40); } setter.Emplace(*this, ++round_); @@ -242,7 +388,7 @@ class MainStage : public MainGameStage void NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) { bool game_end = (Alive_() <= 1 && Global().PlayerNum() > 1) || ((Alive_() <= 0 || board.ExitCount() == 0) && Global().PlayerNum() == 1); - if (!game_end && round_ < 20) { + if (!game_end && round_ < GAME_OPTION(回合数)) { setter.Emplace(*this, ++round_); return; } @@ -260,9 +406,23 @@ class MainStage : public MainGameStage } else { Global().Boardcast() << "回合数到达上限,游戏结束"; } - Global().Boardcast() << "完整行动轨迹:\n" << Markdown(board.GetAllRecord(), 500); + Global().Boardcast() << "完整行动轨迹:\n" << Markdown(board.GetAllRecordHtml(-1), 500); Global().Boardcast() << "玩家分数细则:\n" << Markdown(board.GetAllScore(), 800); - Global().Boardcast() << Markdown(board.GetFinalBoard(), 120 * (GAME_OPTION(边长) + 1)); + Global().Boardcast() << Markdown(board.GetFinalBoard(), (GRID_SIZE + WALL_SIZE) * 2 * (GAME_OPTION(边长) + 1)); + + // 成就结算 + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + const PlayerAchievement& achievement = board.players[pid].achievement; + if (achievement.exit_without_sound) Global().Achieve(pid, Achievement::悄无声息); + if (achievement.explore_all_map) Global().Achieve(pid, Achievement::环游世界); + if (achievement.explore_all_map_silently()) Global().Achieve(pid, Achievement::游荡幽灵); + if (achievement.exit_first_round) Global().Achieve(pid, Achievement::我赶时间); + if (achievement.catch_first_round) Global().Achieve(pid, Achievement::饥渴难耐); + if (achievement.visit_five_grid_type()) Global().Achieve(pid, Achievement::乒铃乓啷); + if (achievement.catch_everyone_4p) Global().Achieve(pid, Achievement::嗜杀成性); + if (achievement.catch_without_moving) Global().Achieve(pid, Achievement::守株待兔); + if (achievement.boss_chase_four_steps) Global().Achieve(pid, Achievement::牛头魅魔); + } } }; @@ -273,21 +433,24 @@ class RoundStage : public SubGameStage<> : StageFsm(main_stage, "第 " + std::to_string(round) + " 回合" , MakeStageCommand(*this, "选择方向,在迷宫中探路", &RoundStage::MakeMove_, AlterChecker(direction_map)), MakeStageCommand(*this, "主动停止行动,并结束回合", &RoundStage::Stop_, VoidChecker("停止")), - MakeStageCommand(*this, "使用“隐匿”技能(仅限隐匿模式)", &RoundStage::Hide_, VoidChecker("隐匿")), - MakeStageCommand(*this, "查看当前回合进展情况", &RoundStage::Status_, VoidChecker("赛况")), - MakeStageCommand(*this, "查看所有玩家完整行动轨迹", &RoundStage::AllStatus_, VoidChecker("完整赛况"))) + MakeStageCommand(*this, "[隐匿模式] 使用“隐匿”技能", &RoundStage::Hide_, VoidChecker("隐匿")), + MakeStageCommand(*this, "[炸弹人模式] 在当前位置安放炸弹", &RoundStage::SetBomb_, VoidChecker("下包")), + MakeStageCommand(*this, "查看当前回合进展情况", &RoundStage::Status_, VoidChecker("赛况"), OptionalDefaultChecker(true, "图片", "文字")), + MakeStageCommand(*this, "查看所有玩家完整行动轨迹", &RoundStage::AllStatus_, VoidChecker("完整赛况"), OptionalDefaultChecker(true, "图片", "文字")), + MakeStageCommand(*this, "多步行动:一次性输入多个方向,自动在迷宫中移动", &RoundStage::MakeMultipleMove_, AnyArg("连续多个方向", "上下左右sxzyUDLR"))) { - step = 0; currentPlayer = 0; if (Main().Alive_() == 0) { - Global().Boardcast() << "[错误] 发生了意料之外的错误:无可行动玩家但游戏仍未判定结束,请联系管理员或中断游戏!"; + Global().Boardcast() << "[错误] 发生了意料之外的错误:无可行动玩家但游戏仍未判定结束,请联系管理员或中断游戏(切勿退出强制)!"; return; } while (Main().board.players[currentPlayer].out > 0) { currentPlayer = currentPlayer + 1; } + step = 0; hide = false; active_stop = false; + door_modified = 0; } // 当前行动玩家 @@ -299,12 +462,12 @@ class RoundStage : public SubGameStage<> // 主动停止或超时 bool active_stop; - // 记录门的状态发生过改变 - bool door_modified = false; + // [回合] 记录门的状态发生改变的次数 + int door_modified; virtual void OnStageBegin() override { - Global().SaveMarkdown(Main().board.GetBoard(Main().board.grid_map), 60 * (GAME_OPTION(边长) + 1)); + Global().SaveMarkdown(Main().board.GetBoard(Main().board.grid_map), (GRID_SIZE + WALL_SIZE) * (GAME_OPTION(边长) + 1)); for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { Main().board.players[pid].ClearMoveRecord(); @@ -313,31 +476,61 @@ class RoundStage : public SubGameStage<> } } Global().Boardcast() << Markdown(Main().board.GetPlayerTable(Main().round_)); - uint32_t think_time = Main().board.players[currentPlayer].hook_status ? 30 : GAME_OPTION(思考时限); - Global().Boardcast() << "请 " << At(currentPlayer) << " 在公屏选择方向移动(第一次行动前有 " << think_time << " 秒思考时间,行动开始后总时限为 " << GAME_OPTION(行动时限) << " 秒)"; - Global().StartTimer(think_time); + + if (GAME_OPTION(谋定后动)) { + uint32_t think_time = Main().board.players[currentPlayer].hook_status ? 30 : GAME_OPTION(行动时限); + Global().Boardcast() << "请 " << At(currentPlayer) << " 在公屏选择方向移动,仅能移动一次(时限 " << think_time << " 秒)"; + Global().StartTimer(think_time); + } else { + StartNextPlayerTurn(Main().board.players[currentPlayer]); + } if (Main().round_ == 1) { - if (GAME_OPTION(BOSS)) SendSoundMessage(Main().board.boss.x, Main().board.boss.y, Sound::BOSS, true); - Global().Boardcast() << "可尝试使用「预览」指令生成自定义地图来记录草稿,格式例如:预览 2 3 0 11 E1 0 E2(其中E前缀表示逃生舱)" - << "\n\n!!!【重要提醒】!!! 请注意:当前版本主动停止或超时将无法得知四周墙壁信息!"; + // [BOSS-米诺陶斯] 开局声音 + if (GAME_OPTION(BOSS) > 0 && Main().board.boss.type == BossType::MINOTAUR) { + SendSoundMessage(Main().board.boss.x, Main().board.boss.y, Sound::BOSS, true); + } + Global().Boardcast() << "指令「预览」可生成自定义地图来记录草稿,格式例如:预览 2 3 0 11 E1 0 E2(E前缀表示逃生舱)\n\n" + << "私信「完整赛况」可查询其他玩家的完整声响方向历史记录\n\n" + << (GAME_OPTION(谋定后动) ? "【谋定后动】每回合仅能执行一次移动,可使用多步行动指令\n" : "") + << (GAME_OPTION(停止私信) + ? "【有停止私信】主动停止或超时可以获得私信四周墙壁信息" + : "【无停止私信】主动停止或超时将无法得知四周墙壁信息"); } } private: - AtomReqErrCode MakeMove_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, Direct direction) + void StartNextPlayerTurn(const Player& player) + { + uint32_t think_time = player.hook_status ? 30 : GAME_OPTION(思考时限); + Global().Boardcast() << "请 " << At(currentPlayer) << " 在公屏选择方向移动(第一次行动前有 " << think_time << " 秒思考时间,行动开始后获得额外时间 " + << GAME_OPTION(行动时限) << " 秒,剩余加时卡 " << player.extra_time_card << " 张)" + << (player.bomb > 0 ? ",剩余炸弹 " + to_string(player.bomb) + " 个" : ""); + Global().StartTimer(think_time); + } + + bool CheckCommon(const PlayerID pid, MsgSenderBase& reply) { Player& player = Main().board.players[pid]; + player.hook_status = false; if (player.out == 2) { reply() << "您已乘坐逃生舱撤离,无需继续行动"; - return StageErrCode::FAILED; + return false; } if (pid != currentPlayer) { reply() << "[错误] 不是您的回合,当前正在行动玩家是:" << Global().PlayerName(currentPlayer); - return StageErrCode::FAILED; + return false; } + return true; + } + + AtomReqErrCode MakeMove_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, Direct direction) + { + if (!CheckCommon(pid, reply)) return StageErrCode::FAILED; + + Player& player = Main().board.players[pid]; if (!is_public && Global().PlayerNum() > 1) { if (GAME_OPTION(隐匿) == 0) { - reply() << "[错误] 请全程在公屏进行行动"; + reply() << "[错误] 多人游戏下,请全程在公屏进行行动"; return StageErrCode::FAILED; } if (!hide) { @@ -345,8 +538,6 @@ class RoundStage : public SubGameStage<> return StageErrCode::FAILED; } } - - if (player.hook_status) player.hook_status = false; bool success = Main().board.MakeMove(pid, direction, hide); step++; @@ -354,170 +545,107 @@ class RoundStage : public SubGameStage<> // 撞墙直接切换下一个玩家 if (!success) { if (step == 1) { - reply() << UnitMaps::RandomHint(UnitMaps::firststep_wall_hints) << "\n移动时碰撞【墙壁】,本回合结束!**请留意机器人私信发送的四周墙壁信息**"; + reply() << "[第 1 步] 尝试向 " + dir_cn[static_cast(direction)] + " 移动\n" + << GetRandomHint(firststep_wall_hints) << "\n移动时碰撞【墙壁】,本回合结束!**请留意机器人私信发送的四周墙壁信息**"; } else { - reply() << UnitMaps::RandomHint(UnitMaps::wall_hints) << "\n移动时碰撞【墙壁】,本回合结束!请留意机器人私信发送的四周墙壁信息"; + reply() << "[第 " << step << " 步] 尝试向 " + dir_cn[static_cast(direction)] + " 移动\n" + << GetRandomHint(wall_hints) << "\n移动时碰撞【墙壁】,本回合结束!请留意机器人私信发送的四周墙壁信息"; } return StageErrCode::READY; } auto sender = reply(); - - bool ready_status = HandleGridInteraction(player, sender); + sender << "[第 " << step << " 步] 向 " + dir_cn[static_cast(direction)] + " 移动"; + + bool ready_status = HandleGridInteraction(player, sender, false); if (ready_status) return StageErrCode::READY; + if (GAME_OPTION(谋定后动)) { + step++; + active_stop = true; + return StageErrCode::READY; + } + // 继续行动 if (step == 1) { - sender << "请继续行动(行动总时限为 " << GAME_OPTION(行动时限) << " 秒)"; - Global().StartTimer(GAME_OPTION(行动时限)); + const int move_time = TimerLeft() + GAME_OPTION(行动时限); + sender << "\n\n请继续行动(行动总时限为 " << move_time << " 秒)"; + Global().StartTimer(move_time); } else { - int time = std::chrono::duration_cast(*Global().TimerFinishTime() - std::chrono::steady_clock::now()).count(); - sender << "请继续行动(剩余时间 " << time << " 秒)"; + sender << "\n\n请继续行动(剩余时间 " << TimerLeft() << " 秒)"; } // 单步隐匿:消除隐匿状态 if (GAME_OPTION(隐匿) == 2 && hide) { hide = false; } return StageErrCode::OK; } - // 处理区块效果(返回玩家回合是否结束) - bool HandleGridInteraction(Player& player, MsgSenderBase::MsgSenderGuard& sender) + AtomReqErrCode MakeMultipleMove_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const string& direction_str) { - if (player.subspace > 0) return false; // 亚空间内不影响地图 - - Grid& grid = Main().board.grid_map[player.x][player.y]; - // [逃生舱] - if (grid.Type() == GridType::EXIT) { - 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.NewContentRecord("【传送】"); - Main().board.TeleportPlayer(player.pid); // 大乱斗随机传送 - sender << UnitMaps::RandomHint(UnitMaps::exit_hints) << "\n\n您已抵达【逃生舱】!此逃生舱已失效," << At(player.pid) << " 被随机传送至地图其他地方!"; - } else { - player.out = 2; - sender << UnitMaps::RandomHint(UnitMaps::exit_hints) << "\n\n您已抵达【逃生舱】!不再参与后续游戏,此逃生舱已失效"; - if (Main().Alive_() > 1) { - Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 - sender << ",剩余玩家捕捉目标顺位发生变更!\n"; - sender << Markdown(Main().board.GetPlayerTable(Main().round_)); - } - } - // 隐匿状态公屏提示 - if (hide) { - Global().Boardcast() << At(currentPlayer) << " 已抵达【逃生舱】!" << (GAME_OPTION(大乱斗) ? "被随机传送至地图其他地方" : "不再参与后续游戏") << ",此逃生舱已失效\n" - << Markdown(Main().board.GetPlayerTable(Main().round_)); - } - return true; - } - // [传送门] - if (player.subspace == 0) { - // 离开亚空间,传送门传送 - Main().board.RemovePlayerFromMap(player.pid); - grid.PortalTeleport(player); - Main().board.player_map[player.x][player.y].push_back(player.pid); - } else if (grid.Type() == GridType::PORTAL) { - if (player.subspace == -1) { - // 进入亚空间 - player.subspace = 2; - // 出口是[单向传送门]则交换 - pair pRelPos = Main().board.grid_map[player.x][player.y].PortalPos(); - Grid& target_grid = Main().board.grid_map[player.x + pRelPos.first][player.y + pRelPos.second]; - if (target_grid.Type() == GridType::ONEWAYPORTAL) { - grid.SetType(GridType::ONEWAYPORTAL); - target_grid.SetType(GridType::PORTAL); - } - } - } - // [陷阱] - if (grid.Type() == GridType::TRAP) { - grid.TrapTrigger(); - if (grid.TrapStatus()) { - player.UpdateEndRecord("陷阱"); - sender << UnitMaps::RandomHint(UnitMaps::trap_hints) << "\n\n移动触发【陷阱】,本回合被强制停止行动!"; - return true; - } - } - // [热源] - string step_info, heat_message; - if (Main().board.HeatNotice(player.pid)) { - step_info = "[热浪(第" + to_string(step) + "步)]"; - heat_message = UnitMaps::RandomHint(UnitMaps::heat_wave_hints) + "\n移动进入【热浪范围】,当前位置附近存在热源"; - } - if (grid.Type() == GridType::HEAT) { - if (player.heated) { - player.UpdateEndRecord("热源"); - sender << UnitMaps::RandomHint(UnitMaps::heat_active_hints) << "\n\n您本局游戏已进入过【热源】,高温难耐,本回合无法继续前进!"; - return true; - } else { - player.heated = true; - step_info = "[热源(第" + to_string(step) + "步)]"; - heat_message = UnitMaps::RandomHint(UnitMaps::heat_core_hints) + "\n移动进入【热源】!请注意,在下一次进入热源时,将公开热源并强制停止行动"; - } + if (!CheckCommon(pid, reply)) return StageErrCode::FAILED; + + Player& player = Main().board.players[pid]; + if (hide && GAME_OPTION(隐匿) == 2) { + reply() << "[错误] 您正处于单步隐匿状态,无法使用多步移动指令,请私信裁判使用常规移动"; + return StageErrCode::FAILED; } - if (heat_message != "") { - Global().Tell(player.pid) << step_info << heat_message; - player.private_record += "\n" + step_info; + if (!is_public && Global().PlayerNum() > 1) { + reply() << "[错误] 多人游戏下,多步行动指令只能在公屏使用"; + return StageErrCode::FAILED; } - // [按钮] - if (grid.Type() == GridType::BUTTON) { - Grid::ButtonTarget target = grid.ButtonTargetPos(); - const int s = Main().board.size; - // 切换[门]状态 - if (target.dir.has_value()) { - int tx = player.x + target.dx; - int ty = player.y + target.dy; - const Direct dir = target.dir.value(); - Main().board.grid_map[tx][ty].switchDoor(dir); - int d = static_cast(dir); - int nx = (tx + k_DX_Direct[d] + s) % s; - int ny = (ty + k_DY_Direct[d] + s) % s; - Main().board.grid_map[nx][ny].switchDoor(opposite(dir)); - door_modified = true; - } + + vector directions; + string result = Board::parseDirections(direction_str, directions); + + if (!result.empty()) { + reply() << "[错误] 解析失败:检测到未知字符 \"" + result + "\",仅支持包含:\n上/s/U、下/x/D、左/z/L、右/y/R"; + return StageErrCode::FAILED; } - // 声响 Sound - Sound sound = Main().board.GetSound(grid, GAME_OPTION(特殊事件) == 3); - if (sound == Sound::SHASHA) { - if (hide) { - sender << "移动进入【树丛】(隐匿中,不会向其他人发出声响)\n\n"; - } else { - player.UpdateSoundRecord(sound); - sender << UnitMaps::RandomHint(UnitMaps::grass_hints) << "\n移动进入【树丛】,请其他玩家留意私信声响信息!\n\n"; - SendSoundMessage(player.x, player.y, sound, false); - } - } else if (sound == Sound::PAPA) { - if (hide) { - sender << "移动发出【啪啪声】(隐匿中,不会向其他人发出声响)\n\n"; - } else { - player.UpdateSoundRecord(sound); - sender << UnitMaps::RandomHint(UnitMaps::papa_hints) << "\n移动发出【啪啪声】,请其他玩家留意私信声响信息!\n\n"; - SendSoundMessage(player.x, player.y, sound, false); + + for (auto it = directions.begin(); it != directions.end(); ++it) { + const Direct& direct = *it; + bool success = Main().board.MakeMove(pid, direct, hide); + step++; + // 中途撞墙直接停止行动 + if (!success) { + if (step == 1) { + reply() << "[第 1 步] 尝试向 " + dir_cn[static_cast(direct)] + " 移动\n" + << GetRandomHint(firststep_wall_hints) << "\n移动时碰撞【墙壁】,本回合结束!**请留意机器人私信发送的四周墙壁信息**"; + } else { + reply() << "[第 " << step << " 步] 尝试向 " + dir_cn[static_cast(direct)] + " 移动\n" + << GetRandomHint(wall_hints) << "\n移动时碰撞【墙壁】,本回合结束!请留意机器人私信发送的四周墙壁信息"; + } + return StageErrCode::READY; } - } - // 非点杀模式检测玩家捕捉(隐匿状态不能捕捉) - if (!GAME_OPTION(点杀)) { - if (PlayerCatch(player, sender)) { - return true; + + if (step == 1) Global().StartTimer(TimerLeft() + GAME_OPTION(行动时限)); + auto sender = reply(); + sender << "[第 " << step << " 步] 向 " + dir_cn[static_cast(direct)] + " 移动"; + + bool ready_status = HandleGridInteraction(player, sender, true); + if (ready_status) return StageErrCode::READY; + + if (std::next(it) == directions.end()) { + if (GAME_OPTION(谋定后动)) { + step++; + active_stop = true; + return StageErrCode::READY; + } + + sender << "\n\n请继续行动(剩余时间 " << TimerLeft() << " 秒)"; } + // 每步随机延迟 + std::uniform_int_distribution dist(0, 1000); + int delay_ms = 1000 + dist(Main().board.g); + std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); } - // 玩家可继续行动 - return false; + return StageErrCode::OK; } AtomReqErrCode Stop_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) { + if (!CheckCommon(pid, reply)) return StageErrCode::FAILED; + Player& player = Main().board.players[pid]; - if (player.out == 2) { - reply() << "您已乘坐逃生舱撤离,无需继续行动"; - return StageErrCode::FAILED; - } - if (pid != currentPlayer) { - reply() << "[错误] 不是您的回合,当前正在行动玩家是:" << Global().PlayerName(currentPlayer); - return StageErrCode::FAILED; - } if (!is_public && Global().PlayerNum() > 1) { if (GAME_OPTION(隐匿) == 0) { reply() << "[错误] 请全程在公屏进行行动"; @@ -526,9 +654,12 @@ class RoundStage : public SubGameStage<> } return StageErrCode::FAILED; } - if (!hide) player.NewContentRecord("(停止)"); + + step++; active_stop = true; - reply() << "您选择主动停止行动,本回合结束!主动停止无法获得四周墙壁信息"; + if (!hide) player.NewContentRecord("(停止)"); + reply() << "[第 " << step << " 步] 您选择主动停止行动,本回合结束!" + << (GAME_OPTION(停止私信) ? "请留意机器人私信发送的四周墙壁信息" : "主动停止无法获得四周墙壁信息"); return StageErrCode::READY; } @@ -538,15 +669,10 @@ class RoundStage : public SubGameStage<> reply() << "[错误] 本局游戏未开启隐匿技能"; return StageErrCode::FAILED; } + + if (!CheckCommon(pid, reply)) return StageErrCode::FAILED; + Player& player = Main().board.players[pid]; - if (player.out == 2) { - reply() << "您已乘坐逃生舱撤离,无需继续行动"; - return StageErrCode::FAILED; - } - if (pid != currentPlayer) { - reply() << "[错误] 不是您的回合,当前正在行动玩家是:" << Global().PlayerName(currentPlayer); - return StageErrCode::FAILED; - } if (hide) { reply() << "[错误] 您已经处于隐匿状态,请在私信选择行动"; return StageErrCode::FAILED; @@ -563,40 +689,69 @@ class RoundStage : public SubGameStage<> hide = true; player.hide_remaining--; if (GAME_OPTION(隐匿) == 1) { - player.NewContentRecord("【隐匿行动】"); + player.NewContentRecord("<隐匿行动>", "hide"); reply() << "使用隐匿技能,本回合剩余时间转为私聊行动,不会发出声响,不会触发捕捉。剩余次数:" << player.hide_remaining; } else if (GAME_OPTION(隐匿) == 2) { - player.NewContentRecord("�"); + player.NewContentRecord("�", "hide"); reply() << "使用隐匿技能,下一步请在私聊行动,不会发出声响,不会触发捕捉。剩余次数:" << player.hide_remaining; } return StageErrCode::OK; } - AtomReqErrCode Status_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + AtomReqErrCode SetBomb_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + if (GAME_OPTION(炸弹) == 0) { + reply() << "[错误] 本局游戏未开启炸弹人模式"; + return StageErrCode::FAILED; + } + + if (!CheckCommon(pid, reply)) return StageErrCode::FAILED; + + Player& player = Main().board.players[pid]; + if (player.bomb == 0) { + reply() << "[错误] 炸弹已用尽。但脚下地雷遍布,请谨慎前行"; + return StageErrCode::FAILED; + } + if (!is_public && Global().PlayerNum() > 1) { + reply() << "[错误] 请在公屏使用下包技能"; + return StageErrCode::FAILED; + } + + Grid& grid = Main().board.grid_map[player.x][player.y]; + if (grid.Attach() == AttachType::EMPTY) grid.SetAttach(AttachType::BOMB); + + player.bomb--; + player.NewContentRecord("[下包]", "bomb"); + reply() << "在当前所在位置执行下包操作。剩余:" << player.bomb; + return StageErrCode::OK; + } + + AtomReqErrCode Status_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const bool show_image) { + Main().board.players[pid].hook_status = false; auto sender = reply(); - if (is_public) { - if (GAME_OPTION(特殊事件) > 0) { - sender << UnitMaps::ShowSpecialEvent(GAME_OPTION(特殊事件)) << "\n"; - } - if (GAME_OPTION(边长) > 9) { - sender << "本局游戏地图为 " << GAME_OPTION(边长) << "x" << GAME_OPTION(边长) << "\n"; - } + if (show_image) { sender << Markdown(Main().board.GetPlayerTable(Main().round_)); + } else { + sender << Main().board.GetPlayerString() << "\n"; + } + + if (is_public) { for (PlayerID pid = 0; pid < currentPlayer.Get(); ++pid) { if (Main().board.players[pid].out == 0) { - sender << "\n[" << pid.Get() << "号]本回合行动轨迹:\n" << Main().board.players[pid].GetMoveRecord(); + sender << "\n[" << pid.Get() << "号]本回合行动轨迹:\n" << Main().board.players[pid].move_record.GetMoveRecord(-1); } } - sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].GetMoveRecord(); + sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].move_record.GetMoveRecord(-1); } else { - sender << Main().board.players[pid].private_record; + sender << "\n" << Main().board.players[pid].private_record; } return StageErrCode::OK; } - AtomReqErrCode AllStatus_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + AtomReqErrCode AllStatus_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const bool show_image) { + Main().board.players[pid].hook_status = false; auto sender = reply(); if (GAME_OPTION(特殊事件) > 0) { sender << UnitMaps::ShowSpecialEvent(GAME_OPTION(特殊事件)) << "\n"; @@ -604,98 +759,42 @@ class RoundStage : public SubGameStage<> if (GAME_OPTION(边长) > 9) { sender << "本局游戏地图为 " << GAME_OPTION(边长) << "x" << GAME_OPTION(边长) << "\n"; } - sender << Markdown(Main().board.GetAllRecord(), 500); - sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].GetMoveRecord(); - if (!is_public) { - sender << "\n\n" << Main().board.players[pid].private_record; - } - return StageErrCode::OK; - } - - bool PlayerCatch(Player& player, MsgSenderBase::MsgSenderGuard& sender) - { - vector list = Main().board.player_map[player.x][player.y]; - PlayerID t = player.target; - if (find(list.begin(), list.end(), t) != list.end() && !hide) { - active_stop = false; - sender << UnitMaps::RandomHint(UnitMaps::catch_hints) << "\n\n"; - sender << At(t) << " 被捕捉!"; - - if (Main().round_ == 1) { - 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; - } - 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.NewContentRecord("【传送】"); - Main().board.TeleportPlayer(player.pid); // 随机传送捕捉方 - Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 - sender << "\n" << At(player.pid) << " 被随机传送至地图其他地方,捕捉目标顺位发生变更!\n"; - sender << Markdown(Main().board.GetPlayerTable(Main().round_)); - } - - if (Main().Alive_() == 1) { - // 无逃生舱最后生还判定 - if (Main().board.ExitCount() == 0) Main().withoutE_win_ = true; - // 有逃生舱但死斗取胜判定 - if (Main().board.ExitCount() > 0) Main().withE_win_ = true; - } - return true; + int query_pid = is_public ? -1 : pid.Get(); + if (show_image) { + sender << Markdown(Main().board.GetAllRecordHtml(query_pid), 550); + } else { + sender << Main().board.GetAllRecordString(query_pid); } - return false; - } - // 私信其他玩家发送声响信息 - void SendSoundMessage(const int fromX, const int fromY, const Sound sound, const bool to_all) - { - for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { - if ((pid != currentPlayer || to_all) && Main().board.players[pid].out == 0) { - string direction = Main().board.GetSoundDirection(fromX, fromY, Main().board.players[pid]); - string step_info = "[" + to_string(currentPlayer) + "号(第" + to_string(step) + "步)]"; - string sound_message; - if (direction == "") { - if (Main().board.players[currentPlayer].target != pid || GAME_OPTION(点杀)) { - switch (sound) { - case Sound::SHASHA: sound_message = step_info + "\n" + UnitMaps::RandomHint(UnitMaps::grass_sound_hints); break; - case Sound::PAPA: sound_message = step_info + "\n" + UnitMaps::RandomHint(UnitMaps::papa_sound_hints); break; - default: sound_message = "[错误] 未知声音类型:相同格子的未知声音"; - } - } - } else { - switch (sound) { - case Sound::SHASHA: sound_message = step_info + "你听见了来自【" + direction + "方】的沙沙声!"; break; - case Sound::PAPA: sound_message = step_info + "你听见了来自【" + direction + "方】的啪啪声!"; break; - case Sound::BOSS: sound_message = "[BOSS] 你听见了来自【" + direction + "方】的巨大响声!"; break; - default: sound_message = "[错误] 未知声音类型:不同格子来自【" + direction + "方】的未知声音"; - } - } - Main().board.players[pid].private_record += "\n" + sound_message; - Global().Tell(pid) << sound_message; - } + int query_pid_current = is_public || currentPlayer == pid ? -1 : pid.Get(); + sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].move_record.GetMoveRecord(query_pid_current); + if (!is_public) { + sender << "\n\n" << Main().board.players[pid].private_record; } + return StageErrCode::OK; } virtual CheckoutErrCode OnStageTimeout() override { - Main().board.players[currentPlayer].NewContentRecord("(超时)"); - active_stop = true; + Player& player = Main().board.players[currentPlayer]; if (step == 0) { - Main().board.players[currentPlayer].hook_status = true; - Global().Boardcast() << "玩家 " << At(PlayerID(currentPlayer)) << " 超时未行动,已进入挂机状态,再次行动前仅有30秒等待时间"; + if (!player.hook_status) { + Global().Tell(currentPlayer) << "您已进入挂机状态,等待时间将缩减至 30 秒,执行游戏指令可恢复至原状态"; + } + player.hook_status = true; + Global().Boardcast() << "玩家 " << At(PlayerID(currentPlayer)) << " 超时未行动,已进入挂机状态,再次行动前仅有 30 秒等待时间"; } else { + if (player.extra_time_card > 0) { + player.extra_time_card--; + Global().Boardcast() << "行动超时,自动使用加时卡,剩余时间延长 " + to_string(EXTRATIMECRAD_TIME) + " 秒,剩余 " + to_string(player.extra_time_card) + " 张加时卡"; + Global().StartTimer(EXTRATIMECRAD_TIME); + return StageErrCode::CONTINUE; + } Global().Boardcast() << "玩家 " << At(PlayerID(currentPlayer)) << " 行动超时,切换下一个玩家"; } + player.NewContentRecord("(超时)"); + active_stop = true; Global().SetReady(currentPlayer); return HandleStageOver(); } @@ -727,32 +826,39 @@ class RoundStage : public SubGameStage<> CheckoutErrCode HandleStageOver() { Player& player = Main().board.players[currentPlayer]; - // 点杀模式:回合结束才触发捉捕 - if (GAME_OPTION(点杀)) { - vector list = Main().board.player_map[player.x][player.y]; - if (find(list.begin(), list.end(), player.target) != list.end() && !hide) { - auto sender = Global().Boardcast(); - PlayerCatch(player, sender); - } + // 回合结束始终检查捕捉 + vector list = Main().board.player_map[player.x][player.y]; + if (find(list.begin(), list.end(), player.target) != list.end() && !hide && player.out == 0) { + auto sender = Global().Boardcast(); + PlayerCatch(player, sender); } - Global().Boardcast() << "[" << currentPlayer.Get() << "号]玩家本回合的完整行动轨迹:\n" << player.GetMoveRecord(); + Grid& grid = Main().board.grid_map[player.x][player.y]; + + Global().Boardcast() << "[" << currentPlayer.Get() << "号]玩家本回合的完整行动轨迹:\n" << player.move_record.GetMoveRecord(-1); // 记录历史行动轨迹 - player.all_record += "
【第 " + to_string(Main().round_) + " 回合】
" + player.GetMoveRecord(); + player.all_record.push_back(player.move_record); // 仅剩1玩家,游戏结束 if ((Main().Alive_() == 1 && Global().PlayerNum() > 1) || (Main().Alive_() == 0 && Global().PlayerNum() == 1)) { if (Main().withoutE_win_) { // 无逃生舱最后生还胜利 - Global().Boardcast() << At(currentPlayer) << "\n" << UnitMaps::RandomHint(UnitMaps::withoutE_win_hints); + Global().Boardcast() << At(currentPlayer) << "\n" << GetRandomHint(withoutE_win_hints); } if (Main().withE_win_) { // 有逃生舱但死斗取胜 - Global().Boardcast() << At(currentPlayer) << "\n" << UnitMaps::RandomHint(UnitMaps::withE_win_hints); + Global().Boardcast() << At(currentPlayer) << "\n" << GetRandomHint(withE_win_hints); } return StageErrCode::CHECKOUT; } // 私信发送四周墙壁信息(主动停止或超时不发送) - if (player.out == 0 && !active_stop) { + if (player.out == 0 && (!active_stop || GAME_OPTION(停止私信))) { auto [info, md] = Main().board.GetSurroundingWalls(currentPlayer); player.private_record = "【第 " + to_string(Main().round_) + " 回合】\n您所在位置的四周墙壁信息,按照 上下左右 顺序分别是:\n" + info; - Global().Tell(currentPlayer) << player.private_record << "\n" << Markdown(md, 110); + Global().Tell(currentPlayer) << player.private_record << "\n" << Markdown(md, (GRID_SIZE + WALL_SIZE * 2) + 40); + } + // 已触发炸弹才能拆除炸弹 + if (grid.Attach() == AttachType::BOMB && player.bomb_trigger) { + player.bomb_trigger = false; + grid.SetAttach(AttachType::EMPTY); + Global().Tell(currentPlayer) << "引线熄灭,爆炸未曾发生,你成功拆除了脚下的【炸弹】"; + player.private_record += "\n回合结束时拆除【炸弹】"; } step = 0; hide = false; @@ -763,78 +869,41 @@ class RoundStage : public SubGameStage<> if (currentPlayer == 0) break; } while (Main().board.players[currentPlayer].out > 0); if (currentPlayer != 0) { - uint32_t think_time = Main().board.players[currentPlayer].hook_status ? 30 : GAME_OPTION(思考时限); - Global().Boardcast() << "请 " << At(currentPlayer) << " 在公屏选择方向移动(第一次行动前有 " << think_time << " 秒思考时间,行动开始后总时限为 " << GAME_OPTION(行动时限) << " 秒)"; + if (GAME_OPTION(谋定后动)) { + uint32_t think_time = Main().board.players[currentPlayer].hook_status ? 30 : GAME_OPTION(行动时限); + Global().Boardcast() << "请 " << At(currentPlayer) << " 在公屏选择方向移动,仅能移动一次(时限 " << think_time << " 秒)"; + Global().StartTimer(think_time); + } else { + StartNextPlayerTurn(Main().board.players[currentPlayer]); + } Global().ClearReady(currentPlayer); - Global().StartTimer(think_time); return StageErrCode::CONTINUE; } // [回合结束] 所有玩家都行动后结束本回合 + // 门变更过进行提示 + if (door_modified > 0) { + Global().Boardcast() << "【注意】在本回合内,门曾被按钮触发,共发生 " + to_string(door_modified) + " 次变化"; + Main().board.all_extra_record += "
【第 " + to_string(Main().round_) + " 回合】门曾被按钮触发,发生 " + to_string(door_modified) + " 次变化"; + door_modified = 0; + } // BOSS相关结算 - if (GAME_OPTION(BOSS)) { - string boss_record = "
【第 " + to_string(Main().round_) + " 回合】"; - auto& boss = Main().board.boss; + if (GAME_OPTION(BOSS) > 0) { + string boss_record = "【第 " + to_string(Main().round_) + " 回合】"; + Boss& boss = Main().board.boss; + boss.NewRecord(""); auto sender = Global().Boardcast(); sender << "【回合结束[BOSS行动]】"; - if (Main().board.BossChangeTarget(false)) { - // 未更换目标,执行移动 - bool is_catch = Main().board.BossMove(); - // 抓住玩家 - if (is_catch) { - for (auto pid: Main().board.player_map[boss.x][boss.y]) { - if (Main().board.players[pid].out > 0) continue; - Main().board.players[pid].all_record += "【BOSS捕捉】"; - Main().board.players[pid].out = 1; - if (Global().PlayerNum() > 1) Global().Eliminate(pid); - Main().board.players[pid].score.catch_score -= 100; // 抓人分 - boss_record += "[" + to_string(pid) + "号] "; - sender << "\n" << At(pid); - } - boss_record += "被BOSS捕捉出局!"; - sender << "\n被BOSS捕捉出局!"; - if (Main().Alive_() > 1) { - Main().board.BossChangeTarget(true); // 重置锁定目标 - Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 - boss_record += "变更目标至 [" + to_string(boss.target) + "号]"; - sender << "\n\nBOSS更换锁定目标至 " << At(boss.target) << ",同时玩家捕捉目标顺位发生变更!\n"; - sender << Markdown(Main().board.GetPlayerTable(Main().round_)); - } - } else { - boss_record += "向 [" + to_string(boss.target) + "号] 移动了 " + to_string(boss.steps) + " 步"; - sender << "【回合结束】\nBOSS向 " << At(boss.target) << " 移动了 " << boss.steps << " 步"; - } - // BOSS移动后发出巨响 - if (Main().Alive_() > 1 || (Global().PlayerNum() == 1 && Main().board.players[0].out == 0)) { - boss_record += "(巨响)"; - sender << "\n\nBOSS发出震耳欲聋的巨响!请所有玩家留意私信声响信息!"; - SendSoundMessage(boss.x, boss.y, Sound::BOSS, true); - } - } else { - // 更换目标,重置步数 - boss_record += "发现更近的目标,变更目标至 [" + to_string(boss.target) + "号]"; - sender << "\nBOSS发现了距离更近的玩家,变更锁定目标至 " << At(boss.target); - } + if (boss.Is(BossType::MINOTAUR)) HandleMinotaurBossAction(boss, boss_record, sender); + if (boss.Is(BossType::BANGBANG)) HandleBangBangBossAction(boss, boss_record, sender); - // BOSS周围8格内获得喘息提示 - for (auto& player : Main().board.players) { - if (Main().board.IsBossNearby(player) && player.out == 0) { - player.private_record += "\n[BOSS] 你听到来自BOSS沉重的喘息声!"; - Global().Tell(player.pid) << "「呼……呼……」你听到来自BOSS沉重的喘息声!"; - } - } - boss.all_record += boss_record; - } - // 门变更过进行提示 - if (door_modified) { - Global().Boardcast() << "【注意】在本回合内,有门的状态发生过变化,但存在恢复原状的可能"; - door_modified = false; + boss.UpdateContentRecord(boss_record); } return StageErrCode::CHECKOUT; } - + virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) override { if (Global().IsReady(pid)) { @@ -852,8 +921,362 @@ class RoundStage : public SubGameStage<> } return StageErrCode::READY; } + + int TimerLeft() const { return std::chrono::duration_cast(*Global().TimerFinishTime() - std::chrono::steady_clock::now()).count(); } + + // ========== 成员函数 ========== + bool HandleGridInteraction(Player& player, MsgSenderBase::MsgSenderGuard& sender, const bool multiple_mode); + bool PlayerCatch(Player& player, MsgSenderBase::MsgSenderGuard& sender); + void SendSoundMessage(const int fromX, const int fromY, const Sound sound, const bool to_all); + void HandleMinotaurBossAction(Boss& boss, string& boss_record, MsgSenderBase::MsgSenderGuard& sender); + void HandleBangBangBossAction(Boss& boss, string& boss_record, MsgSenderBase::MsgSenderGuard& sender); }; + +// 处理区块效果(返回玩家回合是否结束) +bool RoundStage::HandleGridInteraction(Player& player, MsgSenderBase::MsgSenderGuard& sender, const bool multiple_mode) +{ + const string prefix = "\n"; + + // [亚空间] * 优先级必须最高 * + Grid& former_grid = Main().board.grid_map[player.x][player.y]; + // 亚空间内不影响地图 + if (player.InSubspace()) return false; + // 离开亚空间,传送门传送 + bool this_move_teleport = false; + if (player.subspace == 0) { + Main().board.RemovePlayerFromMap(player.pid); + former_grid.PortalTeleport(player); + this_move_teleport = true; + Main().board.player_map[player.x][player.y].push_back(player.pid); + } + + // 玩家当前所在格子 + Grid& grid = Main().board.grid_map[player.x][player.y]; + + // 成就[乒铃乓啷]辅助 + player.achievement.visitGrid(grid.Type()); + player.achievement.visitAttach(grid.Attach()); + /* ========== AttachType ========== */ + // [按钮] + if (grid.Attach() == AttachType::BUTTON) { + vector targets = grid.ButtonTargetPos(); + const int s = Main().board.size; + for (auto const &target : targets) { + // 切换[门]状态 + if (target.dir.has_value()) { + int tx = player.x + target.dx; + int ty = player.y + target.dy; + const Direct dir = target.dir.value(); + Main().board.grid_map[tx][ty].switchDoor(dir); + int d = static_cast(dir); + int nx = (tx + k_DX_Direct[d] + s) % s; + int ny = (ty + k_DY_Direct[d] + s) % s; + Main().board.grid_map[nx][ny].switchDoor(opposite(dir)); + } + } + door_modified++; + } + // [炸弹] + if (player.bomb_trigger) { + // 炸弹爆炸 + player.UpdateEndRecord("炸飞出局"); + player.out = 1; + player.score.catch_score -= 100; + grid.SetAttach(AttachType::EMPTY); + sender << prefix << GetRandomHint(bomb_hints) << "\n\n尝试移动时触发【炸弹】,被炸飞出局,失去行动能力!"; + if (Global().PlayerNum() > 1) Global().Eliminate(player.pid); + if (Main().Alive_() > 1) { + Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 + sender << "\n\n剩余玩家捕捉目标顺位发生变更!\n"; + sender << Markdown(Main().board.GetPlayerTable(Main().round_)); + } + return true; + } + if (grid.Attach() == AttachType::BOMB) { + player.bomb_trigger = true; + } + /* ========== GridType ========== */ + // [逃生舱] + if (grid.Type() == GridType::EXIT) { + player.NewContentRecord("(逃生)", "escape"); + int exited = Main().board.exit_num - Main().board.ExitCount(); + player.score.exit_score += Score::exit_order[exited]; // 逃生分 + grid.SetType(GridType::EMPTY); + if (!player.achievement.trigger_sound) player.achievement.exit_without_sound = true; // 成就【悄无声息】 + if (Main().round_ == 1) player.achievement.exit_first_round = true; // 成就【我赶时间】 + + if (GAME_OPTION(大乱斗)) { + player.NewContentRecord("[传送]", "teleport"); + Main().board.TeleportPlayer(player.pid); // 大乱斗随机传送 + sender << prefix << GetRandomHint(exit_hints) << "\n\n您已抵达【逃生舱】!此逃生舱已失效," << At(player.pid) << " 被随机传送至地图其他地方!"; + } else { + player.out = 2; + sender << prefix << GetRandomHint(exit_hints) << "\n\n您已抵达【逃生舱】!不再参与后续游戏,此逃生舱已失效"; + if (Main().Alive_() > 1) { + Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 + sender << ",剩余玩家捕捉目标顺位发生变更!\n"; + sender << Markdown(Main().board.GetPlayerTable(Main().round_)); + } + } + // 隐匿状态公屏提示 + if (hide) { + Global().Boardcast() << At(currentPlayer) << " 已抵达【逃生舱】!" << (GAME_OPTION(大乱斗) ? "被随机传送至地图其他地方" : "不再参与后续游戏") << ",此逃生舱已失效\n" + << Markdown(Main().board.GetPlayerTable(Main().round_)); + } + return true; + } + // [传送门] 仅本回合未传送时触发 + if (grid.Type() == GridType::PORTAL && !this_move_teleport) { + // 进入亚空间 + player.subspace = 2; + // 出口是[单向传送门]则交换 + pair pRelPos = Main().board.grid_map[player.x][player.y].PortalPos(); + Grid& target_grid = Main().board.grid_map[player.x + pRelPos.first][player.y + pRelPos.second]; + if (target_grid.Type() == GridType::ONEWAYPORTAL) { + grid.SetType(GridType::ONEWAYPORTAL); + target_grid.SetType(GridType::PORTAL); + } + } + // [陷阱] + if (grid.Type() == GridType::TRAP) { + grid.TrapTrigger(); + if (grid.TrapStatus()) { + player.NewContentRecord("(陷阱)"); + sender << prefix << GetRandomHint(trap_hints) << "\n\n移动触发【陷阱】,本回合被强制停止行动!"; + return true; + } + } + // [热源] + string step_info, heat_message; + if (Main().board.HeatNotice(player.pid)) { + step_info = "[热浪(第" + to_string(step) + "步)]"; + heat_message = GetRandomHint(heat_wave_hints) + "\n移动进入【热浪范围】,当前位置附近存在热源"; + } + if (grid.Type() == GridType::HEAT) { + if (player.heated) { + player.NewContentRecord("(热源)"); + sender << prefix << GetRandomHint(heat_active_hints) << "\n\n您本局游戏已进入过【热源】,高温难耐,本回合无法继续前进!"; + return true; + } else { + player.heated = true; + step_info = "[热源(第" + to_string(step) + "步)]"; + heat_message = GetRandomHint(heat_core_hints) + "\n移动进入【热源】!请注意,在下一次进入热源时,将公开热源并强制停止行动"; + } + } + if (heat_message != "") { + Global().Tell(player.pid) << step_info << heat_message; + player.private_record += "\n" + step_info; + } + /* ========== Sound ========== */ + Sound sound = Main().board.GetSound(grid, GAME_OPTION(特殊事件) == 3); + if (sound == Sound::SHASHA) { + if (hide) { + sender << prefix << "移动进入【树丛】(隐匿中,不会向其他人发出声响)"; + } else { + player.UpdateSoundRecord(sound); + sender << prefix << GetRandomHint(grass_hints) << "\n移动进入【树丛】,请其他玩家留意私信声响信息!"; + SendSoundMessage(player.x, player.y, sound, false); + player.achievement.trigger_sound = true; + } + } else if (sound == Sound::PAPA) { + if (hide) { + sender << prefix << "移动发出【啪啪声】(隐匿中,不会向其他人发出声响)"; + } else { + player.UpdateSoundRecord(sound); + sender << prefix << GetRandomHint(papa_hints) << "\n移动发出【啪啪声】,请其他玩家留意私信声响信息!"; + SendSoundMessage(player.x, player.y, sound, false); + player.achievement.trigger_sound = true; + } + } + // 非点杀模式检测玩家捕捉(隐匿状态不能捕捉) + if (!GAME_OPTION(点杀)) { + if (PlayerCatch(player, sender)) { + return true; + } + } + // 玩家可继续行动 + return false; +} + +// 捕捉:坐标重合,玩家没有隐匿且未出局 +bool RoundStage::PlayerCatch(Player& player, MsgSenderBase::MsgSenderGuard& sender) +{ + vector list = Main().board.player_map[player.x][player.y]; + PlayerID t = player.target; + + if (find(list.begin(), list.end(), t) == list.end() || hide || player.out != 0) { + return false; + } + + active_stop = false; // 捉捕成功不视为主动停止 + + sender << GetRandomHint(catch_hints) << "\n\n"; + sender << At(t) << " 被捕捉!"; + + if (Main().round_ == 1) { + player.NewContentRecord("(首轮捕捉)", "catch"); + player.NewContentRecord("[传送]", "teleport"); + + Player& target = Main().board.players[t]; + target.NewContentRecord("[首轮被抓传送]", "teleport"); + if (t < currentPlayer) { // 被抓玩家在前面,强制刷新完整赛况 + target.all_record.back() = target.move_record; + } + + Main().board.TeleportPlayer(t); // 随机传送被捉方 + Main().board.TeleportPlayer(player.pid); // 随机传送捕捉方 + player.achievement.catch_first_round = true; // 成就【饥渴难耐】 + sender << "\n【首轮玩家保护】\n首轮捕捉不生效:双方均被随机传送至地图其他地方!"; + return true; + } + + player.NewContentRecord("(捕捉)", "catch"); + Main().board.players[t].out = 1; + Global().Eliminate(t); + player.score.catch_score += 100; // 抓人分 + Main().board.players[t].score.catch_score -= 100; + player.achievement.recordCatch(Global().PlayerNum()); // 成就[嗜杀成性]辅助 + if (step == 1) player.achievement.catch_without_moving = true; // 成就【守株待兔】 + + if (Main().Alive_() > 1) { + player.NewContentRecord("[传送]", "teleport"); + Main().board.TeleportPlayer(player.pid); // 随机传送捕捉方 + Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 + sender << "\n" << At(player.pid) << " 被随机传送至地图其他地方,捕捉目标顺位发生变更!\n"; + sender << Markdown(Main().board.GetPlayerTable(Main().round_)); + } + + if (Main().Alive_() == 1) { + // 无逃生舱最后生还判定 + if (Main().board.ExitCount() == 0) Main().withoutE_win_ = true; + // 有逃生舱但死斗取胜判定 + if (Main().board.ExitCount() > 0) Main().withE_win_ = true; + } + return true; +} + +// 私信其他玩家发送声响信息 +void RoundStage::SendSoundMessage(const int fromX, const int fromY, const Sound sound, const bool to_all) +{ + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if ((pid != currentPlayer || to_all) && Main().board.players[pid].out == 0) { + string direction = Main().board.GetSoundDirection(fromX, fromY, Main().board.players[pid]); + string step_info = "[" + to_string(currentPlayer) + "号(第" + to_string(step) + "步)]"; + string sound_message; + if (direction == "同格") { + if (Main().board.players[currentPlayer].target != pid || GAME_OPTION(点杀)) { + switch (sound) { + case Sound::SHASHA: sound_message = step_info + "\n" + GetRandomHint(grass_sound_hints); break; + case Sound::PAPA: sound_message = step_info + "\n" + GetRandomHint(papa_sound_hints); break; + default: sound_message = "[错误] 未知声音类型:相同格子的未知声音"; + } + } + } else { + switch (sound) { + case Sound::SHASHA: sound_message = step_info + "你听见了来自【" + direction + "方】的沙沙声!"; break; + case Sound::PAPA: sound_message = step_info + "你听见了来自【" + direction + "方】的啪啪声!"; break; + case Sound::BOSS: sound_message = "[BOSS-米诺陶斯] 你听见了来自【" + direction + "方】的巨大响声!"; break; + default: sound_message = "[错误] 未知声音类型:不同格子来自【" + direction + "方】的未知声音"; + } + } + if (sound == Sound::BOSS) { + Main().board.boss.AddSoundPropagation(direction); + } else { + Main().board.players[currentPlayer].AddSoundPropagation(direction); + } + Global().Tell(pid) << sound_message; + } else { + Main().board.players[currentPlayer].AddSoundPropagation("错误"); + } + } +} + +// [BOSS-米诺陶斯] 行动 +void RoundStage::HandleMinotaurBossAction(Boss& boss, string& boss_record, MsgSenderBase::MsgSenderGuard& sender) +{ + if (boss.BossChangeTarget(false)) { + // 更换目标,重置步数 + boss_record += "发现更近的目标,变更目标至 [" + to_string(boss.target) + "号]"; + sender << "\n[BOSS-米诺陶斯] 发现了距离更近的玩家,变更锁定目标至 " << At(boss.target); + } else { + // 未更换目标,执行移动 + if (boss.BossMove()) { + // 抓住玩家 + for (const auto pid: Main().board.player_map[boss.x][boss.y]) { + Player& catched_player = Main().board.players[pid]; + if (catched_player.out > 0) continue; + catched_player.NewContentRecord("(BOSS捕捉出局)", "end"); + catched_player.all_record.back() = catched_player.move_record; // 回合已经结束,需强制更新完整赛况 + catched_player.out = 1; + if (Global().PlayerNum() > 1) Global().Eliminate(pid); + catched_player.score.catch_score -= 100; // 抓人分 + boss_record += "[" + to_string(pid) + "号] "; + sender << "\n" << At(pid); + } + boss_record += "被BOSS捕捉出局!"; + sender << "\n被BOSS捕捉出局!"; + if (Main().Alive_() > 1) { + boss.BossChangeTarget(true); // 重置锁定目标 + Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 + boss_record += "变更目标至 [" + to_string(boss.target) + "号]"; + sender << "\n\nBOSS更换锁定目标至 " << At(boss.target) << ",同时玩家捕捉目标顺位发生变更!\n"; + sender << Markdown(Main().board.GetPlayerTable(Main().round_)); + } + } else { + // 未抓住玩家 + boss_record += "向 [" + to_string(boss.target) + "号] 移动了 " + to_string(boss.steps) + " 步"; + sender << "\n[BOSS-米诺陶斯] 向 " << At(boss.target) << " 移动了 " << boss.steps << " 步"; + if (boss.steps == 4) Main().board.players[boss.target].achievement.boss_chase_four_steps = true; // 成就【牛头魅魔】 + } + // BOSS移动后发出巨响 + if (Main().Alive_() > 1 || (Global().PlayerNum() == 1 && Main().board.players[0].out == 0)) { + boss.UpdateSoundRecord(Sound::BOSS); + sender << "\n\nBOSS发出震耳欲聋的巨响!请所有玩家留意私信声响信息!"; + SendSoundMessage(boss.x, boss.y, Sound::BOSS, true); + } + } + // BOSS周围8格内获得喘息提示 + for (auto& player : Main().board.players) { + if (boss.IsBossNearby(player) && player.out == 0) { + player.private_record += "\n[BOSS-米诺陶斯] 你听到来自BOSS沉重的喘息声!"; + Global().Tell(player.pid) << "「呼……呼……」你听到来自[米诺陶斯]沉重的喘息声!"; + } + } +} + +// [BOSS-邦邦] 行动 +void RoundStage::HandleBangBangBossAction(Boss& boss, string& boss_record, MsgSenderBase::MsgSenderGuard& sender) +{ + // 更新目标 + if (boss.BossChangeTarget(false)) { + boss_record += "变更目标至 [" + to_string(boss.target) + "号],"; + sender << "\nBOSS发现了距离更近的玩家,变更锁定目标至 " << At(boss.target); + } + // 每回合一定移动 + boss_record += "BOSS 移动中..."; + sender << "\n[BOSS-邦邦] 移动中..."; + if (boss.BossMove()) { + // 到达玩家位置(不会捕捉) + boss_record += "追上了玩家 [" + to_string(boss.target) + "号],"; + sender << "\n【邦邦】追到你了 [" + to_string(boss.target) + "号]!你说邦邦不邦邦!"; + boss.BossChangeTarget(true); // 重置锁定目标 + boss_record += "变更目标至 [" + to_string(boss.target) + "号],"; + sender << "\nBOSS抵达目标位置,更换新目标 " << At(boss.target); + } + // 放置炸弹 + Grid& grid = Main().board.grid_map[boss.x][boss.y]; + if (grid.Attach() == AttachType::EMPTY) grid.SetAttach(AttachType::BOMB); + // 公屏展示炸弹墙壁信息 + auto [info, md] = Main().board.GetBangBangSurroundingWalls(boss.x, boss.y); + string wall_info = "BOSS所在位置的四周墙壁信息,按照 上下左右 顺序分别是:\n" + info; + boss_record += "放置炸弹(" + info + ")"; + sender << "\n\n【邦邦】哈哈,炸弹来喽~" + << "\n" << wall_info + << "\n" << Markdown(md, (GRID_SIZE + WALL_SIZE * 2) + 40); +} + + auto* MakeMainStage(MainStageFactory factory) { return factory.Create(); } } // namespace GAME_MODULE_NAME diff --git a/games/long_night/options.h b/games/long_night/options.h index ffbd890..2b0665a 100644 --- a/games/long_night/options.h +++ b/games/long_night/options.h @@ -1,15 +1,28 @@ -EXTEND_OPTION("每局游戏先根据模式从区块池抽取12+4组成随机池,再抽取地图区块
" +EXTEND_OPTION("[区块] 先根据模式从区块池抽取 12+4 组成随机池,再抽取地图区块
" "【标准】仅从**经典区块**中抽取区块   【幻变】将从**轮换区块**中随机抽取
" "【狂野】将从**所有区块**中随机抽取   【疯狂】随机池包含2个**特殊区块**", 模式, (AlterChecker({{"标准", 0}, {"狂野", 1}, {"幻变", 2}, {"疯狂", 3}})), 2) -EXTEND_OPTION("捉捕目标:设置游戏中玩家的捉捕顺序", 捉捕目标, (BoolChecker("下家", "上家")), true) -EXTEND_OPTION("设置游戏地图边长", 边长, (AlterChecker({{"9", 9}, {"10", 10}, {"12", 12}})), 9) -EXTEND_OPTION("点杀模式:捕捉仅在回合结束时触发,路过不会触发捕捉", 点杀, (BoolChecker("开启", "关闭")), false) -EXTEND_OPTION("隐匿模式:隐匿后私聊行动,可选回合和单步模式", 隐匿, (AlterChecker({{"关闭", 0}, {"回合", 1}, {"单步", 2}})), 0) -EXTEND_OPTION("大乱斗模式:逃生舱改为随机传送", 大乱斗, (BoolChecker("开启", "关闭")), false) -EXTEND_OPTION("在游戏中生成BOSS牛头怪米诺陶斯,在回合结束时追击玩家", BOSS, (BoolChecker("开启", "关闭")), false) -EXTEND_OPTION("设置游戏特殊事件", 特殊事件, (AlterChecker({ +EXTEND_OPTION("[区块] 自定义游戏区块随机池:自定义区块时「模式」配置不生效", 区块, + (RepeatableChecker>("区块", "1 5 6 16 34 38 E1 e7 S4 s1")), (std::vector{"默认"})) + +EXTEND_OPTION("[常规] 设置游戏地图边长", 边长, (AlterChecker({{"9", 9}, {"10", 10}, {"12", 12}})), 9) +EXTEND_OPTION("[常规] 游戏最大回合数限制", 回合数, (ArithChecker(3, 40, "回合数")), 20) +EXTEND_OPTION("[常规] 捉捕目标:设置游戏中玩家的捉捕顺序", 捉捕目标, (BoolChecker("下家", "上家")), true) +EXTEND_OPTION("[常规] 停止私信:玩家主动停止或超时,得知私信墙壁信息", 停止私信, (BoolChecker("开启", "关闭")), false) + +EXTEND_OPTION("[事件] 设置游戏特殊事件", 特殊事件, (AlterChecker({ {"随机", -1}, {"无", 0}, {"怠惰的园丁", 1}, {"营养过剩", 2}, {"雨天小故事", 3} })), 0) -EXTEND_OPTION("行动前思考的时间限制", 思考时限, (ArithChecker(30, 3600, "超时时间(秒)")), 180) -EXTEND_OPTION("开始行动后的总时间限制", 行动时限, (ArithChecker(60, 3600, "超时时间(秒)")), 360) + +EXTEND_OPTION("[模式] BOSS:具体规则详见「#规则 漫漫长夜 BOSS」", BOSS, + (AlterChecker({{"无", 0}, {"米诺陶斯", 1}, {"邦邦", 2}})), 0) +EXTEND_OPTION("[模式] 点杀:捕捉改为仅在回合结束时触发,路过不会触发捕捉", 点杀, (BoolChecker("开启", "关闭")), true) +EXTEND_OPTION("[模式] 隐匿:隐匿后私聊行动,可选回合和单步模式", 隐匿, (AlterChecker({{"关闭", 0}, {"回合", 1}, {"单步", 2}})), 0) +EXTEND_OPTION("[模式] 大乱斗:逃生舱改为随机传送", 大乱斗, (BoolChecker("开启", "关闭")), false) +EXTEND_OPTION("[模式] 谋定后动:每回合仅能执行一次移动,可使用多步行动指令", 谋定后动, (BoolChecker("开启", "关闭")), false) +EXTEND_OPTION("[模式] 炸弹人:公开安置炸弹,任何人经过炸弹并离开会引爆炸弹并出局", 炸弹, (ArithChecker(0, 3, "数量")), 0) + +EXTEND_OPTION("[时限] 行动前思考的时间限制", 思考时限, (ArithChecker(30, 3600, "超时时间(秒)")), 120) +EXTEND_OPTION("[时限] 开始行动后的总时间限制", 行动时限, (ArithChecker(60, 3600, "超时时间(秒)")), 300) + +EXTEND_OPTION("[图片] 设置游戏使用的图片素材&纹理", 纹理, (AlterChecker({{"经典", 0}, {"复古", 1}})), 0) diff --git a/games/long_night/player.h b/games/long_night/player.h new file mode 100644 index 0000000..6d80a9a --- /dev/null +++ b/games/long_night/player.h @@ -0,0 +1,468 @@ + +class Score +{ + public: + Score(const int size) + { + explore_map.resize(size); + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + explore_map[i].push_back(0); + } + } + } + // 抓人分 + int catch_score = 0; + // 逃生分 + static constexpr const int exit_order[8] = {150, 200, 250, 250, 250, 250, 250, 250}; + int exit_score = 0; + // 探索分 + vector> explore_map; + // 退出惩罚 + int quit_score = 0; + + pair ExploreCount() const + { + int count1 = 0, count2 = 0; + for (const auto &row : explore_map) { + count1 += count(row.begin(), row.end(), 1); + count2 += count(row.begin(), row.end(), 2); + } + return make_pair(count1, count2); + } + + static string ScoreInfo() { return score_rule; } + + int FinalScore() const { return catch_score + exit_score + ExploreScore() + quit_score; } + + private: + int ExploreScore() const + { + auto [c1, c2] = ExploreCount(); + return c2 * 2 + c1 * 1; + } + + static constexpr const char* score_rule = R"( + 【抓人分】
+抓人+100,被抓-100
+ 【逃生分】
+第1/2/3/4个逃生+150/200/250/250
+ 【探索分】
+每探索一个自己未探索的格子+1
+每探索一个所有玩家未探索的格子额外+1
+【退出惩罚】强制退出-300分
)"; +}; + + +class PlayerAchievement +{ + public: + // 【悄无声息】 + bool exit_without_sound = false; + // 【环游世界】 + bool explore_all_map = false; + // 【游荡幽灵】 + bool explore_all_map_silently() const { return explore_all_map && !trigger_sound; } + // 【我赶时间】 + bool exit_first_round = false; + // 【饥渴难耐】 + bool catch_first_round = false; + // 【乒铃乓啷】 + bool visit_five_grid_type() const { return uniqueTotalCount() >= 5; } + // 【嗜杀成性】 + bool catch_everyone_4p = false; + // 【守株待兔】 + bool catch_without_moving = false; + // 【牛头魅魔】 + bool boss_chase_four_steps = false; + + PlayerAchievement(const vector>& pos): blocks(move(pos)) + { + InitializeBlockChecker(); + } + + // [悄无声息]辅助:触发声音 + bool trigger_sound = false; + + // [嗜杀成性]辅助 + void recordCatch(const int playerNum) { + if (++catch_count == playerNum - 1 && playerNum >= 4) { + catch_everyone_4p = true; // 成就【嗜杀成性】 + } + } + + // [乒铃乓啷]辅助 + void visitGrid(GridType g) { + // 单向传送门视为普通传送门 + if (g == GridType::ONEWAYPORTAL) { + grids.insert(GridType::PORTAL); + return; + } + if (g != GridType::EMPTY) grids.insert(g); + } + void visitAttach(AttachType a) { + if (a != AttachType::EMPTY) attaches.insert(a); + } + int uniqueGridCount() const { return grids.size(); } + int uniqueAttachCount() const { return attaches.size(); } + int uniqueTotalCount() const { return grids.size() + attaches.size(); } + + // [环游世界]辅助:初始化区块检查器 + void InitializeBlockChecker() + { + sort(blocks.begin(), blocks.end()); + total = blocks.size(); + visited.assign(total, false); + count = 0; + } + + // [环游世界]辅助:每步移动时检查走过的区块 + void MakeStep(int x, int y) + { + if (explore_all_map) return; + + for (int dx = 0; dx < 3; ++dx) { + for (int dy = 0; dy < 3; ++dy) { + pair cand = {x - dx, y - dy}; + auto it = lower_bound(blocks.begin(), blocks.end(), cand); + if (it != blocks.end() && *it == cand) { + int idx = distance(blocks.begin(), it); + if (!visited[idx] && ++count == total) + explore_all_map = true; // 成就【环游世界】 + visited[idx] = true; + return; + } + } + } + } + private: + // [乒铃乓啷]辅助 + template + struct EnumHash { + std::size_t operator()(E e) const noexcept { + return std::hash::type>()( + static_cast::type>(e)); + } + }; + std::unordered_set> grids; + std::unordered_set> attaches; + // [环游世界]辅助 + vector> blocks; + vector visited; + int total = 0; + int count = 0; + // [嗜杀成性]辅助 + int catch_count = 0; +}; + + +struct Move { + int direct; + Sound sound; + vector propagation; // 声音向其他所有玩家的传播方向(私信完整赛况) + pair content; // true(移动失败结束回合)/ false(移动成功结束回合) + string style; + + Move(int direct, Sound sound, pair content) + : direct(direct), sound(sound), content(content) {} + Move(Sound sound, pair content, string style) + : direct(-1), sound(sound), content(content), style(style) {} +}; + +class RoundMove +{ + public: + vector round_move; + + void push_back(const Move& mv) { round_move.push_back(mv); } + bool empty() const { return round_move.empty(); } + Move& back() { return round_move.back(); } + void clear() { round_move.clear(); } + size_t size() const { return round_move.size(); } + Move& operator[](size_t idx) { return round_move[idx]; } + + string GetMoveRecord(const int query_pid, const bool is_html = false) const + { + string result; + + if (is_html) result += MoveRecordStyle(); + + const size_t N = round_move.size(); + size_t i = 0; + + while (i < N) { + const Move& mv = round_move[i]; + + bool mergeable = (mv.direct >= 0 && mv.sound == Sound::NONE && mv.content.first.empty()); + if (mergeable) { + size_t j = i, count = 0; + while (j < N) { + const Move& next = round_move[j]; + if (next.direct == mv.direct && next.sound == Sound::NONE && next.content.first.empty()) { + count++; j++; + } else break; + } + if (count >= 3) { + if (is_html) { + result += "" + dirSymbol(mv.direct) + "*" + to_string(count) + ""; + } else { + result += dirSymbol(mv.direct) + "*" + to_string(count); + } + i = j; + continue; + } + } + result += formatSingle(mv, query_pid, is_html); + i++; + } + return result; + } + + private: + static string formatSingle(const Move& mv, const int query_pid, const bool is_html) + { + string d = dirSymbol(mv.direct); + + // 查询pid非-1,获取私信完整赛况声响方向 + string sound_d; + if (mv.sound != Sound::NONE && query_pid != -1) { + if (query_pid < mv.propagation.size()) { + sound_d = "(" + mv.propagation[query_pid] + ")"; + } else { + sound_d = "{越界异常[pid=" + to_string(query_pid) + "]}"; + } + } + + if (mv.sound == Sound::SHASHA) { // [沙沙] + return is_html ? "" + d + "沙沙" + sound_d + "" : "[" + d + "沙沙" + sound_d + "]"; + } + else if (mv.sound == Sound::PAPA) { // [啪啪] + return is_html ? "" + d + "啪啪" + sound_d + "" : "[" + d + "啪啪" + sound_d + "]"; + } + else if (!mv.content.first.empty()) { + if (is_html) { + return mv.content.second + ? "(" + d + mv.content.first + ")" // 撞墙 + : "" + mv.content.first + ""; // 回合结束 + } + return mv.content.second ? "(" + d + mv.content.first + ")" : mv.content.first; + } + else { // 普通移动 + return is_html ? "" + d + "" : d; + } + } + + static string dirSymbol(int d) + { + switch (d) { + case 0: return "↑"; + case 1: return "↓"; + case 2: return "←"; + case 3: return "→"; + default: return ""; + } + } + + static string MoveRecordStyle(); +}; + + +class Player +{ + public: + Player(const PlayerID pid, const string &name, const string &avatar, const int size, const vector>& pos) + : pid(pid), name(name), avatar(avatar), score(size), achievement(pos) {} + + // 玩家信息 + const PlayerID pid; // 玩家ID + const string name; // 玩家名字 + const string avatar; // 玩家头像 + // 出局(1被抓 2出口) + int out = 0; + // 当前坐标 + int x, y; + // 抓捕目标 + PlayerID target; + // 移动相关 + RoundMove move_record; // 当前回合完整记录 + vector all_record; // 历史回合完整记录 + int subspace = -1; // 亚空间剩余步数 + string private_record; // 当前回合私信记录 + int hide_remaining = 0; // 隐匿剩余次数 + bool inHeatZone = false; // 在热源区块内 + bool heated = false; // 两次烫伤强制停止 + // 挂机状态(等待时间缩减) + bool hook_status = false; + // 加时卡 + int extra_time_card = EXTRATIMECRAD_COUNT; + // 炸弹 + int bomb = 0; // 剩余数量 + bool bomb_trigger = false; // 炸弹触发状态 + + // 玩家分数 + Score score; + // 玩家成就 + PlayerAchievement achievement; + + void NewStepRecord(const Direct direct, const string& end = "") { move_record.push_back({static_cast(direct), Sound::NONE, {end, true}}); } + void NewContentRecord(const string& content, const string& style= "end") { move_record.push_back({Sound::NONE, {content, false}, style}); } + void ClearMoveRecord() { move_record.clear(); } + + void UpdateSoundRecord(const Sound sound) { if (!move_record.empty()) move_record.back().sound = sound; } + void AddSoundPropagation(const string& direct_str) { if (!move_record.empty()) move_record.back().propagation.push_back(direct_str); } + void UpdateEndRecord(const string& content) { if (!move_record.empty()) move_record.back().content = {content, true}; } + + string GetAllMoveRecord(const int query_pid, const bool is_html = true) const + { + string record; + for (size_t round = 0; round < all_record.size(); round++) { + if (round > 0) record += is_html ? "
" : "\n"; + if (is_html) { + record += "【第 " + to_string(round + 1) + " 回合】
"; + } else { + record += "【第 " + to_string(round + 1) + " 回合】\n"; + } + record += all_record[round].GetMoveRecord(query_pid, is_html); + } + return record; + } + + bool InSubspace() const { return subspace > 0; } +}; + +string RoundMove::MoveRecordStyle() +{ + return +R"()"; +} diff --git a/games/long_night/resource/box.png b/games/long_night/resource/box.png deleted file mode 100644 index a6b77febaaaedc01009f06dbc4c9aacf1206df00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nETFi#i9kc`H+=NWkqC|$SR5!k9y&O%C40|@mO(1JK=ilZujau&&>Su|L^~P$Nzrc zGg}n$Q0EUlJ_G>3SsEsZpx%)E{=kuXZe8KoLA}P9!U|(=erq zRLK^>YTU>`GaeiY z1RA+ou2iqqs|6a`+#pbx1XBb?ltjQJqrqf>lSH%uUYHu&r_| zh{WItq=-g&Iv9e{JVI--45f)m7&8emGuaH5J){AkMm>TvClH1K;~F&+HK0b+L|P~; zdjxCN;UsR+;s1tuWce2csMN}2BQ}237o%~+goO;YQeg}w@p}8+Yd$n`Ygjt8a&bB`_3Lzf{>_)2C*O>ofSZ5 z@s$t@<^{rB4xPn?S*$^*4A)><+rL8DupK1$1(Z^a1|gCE3f8D$ElwB_DrU@x#G*{I zDV7F~WD*wR2ArS_Q{HjjotKKm3If++25P|)5jr1~28;P@0iREY80-OeWinW5vXF>L zjY=gV8kHXghG}5FTFYT+*nBz<;c@6%4wp?2jL|@J4y9d`3u)La4S#sQ1Xm~6RWQ6? z^MCFSCorlC5yNlhvDc!e{ z3=GmH>QIXGKj>l*X2G>&B0`|^VySHXFVbUD{xj`mI~WS{pQ9XDd#A*Q;Z)zS4@Vjn z^)b@&P!rYX2&$pI*?qAU0Gu+Vl6gv1YS-0s;U3tu_aA$*{^<5z{G>|9s+uJ$*RB`k zjb^XBaB}8~$n3U_SiurQ+~GH4d9sViySmrAwQ)-{j}Te)`~0{GAr4*6%9SAjMGs15 z*Ew?1PJnAF*s{s-ic<-$$C_>nIZsb>@?tys4)yj4qZ%sS>VbrERmuIdB@4dh6y`l% zt*)%RA)hsUT(`-%)yV<)LgBM#pLj`G{)-o%W=r>9-r3c@KVs4cSs*JNZHtrjz20*_ zq)JJ5X@045-n?uKPlq|TnO~~1WsUnXgFg53GcAPM z_3rB-qcWL(P*AqK@k*3e&oi%pA7Qv``V_>A^c=s!{VU{YfB(s!@_TQ1YW%#@&+W2p ze2p+BMt$!-EA?E)N7gxI%553HTlWv^-R?GK9GAXBTios;yYN@92``p*oO;{W9J}`U z0&01RYAV0L60NxQ;`9E1>K5RY&@0oshKuD+e*5ay(RS}}+f~n7_3>{4FSKIY_ReX+ z9D&sG&7XOZ&f|#SqDM!M28Xy898iLHbo#oz{qkLp1jil*dS%`V@97cs+)lsrQ`T}@ z7Wud5^-e{Pru*)G=(F?Tp)Yeg1=u1A6ukt2pOf+zTwbtJbu|l{7C%68aW~ml6Ba?(mJ@iA&mIQcl_RO@}TrDMyqSBd%DIauk;Qq^SI0&!s2iRO#Mm^R8os zj}H{?+UnN0{c5{@ed%#sb-L0Ij?KAgeHvujaK9$uz>0{H^-atp&Zmnb7oV-I5{ik# z*PAk@V09nODp*u5N^US!mA-bM4tiVJ=*v)pV#8xUhu@#={C3OB?v};b(8sNNrf%Bg z;VHbcH0MaBUtYrYlpm)SnHbGW9o{-?LTVLV7a7xe)NW)0cc3<6951J diff --git a/games/long_night/resource/classic/bomb.png b/games/long_night/resource/classic/bomb.png new file mode 100644 index 0000000000000000000000000000000000000000..d9c2e35b9658c99ac9af160c2b10e62999f3ba48 GIT binary patch literal 1950 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 bhTx-F*2RUiBAu|y`0?Q3<|Qm}Sr-30j;8D2 literal 0 HcmV?d00001 diff --git a/games/long_night/resource/empty.png b/games/long_night/resource/classic/empty.png similarity index 100% rename from games/long_night/resource/empty.png rename to games/long_night/resource/classic/empty.png diff --git a/games/long_night/resource/exit.png b/games/long_night/resource/classic/exit.png similarity index 100% rename from games/long_night/resource/exit.png rename to games/long_night/resource/classic/exit.png diff --git a/games/long_night/resource/grass.png b/games/long_night/resource/classic/grass.png similarity index 100% rename from games/long_night/resource/grass.png rename to games/long_night/resource/classic/grass.png diff --git a/games/long_night/resource/heat.png b/games/long_night/resource/classic/heat.png similarity index 100% rename from games/long_night/resource/heat.png rename to games/long_night/resource/classic/heat.png diff --git a/games/long_night/resource/oneway_portal.png b/games/long_night/resource/classic/oneway_portal.png similarity index 100% rename from games/long_night/resource/oneway_portal.png rename to games/long_night/resource/classic/oneway_portal.png diff --git a/games/long_night/resource/portal.png b/games/long_night/resource/classic/portal.png similarity index 100% rename from games/long_night/resource/portal.png rename to games/long_night/resource/classic/portal.png diff --git a/games/long_night/resource/classic/transparent.png b/games/long_night/resource/classic/transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..ad6b2cb061b3531ed0f52636bd603312d4abfca4 GIT binary patch literal 1484 zcmbVMJ8aZQ81@NqIzW+-xB?+`8k`0s{M_{>p2I|mH?S+=5G^YbB#O+A$Gg6)J;t77 zH=#he3M43K5vOZtXimBEl;WU)l7=2CI)p@n=a}VEW-}i@{RsbGW^Vrt)1jnxEt8}p$MW%m^x)xfNji8kY%Yss?-HgwQi;!7 zOzlSrpe5!JxAX@eok;_wtEVZIgqimrmTQw@BvTdJ5 zrfy7Y`m|=06r+O6IyOx-`ecxn`T=fKFN|ct&XGGpBv{jWy`I`DsXT3KhHcxLZfd5f z07A(&VnO;!oK21ysw|^vn23$1&SC=R%L}X%q9n2UxWM6dF z+(?+#U>RRaDVtvhH79dx66~go2%a`M?~VhtI93_CZXOsLon8uKpZBseyWX%W5zLWI z-Lw?Lgs3%*5-!_Vw-nvNx;}(@+z*3|{h*eP_3|DlWQ|V*`6}3_IN)hSz~(R_ZKfr0 zTSkQ^vCF$W1;L^Ebr(|}gk8AE8W-nKecr7YcBP`2sxeU4^Kd=R1c@oDR~;Gr zqlTf6%PqsODy3EJa&Wh+#K&;x8~L(mSg`wrl3>PVnaD(_6p3{&~{6@Y~&&#~&LF`si8h-u)9_ bJ$NuN`Og9Je&x?U@_t*NU#R{yxAOack>}HX literal 0 HcmV?d00001 diff --git a/games/long_night/resource/trap.png b/games/long_night/resource/classic/trap.png similarity index 100% rename from games/long_night/resource/trap.png rename to games/long_night/resource/classic/trap.png diff --git a/games/long_night/resource/unknown.png b/games/long_night/resource/classic/unknown.png similarity index 100% rename from games/long_night/resource/unknown.png rename to games/long_night/resource/classic/unknown.png diff --git a/games/long_night/resource/classic/walls/corner.png b/games/long_night/resource/classic/walls/corner.png new file mode 100644 index 0000000000000000000000000000000000000000..c75a3ef5670ad8d9d663d850c1a8de8b962ec890 GIT binary patch literal 1411 zcmbVMy>HV%6n8-tP}GT~LJTK&0SSMd6SqFqRIL-5NVr64q=v26^Chur`;2{Ql8)UF zVq`%`EX)k73{3npEc^@H<-=-G`Kl%Rp6`C|_df2u_o}tOx4LqFMNyR1W~0^?tCUx6 z-w@v$t?qZR+{qd*3PrhlPrggayZ38~viutuREzsCg zr@TnWa@$bB6v9tDbw?Tm-Jk`0mQzsG9gSF(D%fy!%dso2`xrpe+BD2f!?HEYK@}50 z2}z!%`&c|xv47oAB)jW#XVFekKrD%@O` z(PYTaDdsHiFcvQ^sOapT(S2XLv_9#T{{Ca+R=at(m&M23|6onAK{$t^hLJ*rSuHpzaiUp7K-s} zl+&{dExv$@xgo`JV2Ph{MAYdf8_x8!JT`QuGx$x)WRz5Z}Z Oj>%?yzxHYO==>+j%B%?h literal 0 HcmV?d00001 diff --git a/games/long_night/resource/classic/walls/door_col.png b/games/long_night/resource/classic/walls/door_col.png new file mode 100644 index 0000000000000000000000000000000000000000..aa0df42ca8584cd18add9486cf090d4ce8e582b8 GIT binary patch literal 1428 zcmbVM%Wl&^6m=_Af})BA8-x%Vxf`hPW1KjNM>SRJgeDR$Q5vaX*NHtzMzuZ0o-|2? zgv1IKEZHEIED;iG7D)UAD}I0tzrZ*TtA+BaEqh+)p8J@4?~CT%?%L{&RY{W88ue;R zjM?0{v@HJLCSNzia5=6&N+s#awd`AxUcb36Nh>#LyTd!~1LUz##Xjp2bsWY5ElHbO z;~0BKgo7^W)5w;;fBYl^>f7>x>FRFmkO8fqBxL_&ukD>2d6qA4-2}IT2ee%G(p)?VX08Gth>O_VMWs`8Z0QV zgo-*c3^0FWp)K)ys8zi`*CkfAJm5S=nl>7Z)KNiYNneAOWof#h8HOSdN;-)+9xGA0 zzF?@5)Jtg0DT_eHh`VgaZCO}4b0Lg#+9;ieNd!zA<5+{Lp1Cvy`d*HUhe6K)7u8&t3>+uB7z^A!ppnl;>77$Dq>4GQWkWYi1sX~LwhbK> z%BW~6x`}js26dTFdy`+G5J8Accm^u6#>X8073_Pc$C3~WpJ|BuM2n-o4Dv`KhXpJV ziiLHB)B6p_X(p^k196bHc6UId=9HjSDk+8vr{=mYYD6i=kw+R;TNd$ADfN+Ggs@kJ zmSPfUDu!MpO4rkM1;R?ts+4`pD&U2Dm3hM~3l{SJf66~hsHj35T$m@T%`AOTou=Yq zC-dsrCy(c|fP!hpAnaw8V9Q>X0>qc++w{T_n9+^~M3DX=FJ`cm_4o)UWVAs(H{e+s`ntY=eV5H{?R va1-s*k_oj~RS@!U#i78~=6FSBW@d&1jx3y?{)RpPYGUwo^>bP0l+XkKGoczf literal 0 HcmV?d00001 diff --git a/games/long_night/resource/classic/walls/dooropen_row.png b/games/long_night/resource/classic/walls/dooropen_row.png new file mode 100644 index 0000000000000000000000000000000000000000..eb7de4b08bbff24e5e1777031aefd2ef42c64ae8 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^MnKHP!3HG1iSZT!DQ!;|#}JF&Ygr+As(G?Pc39*VBk2s;n08k tesMLy-g7FPt+_&Oi!WxFNd0$zz)%s+lECCXKLe2lwQH)W@~igzzTOZX^F2V5||fMJ^4K0J1a9YGsBHfEK{>`f87L{z~JfX K=d#Wzp$PyI~*fD!agpOGE(MSIU`TO28CVv;)zzo#E;OXk;vd$@?2>@bv9M%8; literal 0 HcmV?d00001 diff --git a/games/long_night/resource/classic/walls/wall_row.png b/games/long_night/resource/classic/walls/wall_row.png new file mode 100644 index 0000000000000000000000000000000000000000..3ed98c0d590cc2a2b8bfa3f2ee317c52c82bc4ac GIT binary patch literal 97 zcmeAS@N?(olHy`uVBq!ia0vp^MnKHP!3HG1iSZT!DGg5-#}JF&x91so85lSYZ)mJP vpYXUcj9vDD#s*%_9u}iX$2_Ez0~^>?85o6szL|0csE5JR)z4*}Q$iB}9}*eB literal 0 HcmV?d00001 diff --git a/games/long_night/resource/water.png b/games/long_night/resource/classic/water.png similarity index 100% rename from games/long_night/resource/water.png rename to games/long_night/resource/classic/water.png diff --git a/games/long_night/resource/retro/bomb.png b/games/long_night/resource/retro/bomb.png new file mode 100644 index 0000000000000000000000000000000000000000..c716858664f2439c4e7ce8dfa9969c4119627a5b GIT binary patch literal 2214 zcmbVO4NMbf819%lhO*#f8{LdM53@ns^?pjRccU72u#X*QjU&MZ#QnD{e9GaQ2Wx3DD+oFbXdp@RFaw2e;uiB0bAyYHU& zdEfVW-|tOVW@S!~jCeIdr_)7dq}y`flh>~BQSdHx)j8nfc_}?#(dkBy)2?T9YuCl- zbR+i(jyyHbK9k`@w}Iuw0$}jDC5YDPl2UvU%M}3?DFB6n$Ab3mYeEr$x1e)OcET=M zflElQkU?%mrh}^};%FXCNk)=<3?y&^l|_8+VvoZ3EU2HCfop9WM-e|nEwZ3fHHS!^ zJqxjlGC*hpiE#u$B2(ZhA(1lE^jio;P)3|c#3>U-5ez{xCKD3)q0pVoJDD8Yw16}C zW-)g zi4UO;eZJ2D)Yfhf`4~19w>#uQQPav`8UaCuMJtYS3E(+E5ldwbq?N%iW3_A~#wr6= z6=jDg76&Vp6$}}nOa>zoH%IXBqF0H(#{k$^6XLwd+9}DI=#wp5f7A7pX*+PIzoU zVRS}+6m~n4;Zao9!+{K&1%){q1c7IqCW0eKGm8O0kr-)8pfGbHVT9c`g(PTTHW$!z z(B39;rCMc!_B^yFSdyYiBZaXnZ^C$Uf)iuuL=xixXHK9@G|N+5(0-OIz@x<$KQ)hb z;Ix)t(gg(;t2}TbazRO8RV*O>E?`(r>uw9mX=MdG8rT+wQQ*lk7^3yM03>}xxrbni z=v2L|45k)BwI16}_``x%*s@25{$#~_F!W*D?`c90$jZ4`Pa%M_5=XUUaBZjz6$Sr) zz5L(q89+feoNC%KG*976Xd(j-q?X~l-M*#$piUR|afWTG!?*6r`dyjl-ic20#zxGo zaK-&QB08;e@rSN$QPl@lU!%KS*_$S%M=uN;896ySCv~sz`_{I`nMo5~7f*D*eAc(A z;ZpPGXUMLmmbK{ zXxE%N{cHPmeO34M_?!62y6UR=LDLrFq~;a-8wR%DtgM{t-1;STIN&AQVl|v-EK6>( zU~f-3N_E}&W5&+g7Y7!`F)7aD7k*jvX0E5{Zjb4UoFG$MyS2di?)>-MNq2oEb1TDN zTd5ys#A>Tcq>kOQcBL+e8?h{P!S$=wu5*rsZyag-s??cu;cka7>GHhO$CpN~ji0Rr zniXEtQcq{%njb2vEn|Ip3SV#rAzo z{N~*|UF-E@Cu@SmjTm*cI%}XePphoIybsH`6%Sg+#?MPkl!G8!4 B3uXWS literal 0 HcmV?d00001 diff --git a/games/long_night/resource/retro/box.png b/games/long_night/resource/retro/box.png new file mode 100644 index 0000000000000000000000000000000000000000..670e2ce7c1ad838846e9d6797454b0a883058b19 GIT binary patch literal 4282 zcmb_g2UHW;8pZ}vq^StHNC-t;Hl#y{W|1Wbs5FI@B4v^cAp%KEfl#CfkFv598!SZ> zuu@bI!3`o^l(rzn0wSmjECM3EzX>8|-@bk2?0Y$9=H9vU{r~^{<^Fe0qOC1WH%V=m z5)%{KWNt=20*y4$wNV_pvwJVtLW3mR%z-N=CcRm7tr1I3RTdKyk7C%`^Xx4S6R9k3 zU4X`-fVu*2HiQ-v+h-_X15{6t2cv*=29pFI5k7*$7&H?67~TS9!8Qgx7-s$)@Tk9~ zE!E$XYCwYC*3&pvpwSJg7PD{+D%h7tbQMk1?1umM?eD?*u?HzynEe zC~JBM6dHlT*g{#upou6w9Tb6xLVbo>uxJc-zdt~+wh#`7C1Ozj0EKFX2JnFY2&Peq z?ktWs06EO?2IwG?&7{L&t6?M>v%FXw$S}kXyUfDEf@sd<@&G0kG$)hbkk7gd28~Fd z($F*tsE;tf>QNB{91f2#08x4fJOR)H2oyX{4~zLCPi9g1qRf7gr$O?7KIo1Iv3LYP z0QC^=G#rQ^-~k*0N2OBq01%A>aPD8^tvC#5YXM$=S&wMLiAsWK#^6H9@>|&vN5PXT zGcN{gxeAB?RaD(1I8~HZkOp5_X8cJ6{yPsotM&B&A*KH&-JfAxmOIZE;DAPSC|duq zoRI&DJQwi!_tf2SdQh@(dI*fWJ^{Mn2nYfeg-1}(1^@w#qEILV!WY;7Kpks~#uG76 z`Jum*-(R7=5>~1Qz@&pvuSCNCx-CH`q1{&_F`|C~XJI^^dHL;kV&mf!uZn?i|P zUV>V*Xu5hhKo6_O3&@1x#DNYEv$toE&^h(ooNQz(NF8!^j&Ziz>e81wr;i;v9Cn%9 zyjJ@F&cgG;#=?_kt;*!?Ein^Mlq=WCt%c*8$(hYkh~()b>4&3C%q;{A$~|ovYo~#% zN?gsh&6_gL9y$FK#qDb(^<8>fIa^8^CSiZzSr7!v97IruWZa9h)g6%U@~=p9fd+e~hZ# z(th;a%ggJU7ph0}OdltOYera{i%(Rt!#qv6t#L8JLo5vzpBj&!H9pmdeZ9$gW+$#>6Pcq zV1`|B{mkr^keXM!rc|Y&w5TVC$G`12*B`mEM$VIwC2JZXym1bBLU^pK$93tO`d$6u z54vwPUbdWRXMkjqu1x|oB4DxjIf_q`@g_pBi*|CV`dAZ}}aOq1%Q%c1PuHYXmk zzsg(JK*}Qm>UZ+BZ?4N%pJhBYwi*KXj;Q1CW|gth2T52wnngf>2DtlSqy4}KH%ThdN=gdclONBDZn z2P|%+w@gzL?L|4<)g<-gjiHQE;8h!}Dh)mLvQCPhpmgZeP~x)>jI~$aTx;voA_VNe zn!ly$sB&Uxr3=zi2&m==D^Je2H5|ySfV7Yk<@;_nJ<8I8^X1b0gzW_r4mk#qlOONU zgR;I!Eq6>=eB#tB@9XxsbYQ}Dw%6u%XB8=ZLN3Ws7)sq_x?b}K3x4g9W7m6ST5bER=Q_&QZn(Lp1XzJp)4WZwZs zhyt#wj!UM=Ey%wwabkr>N*jhH?Dv0qq+g{Gdnw_el;Wopw@cRa@Y z3JJ0~*LuZBepPtosSrEUP_H-F=Q)#7u`#ET)jmb+50rcnQZ*jLv zTDpFK-;vjl&0AmgqSZpR>UqG8qT|F8Ll@a-_pCdQN=lh8Jkk@T&c#>BBPknW>v~M< zy910|%bqeO^4b+s{fG265oAnnktdH|&nu~uDu2-^%yyc(rDhzl*j8E4OJb`Z@LozV z-fKe6607SfoilJ&d-=eLUXmXCz4N3jW^=l8iA!WyNPRcAJY^Tl%5N@qgUFUuWV{WU zqVUf7Zle#F76961XIax_4jNVwDsmA8zod2duGL<2J}Pu$=1n?nSF)GmOP209IcOB9 zHTJmYXYP52J5A4houG}{lkfN2QjSkllv!v@nN^9CL*9w=+@^UOlgv1$=hUf8jgkDk z7U2z9!qWZ9z0Gb5aWZj-J?GTVMt2vf$%^L)*L8P%JytsT@KDFI(_y%!0Oyg~Q=Nhy zyW#N`H>iohYg%nkYwnJxKg;V=srTA^ah{n))7yJ!s?9rg@q|h9GuDQ0sy~`uoLT53 zDEpeleKj_D*Tp@p^~aI8)=s&|tHHFN&fI2I)eZ53!Y&5H9&gZx!!i7~kH}=r$gag6 zoqNZ>E-V?;apBH1Ht`wWIUnb_PoK^wPlhh|Upr>hZYq6tw9Ekn9KD6E)~YrcLG#(u zYhu#_4hDOLP4F|?Coqw|O~##aOl8aQho>H&hRW)BNxjL`e(7SBBbaCMbnabkE!0@e zP2hsvR+3j^L9#*N>v^y`Xpzu(f+EnG8q3*U)2ndO>p{}bhH0Q~sgZp1Q3-lwV^dJX zNLBgGU5zkpS=qU1m)yh$KQvy~Ju&zGh~K*%MRVO}2<_#F;XXCng6pb*;Z7CXi9TvO zQ3VO^ioY686#IPTnSG;^lsPz1L0y`__Dh)82i*$Vge=-##nOAKAjlOpFa@pkD#2eR zr0xjPs>|M8Ra_9t=_i*9vo5O}zIy)?=~K3P=NNub=AdBqd`$Flt-IKkh_u(@C%1j? zSM+{3zy7Jev&JR0wMtptsNnB|$B&&aoP09owHv;4<*3{H&Io~Nm(9)x@9*93^ZqDP zYEJW+UCXJ$nEskRSUTazLM_>bPay;v&4^OB%M+WOqq7POSBwr zNF1B=5fbQBWBbU7=DLg2j4pn8}31WFRUK5sCpSD4<((GBc1El8H$$fFNBK zP>(+d2i@}}dE{k#CU)Px(0&bQ%-*5v2O?aQX-hp=H+CtEH zTUAJyF-*RQpjeG7NwW&ZHCCeoXbAF{7po&FHDiPoOq5D1<@MDx@L&}!<%J66m|W-0 zC{@1kdL}qNAcTrnQxcjtZ!YW+O8^5J#z?}knrN+oh?Vlpb_p=%rcoYjh8WdSo+sxJ z4wDDL-mIR1C9Zsg!Z1EO2aMcAxL6{Y4dWOtKrs=D3lSV6Fg_s^!oxovaHppuiN!LX zVQ1h;%2OJRIs!#aCX=g4;L7TwP`*SWK`|V~aReX`L!8z~#v)pS(})EbW1#dZol(VV zVa_6{U}KC@9-ulDgGM)MR%;l}6UZ1EOX^U*E5^lS2GZ0hP8Xw(Han*&l!<0EjMiuX zSpFzhr(}(+LCLcU`Lum*%NoRF8I4Iyzl23^b;*cd&< zELa0*I&snH2yZ<@8d-e^%SMk>Drkf}tSv_zJ*a^%mFLL^ck3?q^tLLg*>ltki4q(XrU zBx1gVqNtJefqE5aEi(G8?SKyWPlO{@JV8VTP&&hKe|V8m!=q@`TrSKvrRjlk%>Wj` zL!%)>=<7Y2B}(|JI0kXUeuJ6SqYPlPDhZO}x=qTXIGHgtZ+Keul8U@pMu*IrlngNX z8u<;u3~Z#)MCuvOC{VpWlsEb+@&!}MP1tg!waD79$FwSS;j= zFvbn%kA(g^>YyF?gq!Gv&UqW^!)c|Iq&A8HdpF8^`(WOzGyb~{FYWFBUod8o4k--% zz4*-UUfA77lnjmxZa8{afrruKj?scV>A_*9c3I^CL1T;jWS$|hxjnfZYWpRV8yie( zGJDgUef%rWjcr+0Xffq|S;2R)Hx^64md1`&3@dlW#ZPuuWbM z_dzoE_fo7az0$@ydM9sl^gOW*rhVVVXIR+1(X>V4u4mubb+@g<>#tT01`9qIb3Dq| zva~p`@kYm5qI+{)=qdLtc#3tIU1#Qn5Brm53OwUilgc9jc^=REXY4!osPDVWhwfEh z&*{6{m8#k8QZ~)^qfH#0CTjoVOB3f&dG-9CQ#57G5nhw{yghp^e4mt#8k?uWK1l2I z+;w)){nP15ZG&Y?ZdKg5^;l-d#o<(u8ACn|@4j=ocv5@4+dx(&dSll1Wao=toj5R< z8l#>%{rmj((DqGVRhq{Cxx}YbP?s_d`h4MvdGwPrk}h@p-DS7B{LPk*s+$PPr>Chy zij(b^nv>K$5)Zb=>LT{A(~^A<{h%p39fHzVz6;&ATDNZ??HUo?Q}J+#+My=#qpY90 z9?KJ3u2wzWE0kAVYC9%Qj5xJu*S-}wfd^#ygU=^G^MfvD^*-y(5f=WP>?=2RF4=Y> z&N}pB|MIoRGFCYi@zCU(KlYt()GV(!bh2%=N_b<#BkKZCql{K1d)TgC$xGC3FFY{Q zX4ldAOC1ZAmW_cn2QPbA@BOeJB|s5x{xl}Lyx2xA#K|uj&A^n9N ztzUGnEW=Z5EWLbs8;Wa6%Gl=*J>BojoNo!O)Vl_|{>v+#JS!f~IW7W22&|`~Z z`Xo)c1yr@O_RFPtwm!LqN9>=s%R!zlS%czG!>+{6DkidjARf+h<{Z z2?yt1-H0SMA6M8qs<%6G%BsyNaZh#Y>OS2Yc%t6^*U6PT`wj)~&-rD4(!5X8I04>& zHa+v}XLXr{iNq6|?Bw7%c-Y1t^mBX@-1CYreAb-4M6$W)V{cBUu}FB5dsorfLVJx% z`1t;xt&5KxIRky(Rl3(S?hEgz&L8yK5~rTs3wJx$efdsH;x~^b>{Jb$Kj%~x-&=cT zTZscCHuj#>41CfYy8pT~v!{s8$Y|B4O#G$gSf1cPk*48XSb6_~5Wa(`ye#9|%$hvI zm9*TD%EkgmF1*}nC7gK|rOZw9E-{@HC^E(a5)G@jkiwj!)kG|Pi|fm9}Z!?)`7JS*){ey3DL&|rTKlw4z_)A zVSVJOD{e886HTn35M2aSS^k1GBjz3%s76$XIbkIo?47?yinI0g40XHjA%7EMD5y^lF9reB~m+$0`XGhR`UADGi{d)#FBjNK@h{u2G|UHLgQ<eL7r&CV+VBpTj zaO5)c;t6Nq%>+9n$xEu$e!pMk*Qf-aU5yb0p+<2vjw1kph?O3R3LqXaWzvF~6=@&u zm3YCUlr2&=pz^s zkwk#SqFAp(kOa{oe2wYo@Ldi-ZY`Fmk11nuyQ3~dDXRj|m`KQ!Xwh2fW!1}AQ7HG( zY*qybGeu_OB^UWvN)milL2yl0Dt|I$C9YFxl#iG29!Btssb8AFnkk7j!Juk-1jP^( zw}PtS7>VlVp+*u#$DtO1;knAYp%`k#a1tlLbr%$9hM^?tTfq!Xa)QrI0m8hSva@Qh z#||r_X(Sg3F2M&JgXlD4QCKWwjz^Rz56$M7O)%iB;(3PD*tD#kkYm^AScGG>Mub37 z8sUrvg4SwSy^R}Z0p^*^eLPrNlMQL?L#zr6p+(;t^jg~=- zsMdz)C>y5L>TDE;MX7%!bb}SgNuW{8I4Qq-34J21w1e{4S+G~C;d>|Z-IO?09i~`I z|35Nnd50WVnEI>Y8yox5ZVC!H)&;g`xfwkiz)SRaVLc#EK5%#}%S?O>g6_-8F)y?R zw)Jn@?SZiwYa^+1Pj!A$@WuBI99v^(KlspGBDp|W;CkVC^K<)-w0*dK0B`NzoiO^$ ztaluzkA5`!6K8t+`SZ8uz!yvUp=;}Bk6a!a@^>2-SL}ZcL6vs6W}%I~3w#sx(cb**|}sI(w$7@K4=sBkLWCCwqd$ zNYAaerfH}D^MgoVOCFybIW=5;VK{jD#Pmq)>XNzFYY&~zpBaifyj8xu_Q5o_>!*D` z_;~PURh1fJooVI2O_b@Ty?J>Tt2TjsiGqddgbI_ zZVjE-d;eB_Q**y%BWF2LeeqKMwGD#$J|`wI5&p`xPmv=1k(6 zivxk?zs=~Xel!yMAMNqZjGG4+SB}ob^^LbyK4YBeAL`BwLm81hLsH%Ha?N&2;tNN& zdw-^A#b4%Pv;}P~XI<^59iNRfm!`dhHT|QpYQ@@DcAk_6o>oPM2R`~Y`2J|SxN7tl z#g8U6b`RXDonLfm<5n)lT)d~RsOMPf1u_!5?p)#WrkSpmp(_;~e`qL!8XD0}*YkF? z&igeNv##+-y?AvH@b-`XF1fe*jWWp4{;TbOjeqY)RUOt{@y=PpBNfG^=2Q9o8+t~1 z@M?$S?P-zNHxE2@7VKjuQrkSGp}73_(xld+`6+lLHve2ykhl>WxhAM zE}Xs!DAwQL&Q^c?S=rKH_|rG@Q@Ssewgr#2KOC~(eB1f*Z&Uk0q2dR3wunueUd#@~ z{r=BS(1Q`Z7L(EPNA^5;QOz3HUL&7 leF;w;?)-1FA`v9|hoSmNbJyb0SLIK+oUA;1<1Vb7n2~BE(3W)ToAq7GUX@DRghyv0qfS@3XUQsFD zOB1P5L}^h(M7j!yv;azzlKd0fzwiI|?)&47pD|9(KIhE6)?91Nwf9M4k6B>1ZIRg` zARw^K3}<{CI1~AYupscwJ0mCroHqO892o)vBI5jGoxrsWDFJ~^QWP5pri1xW41wya zh9^?pNoqm9{s3A)Ku15wA5ZWmF+uJm4~m~Ic)Fqn45AQq!S)*FFmrz+k|zZhOea|f zTi6hSy$NU{SYHpM6NCW__>!1-P>`>W9|IGl3;t#o1C05%ph_0bt?3u>PJ@CY9ky{THTxJ^lv=KyJ;=fBE>QTzq|hxnM9&0)Q}nB;=o> z88)ZRV+(1hTTL^y(n4!MNN6$|f=8&6A?|2*B1D7et_dd-+%-@n)KCAGbPCW~c%T2+4$uMr z9pO(a20%m%pmdBM{b7jr_%ZUK=<&nk(}bglNEjZVNz_7WK~N}wCd!?JfS}=;2sF@h z1Pv5f2g+{-U=ry2=({24-+TNfQ4Eg4XK?C=UuzQW7X#2YRbuc2ez)m@34EE6h~OW$ zDgUe@|EG+8Hyh|l0*wBR{Jz5&R5CLVPbV380M+}q@`nB^@(g^y-&5B_xoc_RHHi?I z29gLNTAtCX20uf0u6zhJ(J^t-~K|5JS5p8aKY|EXkP%is@xZC1d=ukDWH2jqzkY-Ve& zrZWNpqQ}gP4Qzrkrut7M+jM3=nSBvr+u6?)5>xlOW3Xd1KMO{fhh*T2Sh-2&*Pm)fsN26~%4sbbjSU8NPi ztAAI2r%6ged(-`CeK$_UV)e>+=t>~9KX5{Nb<$&T88-W*a>aiwMX$udwct?RBnfQuE&cT)7T=DG?u$>Ij4xx{<7~3l8qAL>G3pI z=K`U(X`fEItex9K^ITMmGB~3o$^E!~;g$ney)8IP`FUQv^`o$Pk)`+jyp*D&A3d=P zQ@Sw&N{e+#;Y`u$(-T?m-{{v!)4u5Gl%3qkxsQ%A7Ib<=!tEC?o6By(tv}G`U%#!4 z$6cZIv${YRz{x42aWws!>3xs6h|ediu!qSKwY1jezJv*CS{yX&y;tTQW?LRD=+v`y z8P=i4oiRrHN8_?;Hfs$onfZj$6cB;lA7ZQ=R;eQXjlL&LrW29~t2fsP4++zf8}Iyi zme${wUN&+~+B0!aV#p<}0d*}8?kBiHPXQ-lWiQ50`?sPCH)GmoO1c*CukRW7qPsWx zh77?j8$57b9_p;>Z4T#@$N3Ldnn-B{pfn43y62N*>n`otz2E(git4d}DkZxDmvivj zFX|tZT!-O5Hqi|i4u(Ch+JWehHA*<67C^gU6WJ$a@|^Q ziq1o-$qn(!;vtBl*a7uYS<~{z8?2HWXLc$avo6|ctp}INon8`cl}7IJ?#g@m+-h{6 zh0@OR2NW+ud?A5E*S0t*U9XFTD(#2U6yK7<&=cXy*e`SX*8vIm$2(qiL;U7(izfbT zU&`b_YDc&0RD7qmCPgFnplu>d`B{OMH;sKJ3BSnAL=`RXn(Qmtv-V>AtgH6IZOcI~ zW7?S!Rn2#kT%lvm70$WXZVOV5_5%9KKux&5phQ_PyEQ|Xw-CtjIdZjCB~pRx8nSk< z^%`2jD%n`-kW&;a-MH<+=+5)?JsRz^@??6Uw4T?Fx_i=AX-~8_jsNK}z0Np96r8_y zP$zzT2X-4S^(wya(=A+%=ukrcCfNxq58~c7&S>dW2t1IjUb)hbooL*y#=xZ6cVtXUW=C`m=H%VtK6Y{QN!$oY;R3_m%Ko%yg(g&NJX{*ySAd#}G* z+)|J39U+%Fo4z;u{%;8s!i(A)p)B92j)959&=!`uA;PQSW<*uVHHi&7o>B5#g74+~ zHRXYE?2uu%K4#ldokw{a@ls$h@K!NTvm6?`J2a`+rQo@S@E$Fme8pfvMi1U)KtE-eOEye>~<@ba84@%Vo9 zQBRfhCw(nS&8=llO|7MosD=wH=)07z1(kH$U06f2N1qSpbr-LdNyvu}Lz4!*YKwA) z-nRdlnP`<)bg6hyBC)I2kulwH;hOzt7Jjt1EPykb+^TYc@TO@0*0H3kkER>TA>p1a zH35w_KD~A!#h&ju#E$mciS0ROy6p_w^WQC@ye0*F9C%I@UEkGX<(Ud}Ld8b_%}Xuo2e zW3!!S91#}zy)%Z+7V~EoEER!O&0u>`X^mUlxV65W%MqnF$pyzIgEzjd;rY%@^=`r= zE)^XGH1`~DXRUC0F5WB8u}WqLJM8v?lRUZdb)fOSKDkopp-;FFO%{}L*fZ0EWDP7T z^fcR46(e)=ZP_TceZ{+&W9!egwhZ*f>rMVC4-F7{?)v1UhDqJTODY;b+1`C|3 z^foimwhj8qej{JL!>B7iRQ_^`a@=*=$0r>ikE00$9`JVY5`(Hv*Le@RzBbS8hl@qM$5xLWQ zxi@VEHZ+vP4xI<#HtiU$9tW1fi}iWCHKz_Sj)>lSO>ym)y9_+T+hjmka3HRcK*T7Mht4-evgSTjG>sRlv5(O^WBY zw^Siq_~~yeGTmyJk+?HD^Yk^PfCP1(C(HfCtIgu3Nu~~x3jJv-TJbETBb z)6cX@3f{gxr_l>KpZbw}RMt7Tyn{qfEAi@=%(oQ%UFJ(CXR>-EOE>c#j+y;&5wx>G zN^8s~&$(p78e3&5drT1By1dlAR>)-qSNH6F%}#d@Y~B6jl5Y4Z-bKDBb3zUyD~j3U zy$_27u}ym)7q&2uW+|6*C&lu$d6-^DC;rye|MeWtnH~>Nxf3P*@J) zyR1msw#QnRhc0#{*D^-&Ek+D_s?JAGx5My>sP-{WhcfFcHI)J_VLIBD?41MAryLuv zp&8kwOie~XvzF!Q#{m*)lMdGrVJQo>fw8jWwScB8JLDUsm6Bd#WH|c8n@3-)9=vOz zE74+44ujhkbl%FXKke*w3q-jyF+SRam zudH!T4;-x;M!T?VH*iAVsVQEzMa@{q=`q?9jTGaXNqBLX}Dah}X!hTmSy0S{CE;U-eel{Q?;11v! z!Ig9gV@Z=OeR-NqcQ3>SjgLwAqDRJD%jRBnT-<&+4ZZ-;DLrUt?UHn0bg}O9=m0y^ z?b{O7y8P_nX}Lh&=a4i^$>t8FC|u5-*)iI>I;N#(u-sr(zTjZl8tkr{ORUEY%(Bby z2k8{wF_HZ#3iRW^L4|bSLpJdN*Vwx?Uxjw3| z9HD-|@_OXHWX1VE-GFz-5c6AH(!UW5=L~b7wu+&?VRLd*` zPkd@W!FbAwp8X__O=|o4+AjI2?#q{VFp`t_T82FG|k6bEul5yEAraQiAH`l zz)qf62`6$_vxX+&75-}`l!mrmkOf%M%k_75C03z`swzk3Hhc;FV43$RPD#?Q){^_X z(yNiGop+r5Tk=_D(;SrsgDv_vFBvwE3d(F~3t)R$QJFy|pl@&MBHkNg$uTsRE zR5ddyc>mzFIDgCxn8hfmgCmRxTY z6PJ^j=;yU_JR&+vH>y~jZ2r*DF&HZERJ@neq&0i#Ml@e3!{NLSs%67R2nrS{QuAwM9NDgbTR3!4Cx$YldP!`keNfi8uuyQfTSgrufy3dU}ci!+&Vey%~OVE=Xh$3 z4@zwBJ&2ojUil#m(>`vV5ORnRu40a{@N( zc(mTA-N_XZN@C^8%rDIlNA10M z>mc{9xt<*P>;w0njS;rv2b-_JZibww3-@#4!+}~E-po>{dJo0qVg-RV;++t(S*g+--G7+?ctOj zbDZXew~z4BUZNZ$jBq<)IAC(%rO>qIomtDkFl$79UF*iVtAO%;Y@^hy)cifWTIwSe#@gFhDXf6Ow?6%C>M& zs>K2#i0GNpmQW#zqC>JRRO`5I#ZzWX*=j0Cr_+Qo8)#Tq}we2rjb?EZkodgo&* zFi0OHTZSoVwF^$r3ev@-)&ew(HP79o#g!4Ho~ks1awxpMmbVqk6F=5pBm5PgIz$HN?u z9VGK(d(t|REpb60B@82M5m@nss8}L#W}`65?{_Xi#g1-D;S@Jb{dHO}LhexWHyj{XjxFjMGNxBKw0AN-h~ms!5HW z0a#=hs|};|G!sVuf$QPNzX55_c8z(g9&*hnhA{7l3ah1p`&z4 z(#rs7+MCsAF;5+d>uFseO-Bw@%5R7=8x^?l*t1tq8iF=5b6zqarMRAS<$$UQAsB&R zG!Rq`MKDK4l3`NaQgpa`z3_*Sa1wBLHdi+npgc4KHIyDXiQ))bv z?P|r5&4pJzn=jb+}HE%Z*XieW#%6} z7&m}iJIsLH-_sbbG|#FlM`>)FaTe`+k;dn6-y>W#6pEYwvg@O{aYP zEJrRFmtPpL{KVo9gRWkar*Aq?kZ5Bt#BKTPv$(xkSt~6qM=ggfCqDd$eJ7x@U7c1E z{c}f0ZESNwSK~>a4waF9@pDT}rl=$<*EbutcIx_`HSeU!ec!~?rHb(Q&I4Ia9nMn| zIGqv}cHn6BPVOzQUsmN#3S1dfZn5U;7c;rn*8g~azU-5PzSJs*(}I=->$B2rh}v%X z?JCKtRjXLBFPcAfs+M}lGi-##39P)Id^;TMx?C@9FRG~T-BmJ2j$!UU{{2p2dD~w$ z*)OeJ^!@gPBljNd_hpU#+lWnV-IIdlGfR7Xg96TX*4jOLSbsP5`SH0;p4RW>Gs?-| z#EEz1I~|B#1}aiw_HX%91wN5qy2``*uUe zTR&a*t!1gTBzO(us#y6iY5u!z^f8N#doISV4VI4{*Yd8&27eN-j`7XgA0zPUF1z7q z5BAZ_sHe|&oxXTDS$M2z=abeE!DU)U_XkBLt32nC4nG|I=Yci0ts5)F&x@R@=NFfH zrfpUH=gQL6vGU%t*C&OvRiVf^6}6$E$27~z?MP~6!&=hzBIh8#uHf;7n_*i5ykzw& zCu<68tRvGNmxnLQ$Qjf5X!6EJ;5&6;_m|U;Z9n5^OWpYQM-jJ@{dx9^?zR_HOz@SN z$chJ5s9#gFHb!s|dD1?9Q;9L;EVE?F?0h*jb(bO2VV}JAU_xVC3p}D>S`IQTqh3qe z&R#DsZ`%_UBj%lNE(2`w>)K+-Cp~?g5j;z}u{xwVa{1}Ai@Fn+-QJ=Xm$nAq%f95f z-ZCU{hjRvezvomj=UQFNj+(;`AT5u1ed!hFk}qkq1L~|Dp9aJ(jCF~9yq5GYE0~bf z9Us1pw441nI_FST%GIrI-=*c8dng7a*>U|zJukd>C2=aNAm@B(t6%SagR^xA(z6;* zv^KOdC)}Jj9P$v`k`^zhPzqrF*Y*J z-7&E-jcJ+H`wXP*S5PC49ukFs2AThPiceXEDJ8DUHz~1*T6)D?s?PZHJ>g4s6n8c? zS~(%{QQtl`rW_=Zx&JE#oYkZLDzEjLQyAMhQvHk2v&Jp? z10Z_gO^koJN!AB8x;9c9t#VUK3nrvUmo(Xyi8iEtElOH3r4qDf^5o|Syqw#OGTnMj z&{(F!Zc%Sa2cMj=Q?wam-68WI4a6@qMyc;jsml-JxL23s_FpCYpPRZmsV^{N;oaJ+ z_4C*sv0rHEu$hJO=QEZa;P!4?wU|P1IR_Ym3Kgo+x|jh z-M)0Y>;_zLu)=goX!B)Q*S_!a{ECkk*)j&rF}}HvX;Zt@?wbb>G?YWtk(+)Kk2}x0 zy({;2k*d+;d!fdTxKw%KXhTXAf3DX_5&ueJG^2X2zC~r57gB!fX$h$VE?$gKj26!< Ts-d=;|3Y|s`AG8?tcm+Ct#;lm literal 0 HcmV?d00001 diff --git a/games/long_night/resource/retro/heat.png b/games/long_night/resource/retro/heat.png new file mode 100644 index 0000000000000000000000000000000000000000..1639124f659749e2d8a3accd2adc8e990a1f2b39 GIT binary patch literal 4340 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 literal 0 HcmV?d00001 diff --git a/games/long_night/resource/retro/oneway_portal.png b/games/long_night/resource/retro/oneway_portal.png new file mode 100644 index 0000000000000000000000000000000000000000..233f8236c8cfd287601c56507c600edb1157897d GIT binary patch literal 94326 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%0AMHZ@g0jO-a<51kq;X1- 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 Y0QcH4Paw;SlK=n!07*qoM6N<$f@NgMCjbBd literal 0 HcmV?d00001 diff --git a/games/long_night/resource/retro/portal.png b/games/long_night/resource/retro/portal.png new file mode 100644 index 0000000000000000000000000000000000000000..ad01003050bd1988ededd5bcbdc13c6a22a70d65 GIT binary patch literal 4217 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 literal 0 HcmV?d00001 diff --git a/games/long_night/resource/retro/transparent.png b/games/long_night/resource/retro/transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..ad6b2cb061b3531ed0f52636bd603312d4abfca4 GIT binary patch literal 1484 zcmbVMJ8aZQ81@NqIzW+-xB?+`8k`0s{M_{>p2I|mH?S+=5G^YbB#O+A$Gg6)J;t77 zH=#he3M43K5vOZtXimBEl;WU)l7=2CI)p@n=a}VEW-}i@{RsbGW^Vrt)1jnxEt8}p$MW%m^x)xfNji8kY%Yss?-HgwQi;!7 zOzlSrpe5!JxAX@eok;_wtEVZIgqimrmTQw@BvTdJ5 zrfy7Y`m|=06r+O6IyOx-`ecxn`T=fKFN|ct&XGGpBv{jWy`I`DsXT3KhHcxLZfd5f z07A(&VnO;!oK21ysw|^vn23$1&SC=R%L}X%q9n2UxWM6dF z+(?+#U>RRaDVtvhH79dx66~go2%a`M?~VhtI93_CZXOsLon8uKpZBseyWX%W5zLWI z-Lw?Lgs3%*5-!_Vw-nvNx;}(@+z*3|{h*eP_3|DlWQ|V*`6}3_IN)hSz~(R_ZKfr0 zTSkQ^vCF$W1;L^Ebr(|}gk8AE8W-nKecr7YcBP`2sxeU4^Kd=R1c@oDR~;Gr zqlTf6%PqsODy3EJa&Wh+#K&;x8~L(mSg`wrl3>PVnaD(_6p3{&~{6@Y~&&#~&LF`si8h-u)9_ bJ$NuN`Og9Je&x?U@_t*NU#R{yxAOack>}HX literal 0 HcmV?d00001 diff --git a/games/long_night/resource/retro/trap.png b/games/long_night/resource/retro/trap.png new file mode 100644 index 0000000000000000000000000000000000000000..76d53fda1ff4a5aba6f5d47f7c2d2fbb0e39a6cb GIT binary patch literal 4471 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 literal 0 HcmV?d00001 diff --git a/games/long_night/resource/retro/unknown.png b/games/long_night/resource/retro/unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..025fe623235d270799f3e4f9db9220c546db668c GIT binary patch literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nET5KkA!kc`H+=NWkq81Nj}Q1^#Z y?5w-;tQ@v~W#8ws&k+|=7E%dPCLT^QG8R8*#{BL^sJ1oGd!zB*@ literal 0 HcmV?d00001 diff --git a/games/long_night/resource/retro/walls/corner.png b/games/long_night/resource/retro/walls/corner.png new file mode 100644 index 0000000000000000000000000000000000000000..e8806005d1913703ae4c2b15380a2a03059e716f GIT binary patch literal 1626 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 literal 0 HcmV?d00001 diff --git a/games/long_night/resource/retro/walls/empty_col.png b/games/long_night/resource/retro/walls/empty_col.png new file mode 100644 index 0000000000000000000000000000000000000000..751e9fba6654cf92bbc9d4c2d182c867c67ebfe6 GIT binary patch literal 2212 zcmbVO3s4kg96zMgEW}1oF?6=0QXYHzxaaP02y(oHL*yZtrta;2cieDyd+qLV2aS_f zrj3m{I+BXgm`*8LlMa%@L6nmwLeZpVmeJ&k(hwhwX`=S+ad&CqW4f8W{dV{F|NVde z*SGstQDIKhsEMN#3Pn_2uBjNFW8lG!90vcN=vY=SpWL}~M1|tXvGNh2*tlteLNPqw zURohlm4s>9B6XV2&9GHKLM*^$cNvxa{&po|=Zwm7tr<7FGeM<2 zw^jfpwS}c@Z52y#%4umxs*i>QP9QOe&*^Z9w9lvv@X~NC-^P?k03uZxl^L=_q{3W; zWby()C>4RSI8GpXxYFoI14X4EBu=U^T!)cbl*DPAptV{g_)|i60%xU*P1!+b@XDyH zlq5HeVP3CS)RW5YPNM{oq3b1ViNzzJNE2(d&M>#pP_UaaKr zLmhs6j{}%nvpMWzNG(oh*o7!%Rl_iX2^kVCme#ldRt!YmBd{Q=8p=$NqjA%j0$?Ow zDCK!as8U5C$q1=csgbGWb{EHc#l*V|fQgZSQ3J~lAv*2GH#%8d=P5p zIlHyyZYU{32@Oq9G>P8>g+0SD67x_n$I@0_a56ApyOXg2%rsP2V?_-XK%x|(BPlp@tkz&n z#pGdtM#$i5kO+IQ$3TnHxprAWP0()%csZ;933MgRu=2DSm8{%mfKvu<+lQ#gqh)lE z)>{c6=>ytt5GL|g$;${J!v?GO(DugeCthT#A1wXRn(4pNha89hBR!Z_wvut#0NlGV zr5sG4u7gFv{#~!Yv%5CL5FGBZ@-n=?!;A160=OWx0Qdjcc}L-ECE`w=DWkMvY2SLk zYk&HftW{CFKW`dHzcQjJ@w)IqZ}jxc3xA9p6a9M0JSy&cLv+pCF&&?z*PS?Vdc&mt zF8c7*@eA93jw~y?JpRC|6Blh|X2pEw&_%8}6EU^(s($#a$3Ct|d2#L9=OP*!CNH_{ zdumhR6m|Nn<%`l<$VmAEPH_i}0TF8LoKrh*8MV>^EX#Hw*Gc&o}GcH4OV^`aOPdj?j=AO8o{B=unm+Me*$$)=KzPVt8 zz@L2P&x1V&%l9S?w4M5bI39a*KIeb;mDObt7Y4@#zxh& z2{%@s_9?o0yVEzWT)d~XuWeV%iq!78Yqdut`tj_oPcQH+sy^G@+dX!9&YL?%S6zI+ za6#Obl^v@bvELqDc6e_4$=eNy(d+t}%daG?Nc^F;?VP$h`TUWkZKpGu3u-FG+})cC zUN}nhpEKF~otI+bt}W@g;PB>cu8LWq>Zrr+oc*wkKL+}DX#4#=J^S>x#~+;5TDGON z>KMN*>9<>DGyF}}1uyCQp0#gmS+h64F>+t|@twb-@8q8!{rh~b?ACj}tvB)uwx`rL cJ>MR=J}Y_ir6`~LJ)+3VDl~mGec@|=13Ntv>;M1& literal 0 HcmV?d00001 diff --git a/games/long_night/resource/retro/walls/empty_row.png b/games/long_night/resource/retro/walls/empty_row.png new file mode 100644 index 0000000000000000000000000000000000000000..1c47bea9b5d72aa49e64119e3dde4a7ecbff2b76 GIT binary patch literal 2441 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 BG6?_x literal 0 HcmV?d00001 diff --git a/games/long_night/rule.md b/games/long_night/rule.md index dac521e..c2c4533 100644 --- a/games/long_night/rule.md +++ b/games/long_night/rule.md @@ -1,8 +1,7 @@ ## 漫漫长夜 -- **游戏人数:** 2-6 +- **游戏人数:** 1-8 - **原作:** 大萝卜姬 -- **详细图片规则可查看群文件:《漫漫长夜》.pdf** ### 游戏简介 - 本游戏适合 4/6 名玩家游玩。你和其他玩家被传送到一座伸手不见五指的迷宫里,你将同时扮演逃生者和狩猎者的身份,在漆黑的迷宫里通过有限的信息到达逃生舱或者捕捉其他玩家,以此获得胜利! @@ -40,11 +39,14 @@ + 【幻变】将从**轮换区块**中抽取12+4组成随机池,根据更新发生变化 + 【狂野】将从**所有区块**中抽取12+4组成随机池 + 【疯狂】从**所有区块**中抽取10+4,同时包含2个**特殊区块** -- ②点杀:捕捉改为仅在回合结束时触发,路过不会触发捕捉 -- ③反侦察手段:玩家获得技能“隐匿”,可选【回合】和【单步】模式 + + 【自定义】从**自选区块**中随机抽取,逃生舱数量随机 +- ②BOSS:具体规则详见「#规则 漫漫长夜 BOSS」 +- ③点杀:捕捉改为仅在回合结束时触发,路过不会触发捕捉 +- ④反侦察手段:玩家获得技能“隐匿”,可选【回合】和【单步】模式 1. 回合隐匿:持续1回合,**仅可使用1次**,当回合的所有行动转为私聊进行,不会发出声响,不会触发捕捉。 2. 单步隐匿:仅作用于下一步,**可使用4次**,隐匿后在私聊行动下一步,不会发出声响,不会触发捕捉。 -- ④10x10!:本局游戏的大地图将更改为10x10大地图,9个区块随机排列塞满,区块不会重叠,没有区块的空隙将变成普通道路。 -- ⑤大乱斗:所有的逃生舱改为随机传送!但仍会统计逃生分 -- ⑥BOSS:米诺陶斯随机生成在地图中,每回合最后行动,首回合锁定最近玩家为目标(公屏显示),每回合移动步数递增,发现更近玩家则更换目标并重置步数。无视地形,移动结束时会发出巨响,如果玩家在其周围会听到喘息声。BOSS踩到玩家则玩家出局,玩家经过米诺陶斯不会出局。 - +- ⑤10x10!:本局游戏的大地图将更改为10x10大地图,9个区块随机排列塞满,区块不会重叠,没有区块的空隙将变成普通道路。 +- ⑥大乱斗:所有的逃生舱改为随机传送!但仍会统计逃生分 +- ⑦谋定后动:每回合仅能执行一次移动,可使用多步行动指令 +- ⑧炸弹人:玩家可在公屏安置炸弹,任何人经过炸弹并离开会引爆炸弹,使玩家立即出局并-100分。**在炸弹上结束行动可拆除炸弹** + From d2c0465694a50e3faf98ee1eb48d3b67f766ccff Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Sat, 7 Feb 2026 00:47:29 -0500 Subject: [PATCH 10/12] escape_building: fix bug that game wouldn't auto end in a tie --- games/escape_building/mygame.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/games/escape_building/mygame.cc b/games/escape_building/mygame.cc index ec233a6..eb1261f 100644 --- a/games/escape_building/mygame.cc +++ b/games/escape_building/mygame.cc @@ -356,6 +356,7 @@ class RoundStage : public SubGameStage<> // 杀手无法安装弹射装置,强制平局 if (Main().police_floor > 2 && hostage_available.empty()) { Global().SetReady(0); Global().SetReady(1); + Main().player_scores_[0] = Main().player_scores_[1] = -1; Global().Boardcast() << "[提示] 杀手已无可安装弹射装置的楼层,且当前未抵达 2 楼,满足平局特殊条件,游戏结束!"; return; } From 605eb63dc3d90c21dd9092ee2787c0b28abec5c2 Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Sun, 8 Feb 2026 17:51:13 -0500 Subject: [PATCH 11/12] keep_drawing: new game --- games/keep_drawing/achievements.h | 0 games/keep_drawing/icon.png | Bin 0 -> 14710 bytes games/keep_drawing/mygame.cc | 498 ++++++++++++++++++++++++++++++ games/keep_drawing/option.cmake | 0 games/keep_drawing/options.h | 4 + games/keep_drawing/rule.md | 15 + games/keep_drawing/unittest.cc | 29 ++ 7 files changed, 546 insertions(+) create mode 100644 games/keep_drawing/achievements.h create mode 100644 games/keep_drawing/icon.png create mode 100644 games/keep_drawing/mygame.cc create mode 100644 games/keep_drawing/option.cmake create mode 100644 games/keep_drawing/options.h create mode 100644 games/keep_drawing/rule.md create mode 100644 games/keep_drawing/unittest.cc diff --git a/games/keep_drawing/achievements.h b/games/keep_drawing/achievements.h new file mode 100644 index 0000000..e69de29 diff --git a/games/keep_drawing/icon.png b/games/keep_drawing/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e4959c3a191c4cb9afbc3df219b52b96ca179577 GIT binary patch literal 14710 zcmbWe1z40_+ct`bl*E9vbfePUNOvh+0z-ExaTimuP1OBSp;x!&xkGFYdUKx$nzQ7*)SPG?Tk#A+->Z^XgD|lVRw5&V=EJ9 zaw8Kn3tK_Ts=ZP)cDTashWfFoBJU zvmv>=jkT>4pSvLCKlAc|_ppzdDarpC;%p^I`4)CVa!mzgaxptc6LL-_E=FTk4o-4z zUM5y9b`DZ$8qmzfNv!OeqtrOLM%^+^#Wb9~R?`&ab zOAedS(8$ikS&$N}^k0u)WB;FNZJqwTO<=>A-3{%TS(#X1kMz$#sPTWs*}FJe|8sMw zF|&!aiH(V^vlAH0`k%4(=623@PUd$1H&g%T>;KaYz}6}#{O2D3<63NN{&Nc_X9-vE z82{do|8ca_dk=dPW>pg>I~PY|6A4$aOe)yZ*z<`wnix9UIli~Ev;MDxQvRYD<3Dg^1lrQRs%J3HvE4b3^nF6wR5yF1kY??V`yf=Y;S8uN&cUmA zwsQnG2CHNL@1M(vi77kUnOazbFPv1RM9F0&#JE^_xwshFm{|W=u7U!ejIEQip{=or zjJO~r*dHbf3n(9y&6v%|*o2+Ykd23zk%yJTn9-2K$dr+tot2%-$QTOc<^I>>g6qZY zj9p+>@L$(M|9@Vu#h~{=YuuKi~Z~7yqx}Aa20k{wH98U;Y#FOl(1%a|8ix!ELDt4vqs(M*Qu2_pHNg zFK@N!^viauW1pkO_~#I8rYD^=^yC_h`Jcy1Kl&a_Pgs^`#}tg1EuGldWY7G)$hIh% z^r)Mj(YMa7SJ!c<{iIy?>9ZPCxiMS>TLc-B0C5!TZ#-_Dz3mm+-q;u@qNbxeINpBI zKkt=$RpvBfCphXkqae*->Xk^ji}4vxJ`_$l{{Z5fd~Cq$aW-o_7_?5ev2kSx_p} zc!e2di0Fzq4CQhBMcdKpFM`Bf$@nPfQZpRy?kZUO&r$1)1#QPUIFQqbs(cIzgOdpN zN}amIY*3bSepCp5lqy8-;D`?~6r>8G$w)e-<)QLNfPdn8^t<2xDbmi)&JT`IolFN) z`iOwhZR3F?hM{c!)RvYOc25Hqg2JMrU}gFstVd;bED06+=EW!?&WMA9gNm2oe<)X3 z6%lCslKQ3}d<}&-sEy9dqE!=LFvYDA(>bn^Oi~FMnRw5!1 zWa7!Z7oxHRs zB+sJDGIsJ|CzgfiB^EC)bg??4Os73a`+bt_*(2@HU)KQUih^~w_KwfrdLQSM?Bz3K z_9x`Z$KsPV#O}O~G|2f~4?a~@0dvu8acBLiUQUS}4nYk@4($vy$apJ){MOf3=(R>g z*ZRqy2|4ERnVBB1SSH3)*LHdI!J^(&LM{imw_fvIZT

vyA5I-3cqDDtbBA*!N+r5GA0O&!qQx;UVS z@?@MozWkWHKqaeYd*$}rY{KTqqqoMI_vmX%0A*O)Et9D+6%R3TQ?+|IPo9#0TCt3m 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 UmO(SHzl@ZTP!z8a{SffK015%toB#j- literal 0 HcmV?d00001 diff --git a/games/keep_drawing/mygame.cc b/games/keep_drawing/mygame.cc new file mode 100644 index 0000000..c301208 --- /dev/null +++ b/games/keep_drawing/mygame.cc @@ -0,0 +1,498 @@ +// Copyright (c) 2018-present, JiaQi Yu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include + +#include "game_framework/stage.h" +#include "game_framework/util.h" +#include "utility/html.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +class MainStage; +template using SubGameStage = StageFsm; +template using MainGameStage = StageFsm; + +const GameProperties k_properties { + .name_ = "抽抽抽", + .developer_ = "铁蛋", + .description_ = "抽是胆识,停是智慧", + .shuffled_player_id_ = true, +}; +uint64_t MaxPlayerNum(const CustomOptions& options) { return 5; } +uint32_t Multiple(const CustomOptions& options) { return 1; } +const MutableGenericOptions k_default_generic_options; +const std::vector k_rule_commands = {}; + +bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) +{ + if (generic_options_readonly.PlayerNum() < 2) { + reply() << "该游戏至少 2 人参加,当前玩家数为 " << generic_options_readonly.PlayerNum(); + return false; + } + return true; +} + +const std::vector k_init_options_commands = { + InitOptionsCommand("设置边长和胜利分数", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options, const uint32_t& size, const uint32_t& score) + { + GET_OPTION_VALUE(game_options, 边长) = size; + GET_OPTION_VALUE(game_options, 分数) = score; + return NewGameMode::MULTIPLE_USERS; + }, + ArithChecker(3, 5, "边长"), OptionalDefaultChecker>(60, 30, 600, "分数")), + InitOptionsCommand("独自一人开始游戏", + [] (CustomOptions& game_options, MutableGenericOptions& generic_options) + { + generic_options.bench_computers_to_player_num_ = 5; + return NewGameMode::SINGLE_USER; + }, + VoidChecker("单机")), +}; + +// ========== GAME STAGES ========== + +class RoundStage; + +class MainStage : public MainGameStage +{ + public: + MainStage(StageUtility&& utility) + : StageFsm(std::move(utility)) + , round_(0) + , player_scores_(Global().PlayerNum(), 0) + {} + + virtual int64_t PlayerScore(const PlayerID pid) const override { return player_scores_[pid]; } + + std::vector player_scores_; + + // 局数 + int round_; + // 当前行动玩家 + PlayerID currentPlayer; + + // 棋盘 + std::vector> board; + + bool GameEnd() { return std::any_of(player_scores_.begin(), player_scores_.end(), [this](int64_t s) { return s >= GAME_OPTION(分数); }); } + + private: + void FirstStageFsm(SubStageFsmSetter setter) + { + srand((unsigned int)time(NULL)); + + currentPlayer = 0; + board.assign(GAME_OPTION(边长), std::vector(GAME_OPTION(边长), 0)); + + setter.Emplace(*this, ++round_); + } + + void NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) + { + if ((++round_) <= GAME_OPTION(局数) && !GameEnd()) { + setter.Emplace(*this, round_); + return; + } + if (GameEnd()) { + Global().Boardcast() << "有玩家到达胜利分数,游戏结束"; + } else { + Global().Boardcast() << "局数到达上限,游戏结束"; + } + } +}; + + +class RoundStage : public SubGameStage<> +{ + public: + RoundStage(MainStage& main_stage, const uint64_t round) + : StageFsm(main_stage, "第 " + std::to_string(round) + " 局", + MakeStageCommand(*this, "继续 / 停止", &RoundStage::Action_, AlterChecker({{"抽", true}, {"继续", true}, {"c", true}, {"停", false}, {"停止", false}, {"t", false}})), + MakeStageCommand(*this, "查看当前游戏进展情况", &RoundStage::Status_, VoidChecker("赛况"))) + , player_stop_(Global().PlayerNum(), false) + {} + + // 玩家停止 + std::vector player_stop_; + // 爆炸掩码:标记哪些格子属于爆炸的行/列/对角线 + std::vector> exploded_mask_; + + virtual void OnStageBegin() override + { + Global().Boardcast() << Markdown(GetBoardHtml()); + + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + if (pid != Main().currentPlayer) { + Global().SetReady(pid); + } + } + Global().Boardcast() << "请 " << At(Main().currentPlayer) << " 选择行动,时限 " << GAME_OPTION(时限) << " 秒:继续(c) / 停止(t)"; + Global().StartTimer(GAME_OPTION(时限)); + } + + private: + AtomReqErrCode Action_(const PlayerID pid, const bool is_public, MsgSenderBase& reply, const bool draw) + { + if (pid != Main().currentPlayer) { + reply() << "[错误] 不是您的回合,当前玩家是:" << Global().PlayerName(Main().currentPlayer); + return StageErrCode::FAILED; + } + + if (draw) { + int num = ActionDraw(); + reply() << "您选择继续!新增数字:" << num; + } else { + int score = ActionStop(pid); + reply() << "您选择停止!本局计入 " << score << " 分,当前分数为 " << Main().player_scores_[pid]; + } + + return StageErrCode::READY; + } + + int ActionDraw() + { + int size = GAME_OPTION(边长); + int num = rand() % (size * size) + 1; + int col = (num - 1) / size; + int row = num - col * size - 1; + Main().board[col][row]++; + return num; + } + + int ActionStop(const PlayerID pid) + { + int score = GetBoardSum(); + Main().player_scores_[pid] += score; + player_stop_[pid] = true; + return score; + } + + AtomReqErrCode Status_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) + { + reply() << Markdown(GetBoardHtml()); + return StageErrCode::OK; + } + + virtual CheckoutErrCode OnStageTimeout() override + { + return HandleStageOver(); + } + + virtual CheckoutErrCode OnStageOver() override + { + return HandleStageOver(); + } + + CheckoutErrCode HandleStageOver() + { + if (!Global().IsReady(Main().currentPlayer)) { + Global().SetReady(Main().currentPlayer); + int score = ActionStop(Main().currentPlayer); + Global().Boardcast() << "玩家 " << At(Main().currentPlayer) << " 行动超时,自动选择停止,本局计入 " << score << " 分"; + } + + // 游戏结束 + if (Main().GameEnd()) { + return StageErrCode::CHECKOUT; + } + + bool round_end = false; + int active = CountActivePlayers(); + // 所有玩家停止 + if (active == 0) { + round_end = true; + Global().Boardcast() << Markdown(GetBoardHtml()); + Global().Boardcast() << "所有玩家均已停止,本局结束"; + } + // 触发三连 + if (CheckBoardExplode()) { + round_end = true; + Global().Boardcast() << Markdown(GetBoardHtml()); + if (active == 1) { + Global().Boardcast() << At(Main().currentPlayer) << " 触发三连!本局结束"; + } else { + int score = GetBoardSum() / (active - 1); + for (int pid = 0; pid < Global().PlayerNum(); pid++) { + if (!player_stop_[pid] && pid != Main().currentPlayer) { + Main().player_scores_[pid] += score; + } + } + Global().Boardcast() << At(Main().currentPlayer) << " 触发三连!本局结束,未停止玩家分得 " << score << " 分"; + } + } + // 本局结束 + if (round_end) { + ClearBoard(); // 重置盘面 + return StageErrCode::CHECKOUT; + } + // 下一个玩家行动 + do { + Main().currentPlayer = (Main().currentPlayer + 1) % Global().PlayerNum(); + } while (player_stop_[Main().currentPlayer]); + Global().Boardcast() << Markdown(GetBoardHtml()); + Global().Boardcast() << "请 " << At(Main().currentPlayer) << " 选择行动,时限 " << GAME_OPTION(时限) << " 秒:继续(c) / 停止(t)"; + Global().ClearReady(Main().currentPlayer); + Global().StartTimer(GAME_OPTION(时限)); + return StageErrCode::CONTINUE; + } + + virtual AtomReqErrCode OnComputerAct(const PlayerID pid, MsgSenderBase& reply) override + { + if (Global().IsReady(pid)) { + return StageErrCode::OK; + } + + int sum = GetBoardSum(); + int range = GAME_OPTION(边长) * GAME_OPTION(边长); + int limit = (range + 1) * range / 4; + if ((sum < limit || rand() % 5 == 0) && Main().player_scores_[pid] + sum < GAME_OPTION(分数)) { + int num = ActionDraw(); + Global().Boardcast() << At(pid) << " 新增数字:" << num; + } else { + int score = ActionStop(pid); + Global().Boardcast() << At(pid) << " 停止!本局计入 " << score << " 分,当前分数为 " << Main().player_scores_[pid]; + } + return StageErrCode::READY; + } + + int CountActivePlayers() { return std::count(player_stop_.begin(), player_stop_.end(), false); } + + std::string GetBoardHtml(); + bool CheckBoardExplode(); + int GetBoardSum(); + void ClearBoard(); +}; + + +std::string RoundStage::GetBoardHtml() +{ + const char* style = R"( +)"; + + bool boom = CheckBoardExplode(); + + std::string round_info = "## 第 " + std::to_string(Main().round_) + " 局"; + + html::Table playerTable(Global().PlayerNum(), 3); + playerTable.SetTableStyle("align=\"center\" cellpadding=\"2\""); + for (int pid = 0; pid < Global().PlayerNum(); pid++) { + if (player_stop_[pid]) { + playerTable.Get(pid, 1).SetColor("#E5E5E5"); + } else if (pid == Main().currentPlayer) { + if (boom) playerTable.Get(pid, 1).SetColor("#FFA07A"); + else playerTable.Get(pid, 1).SetColor("#FFEBA3"); + } + playerTable.Get(pid, 0).SetStyle("style=\"width:40px;\"").SetContent(Global().PlayerAvatar(pid, 40)); + playerTable.Get(pid, 1).SetStyle("style=\"width:280px; text-align:left;\"").SetContent(Global().PlayerName(pid)); + playerTable.Get(pid, 2).SetStyle("style=\"width:120px;\"").SetContent("分数:" + std::to_string(Main().player_scores_[pid]) + ""); + } + + std::string win_info = "

"; + + const auto& board = Main().board; + int size = board.size(); + html::Table boardTable(size, size); + boardTable.SetTableStyle("align='center' cellpadding='6' class='board'"); + for (int i = 0; i < size; ++i) { + for (int j = 0; j < size; ++j) { + auto& cell = boardTable.Get(i, j); + int value = i * size + j + 1; + int count = board[i][j]; + std::string classes = "grid "; + classes += (count <= 0) ? "empty" : "filled"; + if (boom && i < (int)exploded_mask_.size() && j < (int)exploded_mask_[i].size() && exploded_mask_[i][j]) classes += " explode"; + std::string style_attr = "class='" + classes + "'"; + cell.SetStyle(style_attr); + if (count <= 0) { + cell.SetContent("" + std::to_string(value) + ""); + continue; + } + if (count == 1) { + cell.SetContent( + "" + std::to_string(value) + "" + + "" + std::to_string(value) + "" + ); + } else { + cell.SetContent( + "" + std::to_string(value) + "" + + "" + std::to_string(value) + "" + + "" + std::to_string(count) + "" + ); + } + } + } + + std::string boom_info; + if (boom) boom_info = "
你爆啦
"; + + std::string sum_info = "
桌上总点数
" + std::to_string(GetBoardSum()) + "
"; + + return round_info + style + playerTable.ToString() + win_info + boardTable.ToString() + boom_info + sum_info; +} + +bool RoundStage::CheckBoardExplode() +{ + const auto& board = Main().board; + int size = board.size(); + if (size == 0) return false; + // 重置爆炸掩码 + exploded_mask_.assign(size, std::vector(size, false)); + // 4 个方向:右、下、右下、左下 + const int dx[4] = {0, 1, 1, 1}; + const int dy[4] = {1, 0, 1, -1}; + + for (int x = 0; x < size; ++x) { + for (int y = 0; y < size; ++y) { + if (board[x][y] <= 0) continue; + for (int dir = 0; dir < 4; ++dir) { + int nx1 = x + dx[dir]; + int ny1 = y + dy[dir]; + int nx2 = x + 2 * dx[dir]; + int ny2 = y + 2 * dy[dir]; + if (nx2 < 0 || nx2 >= size || ny2 < 0 || ny2 >= size) + continue; + // 三连判定 + if (board[nx1][ny1] > 0 && board[nx2][ny2] > 0) { + exploded_mask_[x][y] = true; + exploded_mask_[nx1][ny1] = true; + exploded_mask_[nx2][ny2] = true; + return true; + } + } + } + } + + return false; +} + +int RoundStage::GetBoardSum() +{ + const auto& board = Main().board; + int size = board.size(); + int sum = 0; + for (int i = 0; i < size; ++i) { + for (int j = 0; j < size; ++j) { + int value = i * size + j + 1; + sum += board[i][j] * value; + } + } + return sum; +} + +void RoundStage::ClearBoard() +{ + auto& board = Main().board; + int size = board.size(); + for (int i = 0; i < size; ++i) { + for (int j = 0; j < size; ++j) { + board[i][j] = 0; + } + } + // 清空爆炸标记 + exploded_mask_.assign(size, std::vector(size, false)); +} + + +auto* MakeMainStage(MainStageFactory factory) { return factory.Create(); } + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + diff --git a/games/keep_drawing/option.cmake b/games/keep_drawing/option.cmake new file mode 100644 index 0000000..e69de29 diff --git a/games/keep_drawing/options.h b/games/keep_drawing/options.h new file mode 100644 index 0000000..30b3b1a --- /dev/null +++ b/games/keep_drawing/options.h @@ -0,0 +1,4 @@ +EXTEND_OPTION("游戏棋盘边长", 边长, (ArithChecker(3, 5, "长度")), 3) +EXTEND_OPTION("获胜所需的分数", 分数, (ArithChecker(30, 600, "分数")), 60) +EXTEND_OPTION("游戏局数上限", 局数, (ArithChecker(1, 20, "局数")), 20) +EXTEND_OPTION("每回合时间限制", 时限, (ArithChecker(10, 3600, "超时时间(秒)")), 90) diff --git a/games/keep_drawing/rule.md b/games/keep_drawing/rule.md new file mode 100644 index 0000000..65dd4a2 --- /dev/null +++ b/games/keep_drawing/rule.md @@ -0,0 +1,15 @@ +## 抽抽抽 + +- **游戏人数:** 2-5 +- **原作:** saiwei + +### 游戏简介 +- 抽抽抽是一款数字棋盘对战游戏,玩家通过抽取数字和策略决策来获得分数,率先达到或超过 60 分者获胜。 + +### 游戏流程 +- 3x3 棋盘,每个格子对应数字 1-9。玩家轮流选择继续或停止 +- **继续:** 随机 1-9 之间的数字,数字会自动放到棋盘上对应的位置,**如果该位置已有相同数字,则叠放**。 +- **停止:** 立即获得桌上所有数字的**总和**,停止后退出本局,其他玩家继续游戏,如果所有玩家都停止,本局结束进入下一局 +- **三连爆炸:** 如果放入的数字与其他数字形成横向、竖向或斜向的**三连**,则爆炸。未停止且未爆炸的玩家平分桌上所有数字的总和,本局结束,进入下一局。**下一局由上一局爆炸玩家或最后停止玩家率先抽取。** +- **胜利条件:当有玩家达到或超过 60 分时,游戏结束,该玩家获胜。** + diff --git a/games/keep_drawing/unittest.cc b/games/keep_drawing/unittest.cc new file mode 100644 index 0000000..1e75f1b --- /dev/null +++ b/games/keep_drawing/unittest.cc @@ -0,0 +1,29 @@ +// Copyright (c) 2018-present, JiaQi Yu . All rights reserved. +// +// This source code is licensed under LGPLv2 (found in the LICENSE file). + +#include "game_framework/unittest_base.h" + +namespace lgtbot { + +namespace game { + +namespace GAME_MODULE_NAME { + +GAME_TEST(1, player_not_enough) +{ + ASSERT_FALSE(StartGame()); +} + +} // namespace GAME_MODULE_NAME + +} // namespace game + +} // namespace lgtbot + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + gflags::ParseCommandLineFlags(&argc, &argv, true); + return RUN_ALL_TESTS(); +} From ea07cf214421adc350541cf2e4d7e97d5fcb7829 Mon Sep 17 00:00:00 2001 From: tiedanGH <2295824927@qq.com> Date: Thu, 12 Mar 2026 23:53:17 -0400 Subject: [PATCH 12/12] long_night: refactor game options, new block mode, rule update --- games/long_night/achievements.h | 2 +- games/long_night/board.h | 178 +++++++----- games/long_night/boss.h | 34 +-- games/long_night/constants.h | 187 +++++++++---- games/long_night/map.h | 280 +++++++++++-------- games/long_night/mygame.cc | 480 +++++++++++++++++++------------- games/long_night/options.h | 42 ++- games/long_night/player.h | 92 +++++- games/long_night/rule.md | 22 +- games/long_night/unittest.cc | 33 +++ 10 files changed, 850 insertions(+), 500 deletions(-) diff --git a/games/long_night/achievements.h b/games/long_night/achievements.h index bb3b946..1d0fc51 100644 --- a/games/long_night/achievements.h +++ b/games/long_night/achievements.h @@ -6,4 +6,4 @@ EXTEND_ACHIEVEMENT(饥渴难耐, "在第一回合捕捉目标玩家") EXTEND_ACHIEVEMENT(乒铃乓啷, "路过或触发 5 种及以上不同的地形") EXTEND_ACHIEVEMENT(嗜杀成性, "4人及以上对局中,捕捉所有其他玩家") EXTEND_ACHIEVEMENT(守株待兔, "在单个回合中未进行移动并成功捕捉目标玩家") -EXTEND_ACHIEVEMENT(牛头魅魔, "持续被米诺陶斯锁定,BOSS移动 4 步仍未被抓到") +EXTEND_ACHIEVEMENT(牛头魅魔, "持续被米诺陶斯锁定,BOSS移动 3 步仍未被抓到") diff --git a/games/long_night/board.h b/games/long_night/board.h index 570d59d..75b0873 100644 --- a/games/long_night/board.h +++ b/games/long_night/board.h @@ -8,15 +8,15 @@ struct GetBoardOptions { class Board { public: - Board(string image_path, const int32_t image_type, const int32_t mode, const vector& custom_blocks) - : image_path_(std::move(image_path)), image_type_(image_type), gamemode(mode), g(std::random_device{}()), unitMaps(mode, g, custom_blocks) {} + Board(string image_path, const Texture image_texture, const BlockMode mode, const vector& custom_blocks) + : image_path_(std::move(image_path)), image_texture_(image_texture), block_mode(mode), g(std::random_device{}()), unitMaps(mode, g, custom_blocks) {} // 随机引擎 std::mt19937 g; // 图片资源文件夹 const string image_path_; - const int32_t image_type_; + const Texture image_texture_; // 玩家 uint32_t playerNum; @@ -27,7 +27,7 @@ class Board int size = 9; // 地图 vector> grid_map; - int gamemode; + BlockMode block_mode; int exit_num; // 逃生舱数量 string init_html_; // 区块模板 @@ -165,55 +165,69 @@ class Board } // 全部区块信息展示 - string GetAllBlocksInfo(const int special, const bool bomb_mode, const int32_t test_mode = 0) const + string GetAllBlocksInfo(const SpecialEvent event, const bool bomb_mode, const optional test_block_mode = std::nullopt) const { - int col_num = (test_mode == 1 || test_mode == 3) ? 8 : 4; - const vector& maps = test_mode == 0 ? unitMaps.maps : (test_mode == 2 ? unitMaps.twist_maps : unitMaps.all_maps); - const vector& exits = test_mode == 0 ? unitMaps.exits : (test_mode == 2 ? unitMaps.twist_exits : unitMaps.all_exits); - const vector& special_maps = unitMaps.special_maps; - const string title = - (test_mode == 0) ? "" - : (test_mode == 2) ? - (HTML_SIZE_FONT_HEADER(6) "《漫漫长夜》幻变模式轮换区块" HTML_FONT_TAIL) - : (HTML_SIZE_FONT_HEADER(6) "《漫漫长夜》狂野+疯狂模式全部区块" HTML_FONT_TAIL); - bool has_special = (test_mode == 3); + const BlockMode test_mode = test_block_mode.value_or(BlockMode::CLASSIC); + + string title = ""; + int col_num = (test_mode == BlockMode::CRAZY) ? 8 : 4; + const auto& maps = [&]() -> const std::vector& { + if (test_mode == BlockMode::CLASSIC) return unitMaps.maps; + if (test_mode == BlockMode::CRAZY) return unitMaps.all_maps; + return unitMaps.pool_maps; + }(); + const auto& exits = [&]() -> const std::vector& { + if (test_mode == BlockMode::CLASSIC) return unitMaps.exits; + if (test_mode == BlockMode::CRAZY) return unitMaps.all_exits; + return unitMaps.pool_exits; + }(); + const auto& all_special_maps = unitMaps.all_special_maps; + + // 调试:根据模式生成完整区块预览 + switch (test_mode) { + case BlockMode::TWIST: title = HTML_SIZE_FONT_HEADER(6) "《漫漫长夜》「幻变」模式轮换区块" HTML_FONT_TAIL; break; + case BlockMode::CRAZY: title = HTML_SIZE_FONT_HEADER(6) "《漫漫长夜》「狂野」+「疯狂」模式全部区块" HTML_FONT_TAIL; break; + case BlockMode::BUTTON: title = HTML_SIZE_FONT_HEADER(6) "《漫漫长夜》「按钮」模式主题区块" HTML_FONT_TAIL; break; + case BlockMode::TRAP: title = HTML_SIZE_FONT_HEADER(6) "《漫漫长夜》「陷阱」模式主题区块" HTML_FONT_TAIL; break; + default:; + } + + bool has_special = (test_mode == BlockMode::CRAZY); for (const auto& map : maps) { if (map.is_special) { has_special = true; } } + SpecialEvent rain_event = event == SpecialEvent::RAINSTORY ? SpecialEvent::RAINSTORY : SpecialEvent::NONE; - int line_num = (ceil(maps.size() / (double) col_num) + ceil(exits.size() / (double) col_num)) * 2 + + has_special * (ceil(special_maps.size() / (double) col_num) * 2 + 1); + int line_num = (ceil(maps.size() / (double) col_num) + ceil(exits.size() / (double) col_num)) * 2 + + has_special * (ceil(all_special_maps.size() / (double) col_num) * 2 + 1); html::Table blocks(line_num, col_num); - blocks.SetTableStyle("align=\"center\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-size: 25px; line-height: 1.2;\""); + blocks.SetTableStyle("align=\"center\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-size: 25px; line-height: 1.1;\""); int row = 0; for (int i = 0; i < maps.size(); i++) { - blocks.Get(row, i % col_num).SetStyle("style=\"padding: 25px 25px 0 25px\"").SetContent(GetSingleBlock(0, maps[i].id, special)); - // 特殊地图不显示id - if (maps[i].is_special && gamemode >= 0) { + blocks.Get(row, i % col_num).SetStyle("style=\"padding: 25px 25px 5px 25px\"").SetContent(GetSingleBlock(maps[i].id, false, rain_event)); + // [疯狂模式] 特殊地图不显示id + if (maps[i].is_special && block_mode == BlockMode::CRAZY) { blocks.Get(row + 1, i % col_num).SetContent(HTML_COLOR_FONT_HEADER(red) HTML_SIZE_FONT_HEADER(6) "???" HTML_FONT_TAIL HTML_FONT_TAIL); } else { - blocks.Get(row + 1, i % col_num).SetContent(HTML_COLOR_FONT_HEADER(red) "" + maps[i].id + "
" - HTML_COLOR_FONT_HEADER(#990000) HTML_SIZE_FONT_HEADER(4) + maps[i].title + HTML_FONT_TAIL HTML_FONT_TAIL "
" HTML_FONT_TAIL); + blocks.Get(row + 1, i % col_num).SetContent(GetMapTitleContent(maps[i].title, maps[i].id, false)); } if ((i + 1) % col_num == 0 || i == maps.size() - 1) row += 2; } for (int i = 0; i < exits.size(); i++) { - blocks.Get(row, i % col_num).SetStyle("style=\"padding: 20px 25px 0 25px\"").SetContent(GetSingleBlock(1, exits[i].id, special)); - blocks.Get(row + 1, i % col_num).SetContent(HTML_COLOR_FONT_HEADER(red) "EXIT " + exits[i].id + "
" - HTML_COLOR_FONT_HEADER(#990000) HTML_SIZE_FONT_HEADER(4) + exits[i].title + HTML_FONT_TAIL HTML_FONT_TAIL "
" HTML_FONT_TAIL); + blocks.Get(row, i % col_num).SetStyle("style=\"padding: 20px 25px 5px 25px\"").SetContent(GetSingleBlock(exits[i].id, true, rain_event)); + blocks.Get(row + 1, i % col_num).SetContent(GetMapTitleContent(exits[i].title, exits[i].id, true)); if ((i + 1) % col_num == 0 || i == exits.size() - 1) row += 2; } - if (has_special && gamemode >= 0) { + if (has_special && block_mode == BlockMode::CRAZY) { blocks.MergeRight(row, 0, col_num); blocks.Get(row++, 0).SetStyle("style=\"padding: 20px 25px 5px 25px\"") .SetContent(HTML_SIZE_FONT_HEADER(6) "特殊区块列表
" HTML_FONT_TAIL HTML_SIZE_FONT_HEADER(5) "(多传送门按照图中字母代号传送)" HTML_FONT_TAIL "
"); - for (int i = 0; i < special_maps.size(); i++) { - blocks.Get(row, i % col_num).SetStyle("style=\"padding: 20px 25px 0 25px\"") - .SetContent(GetBoard(unitMaps.FindBlockById(special_maps[i].id, false, special == 3), GetBoardOptions{.with_content = true})); - blocks.Get(row + 1, i % col_num).SetContent(HTML_COLOR_FONT_HEADER(red) "" + special_maps[i].id + "
" - HTML_COLOR_FONT_HEADER(#990000) HTML_SIZE_FONT_HEADER(4) + special_maps[i].title + HTML_FONT_TAIL HTML_FONT_TAIL "
" HTML_FONT_TAIL); - if ((i + 1) % col_num == 0 || i == special_maps.size() - 1) row += 2; + for (int i = 0; i < all_special_maps.size(); i++) { + blocks.Get(row, i % col_num).SetStyle("style=\"padding: 20px 25px 5px 25px\"") + .SetContent(GetBoard(unitMaps.FindBlockById(all_special_maps[i].id, false, SpecialEvent::NONE), GetBoardOptions{.with_content = true})); + blocks.Get(row + 1, i % col_num).SetContent(GetMapTitleContent(all_special_maps[i].title, all_special_maps[i].id, false)); + if ((i + 1) % col_num == 0 || i == all_special_maps.size() - 1) row += 2; } } @@ -233,7 +247,7 @@ class Board { GridType::ONEWAYPORTAL, "【传送门出口】玩家进入时会发出其他人听见的**啪啪声**。(出生不算)
传送门的单向出口,进入时不会触发传送(必须从入口进入才会传送至此处)
**玩家在进入同一区块的传送门入口时,传送门会转换方向,入口和出口交换位置**" }, { GridType::TRAP, "【陷阱】陷阱隐藏在树丛中:被奇数次进入时,会发出让其他人听见的**沙沙声**(出生不算)
被偶数次进入时,不发出声响,并**强制玩家停止**(出生不算)" }, { GridType::HEAT, "【热源】进入热源周围8格时,将**私信**收到热浪提示。(只有移动时才能感受到热浪)
当进入热源时,将**私信**收到高温烫伤提示(不会出生在热源内)
在整局游戏中,**当第 2 次或更多次进入热源时,会被强制停止行动**" }, - { GridType::EXIT, "【逃生舱】逃生者使用后,**会消失**。" + (test_mode == 0 ? ("本局逃生舱数量为 **" + to_string(exit_num) + "** 个。") : "") }, + { GridType::EXIT, "【逃生舱】逃生者使用后,**会消失**。" + (test_mode == BlockMode::CLASSIC ? ("本局逃生舱数量为 **" + to_string(exit_num) + "** 个。") : "") }, }; auto GenerateWallStyle = [&](Wall wall, const string& direction) { @@ -244,7 +258,7 @@ class Board vector all_maps_in_game; all_maps_in_game.insert(all_maps_in_game.end(), maps.begin(), maps.end()); all_maps_in_game.insert(all_maps_in_game.end(), exits.begin(), exits.end()); - if (has_special && gamemode >= 0) all_maps_in_game.insert(all_maps_in_game.end(), special_maps.begin(), special_maps.end()); + if (has_special && block_mode == BlockMode::CRAZY) all_maps_in_game.insert(all_maps_in_game.end(), all_special_maps.begin(), all_special_maps.end()); int legend_max_size = all_walls_info.size() + all_attachs_info.size() + all_grids_info.size() + 1; html::Table legend(legend_max_size, 2); @@ -277,7 +291,7 @@ class Board } for (const auto& grid_info: all_grids_info) { if (UnitMaps::MapContainGridType(all_maps_in_game, grid_info.first)) { - if (grid_info.first == GridType::GRASS && special == 3) continue; + if (grid_info.first == GridType::GRASS && event == SpecialEvent::RAINSTORY) continue; legend.Get(row, 0).SetContent(GetGridStyle(grid_info.first, AttachType::EMPTY, false)); legend.Get(row, 1).SetContent(grid_info.second); row++; @@ -287,23 +301,23 @@ class Board return title + blocks.ToString() + legend.ToString(); } - string GetSingleBlock(const int type, const string& id, const int special) const + string GetMapTitleContent(const string& title, const string& id, const bool is_exit) const { - // 特殊地图不显示预览(自定义模式除外) - if (id[0] == 'S' && gamemode >= 0) { + return "" HTML_COLOR_FONT_HEADER(#990000) HTML_SIZE_FONT_HEADER(4) + title + HTML_FONT_TAIL HTML_FONT_TAIL + "
" HTML_COLOR_FONT_HEADER(red) + (is_exit ? "EXIT " : "") + id + HTML_FONT_TAIL "
"; + } + + string GetSingleBlock(const string& id, const bool is_exit, const SpecialEvent event) const + { + // [疯狂模式] 特殊地图不显示预览 + if (id[0] == 'S' && block_mode == BlockMode::CRAZY) { string size = to_string(GRID_SIZE * 3 + WALL_SIZE * 4) + "px"; return "
" "特殊区块
"; } - vector> grid; - if (type == 0) { - grid = unitMaps.FindBlockById(id, false, special == 3); - } else if (type == 1) { - grid = unitMaps.FindBlockById(id, true); - } - return GetBoard(grid, GetBoardOptions{.with_content = true}); + return GetBoard(unitMaps.FindBlockById(id, is_exit, event), GetBoardOptions{.with_content = true}); } // 获取玩家信息 @@ -364,7 +378,7 @@ class Board } // 完整赛况 - string GetAllRecordHtml(const int query_pid) const + string GetAllRecordHtml(const int query_pid, const int is_public) const { const char* style = R"(