diff --git a/bot_core/bot_ctx.cc b/bot_core/bot_ctx.cc index 37766475..047d5180 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; } diff --git a/bot_core/match.cc b/bot_core/match.cc index 86c34bf6..afabf24c 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.cc b/game_framework/stage_utility.cc index 1387aa90..3d93985e 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 af2ccd00..0099bb6c 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); @@ -110,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; diff --git a/games/escape_building/achievements.h b/games/escape_building/achievements.h new file mode 100644 index 00000000..41900af8 --- /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 00000000..fd572a35 --- /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 00000000..24179967 Binary files /dev/null and b/games/escape_building/icon.png differ diff --git a/games/escape_building/mygame.cc b/games/escape_building/mygame.cc new file mode 100644 index 00000000..eb1261f6 --- /dev/null +++ b/games/escape_building/mygame.cc @@ -0,0 +1,716 @@ +// 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); + Main().player_scores_[0] = Main().player_scores_[1] = -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 00000000..e69de29b diff --git a/games/escape_building/options.h b/games/escape_building/options.h new file mode 100644 index 00000000..d59181ac --- /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 00000000..62f878f0 --- /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 00000000..3882d757 --- /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(); +} diff --git a/games/garnet_thief/mygame.cc b/games/garnet_thief/mygame.cc index 857aae27..ef00d58a 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 8dfb6a61..2da1e467 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 60724160..d4a56e61 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 分配规则** diff --git a/games/keep_drawing/achievements.h b/games/keep_drawing/achievements.h new file mode 100644 index 00000000..e69de29b diff --git a/games/keep_drawing/icon.png b/games/keep_drawing/icon.png new file mode 100644 index 00000000..e4959c3a Binary files /dev/null and b/games/keep_drawing/icon.png differ diff --git a/games/keep_drawing/mygame.cc b/games/keep_drawing/mygame.cc new file mode 100644 index 00000000..c3012087 --- /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 = "
胜利分数:" + std::to_string(GAME_OPTION(分数)) + "
"; + + 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 00000000..e69de29b diff --git a/games/keep_drawing/options.h b/games/keep_drawing/options.h new file mode 100644 index 00000000..30b3b1a8 --- /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 00000000..65dd4a29 --- /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 00000000..1e75f1bf --- /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(); +} diff --git a/games/long_night/achievements.h b/games/long_night/achievements.h index e69de29b..1d0fc51b 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移动 3 步仍未被抓到") diff --git a/games/long_night/board.h b/games/long_night/board.h index d094895a..75b08735 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 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 Texture image_texture_; // 玩家 uint32_t playerNum; @@ -21,38 +27,42 @@ class Board int size = 9; // 地图 vector> grid_map; + BlockMode block_mode; 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"); + map.Get(x, y).SetStyle("class=\"corner\" style=\"background-image: url('" + ToFileUrl(image_path_ + GetImageTypeFolder() + "walls/corner.png") + "');\""); } } - return style + map.ToString(); - } - - // 获取墙壁上的提示文本 - string GetWallContent(const vector>& grid_map, const int x, const int y, const Direct direction) const - { - const int d = static_cast(direction); - const int od = static_cast(opposite(direction)); - const vector vec1 = grid_map[x][y].GetContent().second; - if (d >= 0 && d < (int)vec1.size() && !vec1[d].empty()) { - return vec1[d]; - } - int nx = x + k_DX_Direct[d]; - int ny = y + k_DY_Direct[d]; - if (0 <= nx && nx < grid_map.size() && 0 <= ny && ny < grid_map.size()) { - const vector vec2 = grid_map[nx][ny].GetContent().second; - if (od >= 0 && od < (int)vec2.size() && !vec2[od].empty()) { - return vec2[od]; - } - } - return ""; + return GetBoardStyle() + map.ToString(); } + // 获取终局对比盘面 string GetFinalBoard() const { html::Table finalTable(2, 2); @@ -166,139 +161,138 @@ 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 SpecialEvent event, const bool bomb_mode, const optional test_block_mode = std::nullopt) const { - const vector& maps = test_mode == 0 ? unitMaps.maps : (test_mode == 2 ? unitMaps.rotation_maps : unitMaps.all_maps); - const vector& exits = test_mode == 0 ? unitMaps.exits : (test_mode == 2 ? unitMaps.rotation_exits : unitMaps.all_exits); - const vector& special_maps = unitMaps.special_maps; - const string title = - (test_mode == 0) ? "" - : (test_mode == 2) ? - (HTML_SIZE_FONT_HEADER(6) "《漫漫长夜》幻变模式轮换区块" HTML_FONT_TAIL) - : (HTML_SIZE_FONT_HEADER(6) "《漫漫长夜》狂野+疯狂模式全部区块" HTML_FONT_TAIL); - bool has_special = (test_mode == 3); + 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.type == GridType::SPECIAL) { + if (map.is_special) { has_special = true; } } + SpecialEvent rain_event = event == SpecialEvent::RAINSTORY ? SpecialEvent::RAINSTORY : SpecialEvent::NONE; - 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(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.1;\""); int row = 0; for (int i = 0; i < maps.size(); i++) { - blocks.Get(row, i % 4).SetStyle("style=\"padding: 25px 25px 0px 25px\"").SetContent(GetSingleBlock(0, maps[i].id, special)); - // 特殊地图不显示id - blocks.Get(row + 1, i % 4).SetContent(HTML_COLOR_FONT_HEADER(red) "" + (maps[i].id[0] == 'S' ? "???" : maps[i].id) + "" HTML_FONT_TAIL); - if ((i + 1) % 4 == 0 || i == maps.size() - 1) row += 2; + 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(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 % 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 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) { - blocks.MergeRight(row, 0, 4); + 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 % 4).SetStyle("style=\"padding: 20px 25px 5px 25px\"") - .SetContent(GetBoard(unitMaps.FindBlockById(special_maps[i].id, false, special == 3), GetBoardOptions{.with_content = true})); - blocks.Get(row + 1, i % 4).SetContent(HTML_COLOR_FONT_HEADER(red) "" + special_maps[i].id + "" HTML_FONT_TAIL); - if ((i + 1) % 4 == 0 || i == special_maps.size() - 1) row += 2; + 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; } } - 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 == BlockMode::CLASSIC ? ("本局逃生舱数量为 **" + 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 && block_mode == BlockMode::CRAZY) all_maps_in_game.insert(all_maps_in_game.end(), all_special_maps.begin(), all_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)); + 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++; } @@ -307,43 +301,184 @@ 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') 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 "" 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 GetAllRecord() const + string GetSingleBlock(const string& id, const bool is_exit, const SpecialEvent event) const { - string all_record; + // [疯狂模式] 特殊地图不显示预览 + if (id[0] == 'S' && block_mode == BlockMode::CRAZY) { + string size = to_string(GRID_SIZE * 3 + WALL_SIZE * 4) + "px"; + return "
" + "特殊区块
"; + } + return GetBoard(unitMaps.FindBlockById(id, is_exit, event), GetBoardOptions{.with_content = true}); + } + + // 获取玩家信息 + string GetPlayerTable(const int round) const + { + 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(); } - static string clean_markdown(const string& input) + string GetPlayerString() 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 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 int is_public) 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, is_public); + 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, is_public); + 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; + } + + string GetAllRecordString(const int query_pid, const int is_public) const + { + 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, is_public, false) + "\n"; + } + // BOSS 记录 + if (boss.Enable()) { + record_string += "[BOSS] " + boss.GetBossName() + " " + boss.GetBossIcon(); + record_string += boss.GetBossRecord(query_pid, is_public, 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 +518,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 +559,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 +583,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 +603,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,15 +618,46 @@ class Board if (it != oldCell.end()) oldCell.erase(it); } - // 地区声响(0无 1沙沙 2啪啪) - Sound GetSound(const Grid& grid, const bool water_mode) const + // 解析多步行动:将输入字符串解析为方向数组 + 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 SpecialEvent event) const { switch(grid.Type()) { case GridType::GRASS: return Sound::SHASHA; case GridType::WATER: return Sound::PAPA; case GridType::ONEWAYPORTAL: case GridType::PORTAL: return Sound::PAPA; - case GridType::TRAP: return grid.TrapStatus() ? Sound::NONE : (water_mode ? Sound::PAPA : Sound::SHASHA); + case GridType::TRAP: return grid.TrapStatus() ? Sound::NONE : (event == SpecialEvent::RAINSTORY ? Sound::PAPA : Sound::SHASHA); default: return Sound::NONE; } } @@ -513,7 +693,7 @@ class Board // 两玩家在同一位置 if (dx == 0 && dy == 0) { - return ""; + return "同格"; } if (dx == 0) { @@ -559,24 +739,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 +783,7 @@ class Board } } } - if (boss.steps >= 0) { + if (boss.Enable()) { forbiddenSources.push_back({boss.x, boss.y}); } for (const auto& player: players) { @@ -614,14 +809,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); @@ -635,20 +829,20 @@ class Board } // 捕捉顺位变更 - void UpdatePlayerTarget(const bool next) + void UpdatePlayerTarget(const Target type) { for (PlayerID pid = 0; pid < playerNum; ++pid) { if (players[pid].out > 0) continue; - PlayerID target = next ? (pid + 1) % playerNum : (playerNum + pid - 1) % playerNum; + PlayerID target = type == Target::NEXT ? (pid + 1) % playerNum : (playerNum + pid - 1) % playerNum; while (players[target].out > 0 && target != pid) { players[target].target = -1; - target = next ? (target + 1) % playerNum : (playerNum + target - 1) % playerNum; + target = type == Target::NEXT ? (target + 1) % playerNum : (playerNum + target - 1) % playerNum; } players[pid].target = target; } } - string MapGenerate(const vector& map_str) const + string MapPreview(const vector& map_str) const { vector> grid_map; grid_map.resize(size); @@ -667,9 +861,9 @@ class Board map_id = str; } if (is_exit) { - unitMaps.SetExitBlock(unitMaps.origin_pos[i].first, unitMaps.origin_pos[i].second, grid_map, map_id, false); + unitMaps.SetExitBlock(unitMaps.origin_pos[i].first, unitMaps.origin_pos[i].second, grid_map, map_id, SpecialEvent::NONE); // 预览时需取用原始地图 } else { - unitMaps.SetMapBlock(unitMaps.origin_pos[i].first, unitMaps.origin_pos[i].second, grid_map, map_id, false); + unitMaps.SetMapBlock(unitMaps.origin_pos[i].first, unitMaps.origin_pos[i].second, grid_map, map_id, SpecialEvent::NONE); // 预览时需取用原始地图 } } FixAdjacentWalls(grid_map); @@ -677,12 +871,12 @@ class Board return GetBoard(grid_map, GetBoardOptions{.with_content = true}); } - int ExitCount() const + int TypeCount(const GridType type) const { int count = 0; for (int x = 0; x < size; x++) { for (int y = 0; y < size; y++) { - if (grid_map[x][y].Type() == GridType::EXIT) { + if (grid_map[x][y].Type() == type) { count++; } } @@ -690,142 +884,63 @@ 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); + int total_pos = unitMaps.pos.size(); + int selected_map_num = 0; + int selected_exit_num = 0; + + if (block_mode != BlockMode::CUSTOM) { + // 非自定义模式 + selected_map_num = total_pos - exit_num; + selected_exit_num = exit_num; + } else { + // 自定义模式 + int map_count = maps.size(); + int exit_count = exits.size(); + if (exit_count < exit_num) { + // 情况1:逃生舱数量不足exit_num,使用全部逃生舱 + selected_exit_num = exit_count; + selected_map_num = std::min(map_count, total_pos - selected_exit_num); + } else { + int required_map_num = total_pos - exit_num; + if (map_count >= required_map_num) { + // 情况2:区块数量充足,exit_num个逃生舱 + selected_exit_num = exit_num; + selected_map_num = required_map_num; + } else { + // 情况3:区块不足,使用全部普通区块 + selected_map_num = map_count; + selected_exit_num = total_pos - selected_map_num; + selected_exit_num = std::min(selected_exit_num, exit_count); + } + } + } + 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) { - unitMaps.SetExitBlock(unitMaps.pos[i].first, unitMaps.pos[i].second, grid_map, selected[i].id, true); + if (selected[i].is_exit) { + unitMaps.SetExitBlock(unitMaps.pos[i].first, unitMaps.pos[i].second, grid_map, selected[i].id, SpecialEvent::RANDOM); // 此处特殊事件仅用于调用原始地图 } else { - unitMaps.SetMapBlock(unitMaps.pos[i].first, unitMaps.pos[i].second, grid_map, selected[i].id, true); + unitMaps.SetMapBlock(unitMaps.pos[i].first, unitMaps.pos[i].second, grid_map, selected[i].id, SpecialEvent::RANDOM); // 此处特殊事件仅用于调用原始地图 } // 记录特殊地图坐标(暂不使用) - // if (selected[i].type == GridType::SPECIAL) { + // if (selected[i].is_special) { // special_pos = unitMaps.pos[i]; // } } @@ -842,23 +957,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 +986,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 +1261,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_texture_) { + case Texture::CLASSIC: return "classic/"; + case Texture::RETRO: 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 00000000..7724b898 --- /dev/null +++ b/games/long_night/boss.h @@ -0,0 +1,196 @@ + +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_public, const bool is_html = true) const + { + string result; + + for (const auto& mv : boss_all_record) { + string sound_d; + if (mv.sound != Sound::NONE && !is_public) { + 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 BossType type) + { + this->type = type; + 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; } +}; diff --git a/games/long_night/constants.h b/games/long_night/constants.h new file mode 100644 index 00000000..6f9f300c --- /dev/null +++ b/games/long_night/constants.h @@ -0,0 +1,434 @@ + +#pragma once + +#include +#include + +/* ========== constants ========== */ +inline constexpr int GRID_SIZE = 50; +inline constexpr int WALL_SIZE = 10; + +inline constexpr std::string_view num[10] = {"⓪", "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨"}; +inline constexpr std::string_view dir_cn[4] = {"上", "下", "左", "右"}; +inline constexpr int k_DX_Direct[4] = {-1, 1, 0, 0}; +inline constexpr int k_DY_Direct[4] = {0, 0, -1, 1}; + +inline constexpr std::string_view mode_str[7] = {"自定义", "经典", "狂野", "幻变", "疯狂", "按钮", "陷阱"}; + +inline constexpr int HIDE_LIMIT = 4; + +inline constexpr int EXTRATIMECRAD_COUNT = 3; +inline constexpr int EXTRATIMECRAD_TIME = 60; + + +/* ========== enum class ========== */ +// 方向 +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, +}; + +// BOSS +enum class BossType { + NONE, + MINOTAUR, + BANGBANG, +}; + + +/* ========== Game Option enum class ========== */ +enum class BlockMode { + CUSTOM, + CLASSIC, + WILD, + TWIST, + CRAZY, + BUTTON, + TRAP, +}; + +enum class SpecialEvent { + NONE, + RANDOM, + LAZYGARDENER, + OVERGROWTH, + RAINSTORY, +}; + +enum class Target { + PREVIOUS, + NEXT, +}; + +enum class HideMode { + NONE, + TURN, + STEP, +}; + +enum class Texture { + CLASSIC, + RETRO, +}; + + +/* ========== utils ========== */ +inline const std::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}, +}; + +// 图片名称 +inline std::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"; + } +} + +inline std::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"; + } +} + +inline std::string GetWallImage(const Wall wall, const std::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"; + } +} + +// 相反方向 +inline 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"); +} + +// 计算两点间曼哈顿距离 +inline 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; +} + +inline bool CompareMapId(const std::string& a, const std::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; + } + return std::stoi(a) < std::stoi(b); +} + +// UTF-8字符长度 +inline 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换行为文本换行 +inline std::string replace_br_with_line(std::string s) +{ + const std::string from = "
"; + const std::string to = "\n"; + size_t pos = 0; + while ((pos = s.find(from, pos)) != std::string::npos) { + s.replace(pos, from.length(), to); + pos += to.length(); + } + return s; +} + +// 转义 html/markdown +inline std::string esc_html(const std::string& input) +{ + std::string result = input; + result = regex_replace(result, std::regex("&"), "&"); + result = regex_replace(result, std::regex("<"), "<"); + result = regex_replace(result, std::regex(">"), ">"); + result = regex_replace(result, std::regex("\""), """); + result = regex_replace(result, std::regex("'"), "'"); + return result; +} + +inline std::string clean_markdown(const std::string& input) +{ + std::string result = input; + result = regex_replace(result, std::regex(R"(\*)"), R"(\*)"); + result = regex_replace(result, std::regex(R"(_)"), R"(\_)"); + result = regex_replace(result, std::regex(R"(\[)"), R"(\[)"); + result = regex_replace(result, std::regex(R"(\])"), R"(\])"); + result = regex_replace(result, std::regex(R"(`)"), R"(\`)"); + return result; +} + +// 根据系统转换文件URL +inline std::string ToFileUrl(const std::string& path) +{ + std::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; +} + +/* ========== 游戏文案 ========== */ +inline std::string GetRandomHint(std::span hints) +{ + return std::string(hints[rand() % hints.size()]); +} + +// 撞墙提示 +inline const std::array wall_hints = { + "砰!你狠狠地撞在了一堵墙上!", // 铁蛋 + "一阵剧痛传来,你撞上了一堵墙,看来这里走不通。", + "黑暗中,你的身体撞击了一面粗糙的墙壁。", + "你听到一声沉闷的回响——是墙壁挡住了你的去路。", + "墙壁在你面前横亘,仿佛无情地阻挠着前行的路。", + "墙壁冷漠地矗立在你面前,拒绝让你通过。", + "前方一堵高墙挡住了你的去路。", + "你试图继续前进,但一堵墙挡住了你的去路。", + "砰!你撞上了一堵墙,幸好没人看到。", + "你决定挑战墙壁,结果墙壁胜利了。", + "看来你的“穿墙术”还没练成。", + "你试图与墙壁讲道理,但它完全不想搭理你。", + "你向前冲去,然后优雅地和墙壁来了个亲密接触。", + "你试图用意念穿过墙壁,然而它比你的意念还坚定。", + "黑暗中,你的手掌摸到一堵砖墙上,你期待的邂逅没有发生。", // 大萝卜姬 + "噢是墙壁,你情不自禁地把耳朵贴了上去,你很失望。", + "是一面光滑湿软的墙!快趁机误导一下对手!信我的邪,你发出了只能自己听见的啪啪声。", + "你的手掌按在冰冷的墙面上,它仿佛正在吞噬你的温度。你是南方人啊,试试舌头吧!", + "彭!靠北啦,你不看路的吗?拜托~这么大一面墙你就这样撞啊。", + "砰!你撞到了墙上。看来你数错了,9又3/4并不是那么好找的。", + "你似乎走到了墙面上?快醒醒,这里并不是匹诺康尼,别做白日梦了。", // 三月七 + "这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。", + "你申请对墙壁过一个说服,dm拒绝了你。", + "你尝试使用闪现过墙,但可惜墙体的厚度超出了你的预期。", + "墙壁温柔的注视着你,不再言语。", + "仿生铁蛋bot会梦到电子萝卜吗?至少墙壁不会给你答案。", + "你在平原上走着,突然迎面遇到一堵墙,这墙向上无限高,向下无限深,向左无限远,向右无限远,这墙是什么?\n当然不可能有这样的墙,无论材质是什么,都会因为无限大的重力坍缩的。这只是那些神经兮兮的糟老头子的臆想罢了。\n不过,你确实撞在了一堵墙上。", // H3PO4 + "黑暗中突然出现的墙壁,像是命运在说:换个方向试试?", // 月影 + "你试图用脸测量墙壁的硬度,恭喜获得物理系荣誉学位!", + "砰!你与墙壁进行了深入交流,结论是它比你想象的更固执。", + "砰!脑门和墙壁的亲密接触,证明了你对探索的执着!", // 小葵 +}; +// 第一步撞墙提示 +inline const std::array firststep_wall_hints = { + "这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。\n这是什么?墙壁?撞一下。", // 三月七 + "你知道吗,撞击同一面墙114514次即可将其撞倒!", + "看来有人认为自己在玩多层迷宫……别想了,这是永久墙壁。", + "俗话说不撞南墙不回头,但有人撞了南墙也不回头。", +}; +// 树丛提示 +inline const std::array grass_hints = { + "你踏入一片树丛,枯叶和树枝在脚下沙沙作响。", // 铁蛋 + "你一脚踏入了一片树丛,树叶发出了沙沙声,仿佛某种回应。", + "树叶微微颤动,沙沙声仿佛在轻声低语。", + "脚下传出“沙沙”的声音……你希望这只是树叶,而不是别的东西。", + "你踏入一片树丛,沙沙声在寂静的黑暗里显得格外刺耳。", + "枝叶在你身上扫过,沙沙声中,它刮破了你的蚕丝薄衫和渔网袜。", // 大萝卜姬 + "你突然跌进了一片黑暗的树丛,诡异的沙沙声勾起了你不好的回忆。", + "随着一阵沙沙声,密集的枝杈无情地划开了你的衣物和皮肤。", + "你路过一片树丛,出现了野生的妙蛙种子!快使用大师球!", // 三月七 + "你踩到了一根萝卜……等等,树林里怎么会有萝卜?", + "沙沙,沙沙,这片丛林的背后,会不会住着小红帽?", + "沙沙,你踏入了危险的树丛。这里要是藏着一个老六,可就遭了……", // Hyacinth +}; +// 啪啪提示 +inline const std::array papa_hints = { + "你向前动了动,下半身传来了啪啪声。", // 大萝卜姬 + "啪啪!常在迷宫走,哪有不湿鞋?是的,你踩到了某些液体。", + "啪啪!你问踩的是什么液体?也许是我为那情人留下的泪。", + "啪啪!冰冷的液体渗入了你的鞋里。", + "啪!尽管你动作已经很轻,但还是发出了很大的声音。啪!你决定不管了。", + "啪啪!是谁那么不讲公德!随地……", + "啪啪!冰冷的涟漪在你的鞋边回荡,你想起了那个陪着铁蛋看冰块的下午。", + "啪啪!突如其来的声音让你的动作瞬间冻结;被打破的静谧仿佛被劈开的水,迅速地恢复了无声。你的敏锐好像没有得到回应。", + "啪嗒!你好像踩到了地雷?低头看一看,还好,只是一些液体。", // 三月七 + "啪!你似乎有什么东西掉了进去,可惜这里并没有河神。", +}; +// 陷阱触发提示 +inline const std::array trap_hints = { + "深阱垂空百尺方,足悬铁索断人肠。", // 齐齐 + "犹似魂断垓下道,恨满胸中万古刀。", + "你想起守株待兔的故事,只是此刻,你成为了那只兔子。", // 纤光 + "为坠落的人类命名:_________", + "恭喜🎉被特斯拉捕获,电击调教一回合", // 特斯拉 +}; +// 热浪提示 +inline const std::array heat_wave_hints = { + "你感受到了迎面扑来的热浪,炽热的空气仿佛要将你吞没", // 铁蛋 + "你感受到周围弥漫着炽热的气息!", + "!!请注意!!局部出现厄尔尼诺现象,气温异常升高,请注意做好防中暑措施。", // 纤光 + "你感到难以忍受的炎热,“要是周围有个水池就好了…”", + "在这座冰冷的迷宫里,你感到一阵久违的温暖,周围似乎有明亮的光源,吸引你一探究竟…?", +}; +// 热源进入提示 +inline const std::array heat_core_hints = { + "你的脚被高温烫伤了,刺痛让你不由得倒吸一口凉气,然而周围的空气同样炙热无比", // 铁蛋 + "然而,光源并不总象征着安全,烈焰利用人对光明的向往,试图再次吞噬一个失落的灵魂。", // 纤光 + "oops!检测到核心温度急剧上升,即将超过阈值…准备启动自毁程序…", + "你相信自己的铜头铁臂可以击败一切,却不知眼前的岩浆能轻易融化所有金属。", +}; +// 热源公开提示 +inline const std::array heat_active_hints = { + "你被滚滚热浪淹没了...周围的一切都在高温中扭曲变形,只剩下火焰的深渊。", // 铁蛋 + "哦不!你落入了巨人萝卜的火锅池里,这下你只好成为萝卜的夜宵了。", // 纤光 + "你失败了!\nSteve试图在岩浆中游泳。", +}; +// 炸弹爆炸提示 +inline const std::array bomb_hints = { + "轰——你脚下的土地猛地炸裂,烈焰与烟尘把你吞没,眼前一片黑暗。", // 铁蛋 + "轰隆!炸弹瞬间炸响,你被高高弹起,如同流星坠入未知深渊。", + "炽热的冲击波把你掀起,四周的碎石与烟尘如同流星雨,你在空中划出最后的弧线。", + "耳边传来震耳欲聋的巨响,火光吞噬了一切,你的身影消失在滚滚浓烟之中。", + "世界在强光中褪色成负片,最后涌入意识的不是疼痛,而是童年某个夏日的蝉鸣。", + "好消息:你确实飞起来了。坏消息:是以分子扩散的形式。", + "这可能是你最亮眼的时刻——字面意义上的。", + "你刚刚完成了一次无需火箭推进的轨道发射。遗憾的是,没有返回舱。", +}; +// 逃生舱提示 +inline const std::array exit_hints = { + "你坐进了逃生舱,在启动的轰鸣声中,你想起了那句话:“不要忘了,这个世界穿透一切高墙的东西,它就在我们的内心深处,他们无法达到,也接触不到,那就是希望。”", // 大萝卜姬 + "躺在逃生舱内,平日并不虔诚的你颤巍巍地画着十字,双手合十,嘴里念念有词。诸如什么真主阿拉耶稣基督释迦牟利急急如律令之类。前窗仿佛响应了你的号召,一阵白色闪光迅速笼罩了你。正当你诧异得到了哪位神仙的庇佑时,眼前浮现出两个大字。一个振奋人心的声音在你耳边响起:“原神,启动!”", + "你忘记躺了多久,你只记得这里很温暖、舒适、令人安心。直到一股力量把你从舱内抽离;强光穿透了你稚嫩的眼皮;你哭了,你向世界宣告着你的降临。也许你只是此刻降生的其中之一,但在她眼里,你就是她的唯一。", + "是的。夜色再浓,也挡不住黎明的到来,就像再大的困难也挡不住我们的前进。黑暗即将过去,曙光就在前头!", + "“我们城市崇尚和平,人人都善良和谐自觉拥护一切美好。是因为我们坚信堵不如疏。每年的今天我们都会举办 [长夜节]。在这一天我们可以放下所有原则,释放心中的恶意并且不被追究责任。”今年优胜的你当然可以如此说道。", + "“恭喜逃生!请选择:①清空记忆并返回现实②复活所有选手重新逃杀”\n“②”你斩钉截铁道。\n无论还要重来多少次,我只要那个有你的未来。", + "你和同伴睁开眼睛,熟练且迅速地摘下入耳式共享梦境机。\n“我觉得我们应该给足暗示了吧”同伴迅速地收拾着周围散落的仪器。\n“给的足足的,关键是他会认为是自己这样想的。他一定会给我们升至少两个职级。”你立马摘下床上目标的梦境机,与同伴一起安静又麻利地从窗户离开了卧室。", + "你出生后 时间就已所剩无几\n在妈妈离世之后 我不知对你倾注了多少的爱呢\n但是你的微笑让爸爸备受鼓舞呀(^_^)\n其实要是能一起走就好了 但没能做到\n希望你能忘记一切继续前行 你一定可以做到的", // 纤光 + "当逃生舱门缓缓关闭,伴随着沉闷的启动声,黑暗迷宫逐渐远去。依靠在逃生舱内冷冽的仪表光芒中,你仿佛听见遥远星辰的低语:“未来,总为勇者留下一缕希望。”", // 铁蛋 + "舱门缓缓关闭,逃生舱的指示灯一一亮起,冰冷的金属包裹着你,但比起外面的黑暗,这里却意外地令人安心。你知道,一切都已经结束,或许,也是一切的开始。", + "你说的对,但是《漫漫长夜》是由大萝卜姬自主研发的一款全新大逃杀游戏。游戏发生在一个被称作“黑暗迷宫”的地图,在这里,被铁蛋选中的人将被授予“树丛”,导引“沙沙”之力。你将扮演一位名为“狩猎者”的神秘角色,在自由的旅行中邂逅性格各异、能力独特的墙壁,和它们一起阻拦对手,找到隐匿的“逃生者”——同时,逐步发掘“逃生舱”的真相。", // 三月七 + "进入逃生舱后,随着几下逐渐变弱的震动,周围的环境随之稳定下来。也就在这时,你眼前闪过一道白光,似乎是这使得你进入了一个全新的环境,伴随着的还有来自外部的一阵欢呼声:“太好了!成功抓住宝可梦了!”", // faust + "一阵失重后,舱门终于打开。随着刺眼的白光,在指缝间你看见几个面目可憎的巨人在围观你。很快地你被巨大的餐叉粗暴地刺穿;顾不及对痛觉反应,你便殒命在血盘大口之中。健硕、坚定、智慧、乐观,这些优秀的品质在他们嘴里同样珍贵。", // 大萝卜姬 + "当逃生舱的舱门关闭时,你才发现手中的钥匙根本不属于这里。系统提示音冰冷地重复着:身份验证失败。原来从一开始,你就只是这个迷宫的装饰品而已。", // 月影 + "逃生舱启动的瞬间,你突然想起那个古老的预言:'逃出迷宫的人将获得永生,但代价是永远孤独'。舱体剧烈震动起来,不知是故障还是某种警告...", + "舱内显示屏突然亮起:'恭喜您成为第1024位逃生者!作为奖励,系统将向您展示迷宫的真相...'画面切换的瞬间,你看到了无数个一模一样的逃生舱,里面坐着无数个一模一样的你。", + "当逃生舱启动时,你突然明白:黑暗不是终点,而是黎明前的温柔。舱内温度逐渐升高,那不是故障,而是新生的心跳。", + "逃生舱启动的轰鸣声渐渐平息,取而代之的是轻柔的摇篮曲。透过舷窗,你看见繁星组成的银河缓缓流动——原来迷宫的出口,一直连接着整片宇宙。", + "当舱门完全关闭的瞬间,你听见系统轻声说:'恭喜,这是第1024次模拟。根据数据,你这次终于选择相信自己了。' 周围突然亮起温暖的阳光,原来真正的逃生舱,一直都在你心里。", + "逃生舱的显示屏突然亮起一行字:'记住,黑暗只是光明的候车室。' 随着这句话,整个舱体开始散发出柔和的金色光芒,照亮了通往新世界的道路。", +}; +// 捕捉提示 +inline const std::array catch_hints = { + "星光黯淡,你们的相遇,是命中注定,亦是命终注定。", // 纤光 + "你化作一道黑影,在血月之下,无情地终结了又一条生命。", + "你想触碰一切的真相,但在对方空洞无神的双眼中,你没能找到答案。", + "你叹了口气,在黑暗森林里,你不得不这样做。", + "我说我杀人不眨眼,你问我眼睛干不干?永别了", // 克里斯丁 + "感谢你为了我自愿放弃逃生资格", +}; +// 同格树丛声响提示 +inline const std::array grass_sound_hints = { + "你听见有人进入了你的小树丛,沙沙声很近很近;一股接一股热气扑向了你的耳朵;呼…哈……呼…哈……他好像很累的样子。积极地想,他也许没有察觉到你", // 大萝卜姬 + "你听见有人进入了你所在的树丛,他从旁边匆匆走过,没有发现你。阴暗的想法在你心里成长起来,是让他帮你探路,还是直接干掉。甚至运气好的话,抢在前面牛了他的逃生舱……(额外探索分只有1分并逃生一事在漫漫长夜中亦有记载)", // Hyacinth +}; +// 同格啪啪声响提示 +inline const std::array papa_sound_hints = { + "“啪!”你汗毛直立,有人来了。。幸好,你并没有站在中间,多疑多虑的性格给了你久违的回报。你小心翼翼地蹲了下来,尽力减少接触概率。只是黑暗中你低估了脚下液体的深度。“等他走远,再把内裤脱了吧。。”你暗暗地想。", // 大萝卜姬 + "啪!啪!看来是有人来了。两个人,狭小的隔间,不间断地啪啪声……'淫秽的人!'你的脑海回想起了她的声音。是啊。我承认,我确实有点想她了。", +}; +// 无逃生舱最后生还 +inline const std::array withoutE_win_hints = { + "我睁开了双眼,眼前的一切既熟悉又陌生。看来这次终于是我赢了。我用力地端详着周遭的一切,试图捕捉错过的几日时光的任何蛛丝马迹。“我真希望他们彻底离开了 ......”说完,我在床脚拿起了本该在枕边的剃须刀;“看来上次赢的是萝卜。”我下意识地抹了抹嘴唇。在指尖晕开的口红证实了我的猜测。我笑了。", // 大萝卜姬 + "你醒啦?现在已经是第二天了哦。\n明媚的阳光照进迷宫,耳旁传来小鸟的叫声,一切美好的不太真实,唯有眼前冰冷的血迹,无声的诉说着昨晚的那场噩梦,而有些人,永远留在了那场梦中。\n可你,真的从中逃出来了吗?\n“地形参数设置完毕,新的循环正在重启……”", // 纤光 +}; +// 有逃生舱但死斗取胜 +inline const std::array withE_win_hints = { + "“已经没事啦~”她温柔地从背后把你抱住,轻轻抚摸着头。“你很勇敢,这一步太不容易了。”你转过身,紧紧埋进她柔软的身体里放肆哭泣。“一切都结束了。不用再害怕了。”她低下头凑近你的耳边,“咱们回家叭…”", // 大萝卜姬 + "多年以后,在家族背景下你在事业上取得了巨大成就。大家把你的性情大变归功于当年失踪逃生的经历。“之前那个玩世不恭的我已经死了。”你每次都这样认真回答大家。至于细节的提问嘛,失忆这个理由你很喜欢。", +}; diff --git a/games/long_night/grid.h b/games/long_night/grid.h index 41744989..3c9569e8 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 e8b8dfd5..956bf15c 100644 --- a/games/long_night/map.h +++ b/games/long_night/map.h @@ -1,271 +1,201 @@ // 感谢 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; + const int k_special_num = 2; vector> pos = { {0, 0}, {0, 3}, {0, 6}, {3, 0}, {3, 3}, {3, 6}, {6, 0}, {6, 3}, {6, 6}, }; - vector> origin_pos; + vector> origin_pos = pos; 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", "女卫生间", GridType::TRAP, AttachType::BUTTON}, + {Map40(), "40", "男卫生间", GridType::TRAP, AttachType::BUTTON}, + {Map41(), "41", "红石迷宫A", GridType::GRASS, AttachType::BUTTON}, + {Map42(), "42", "红石迷宫B", GridType::GRASS, AttachType::BUTTON}, + // {Map31(), "31", "单向门A", GridType::PORTAL}, + // {Map32(), "32", "单向门B", 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"}, + vector all_special_maps = { + {MapS1(), "S1", "实验场", GridType::HEAT, AttachType::EMPTY, true}, + {MapS2(), "S2", "原子空间", GridType::PORTAL, AttachType::EMPTY, true}, + {MapS3(), "S3", "原子阱", GridType::PORTAL, AttachType::EMPTY, true}, + {MapS4(), "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_mode_ids = { + "2", "3", "4", "6", + "7", "8", "11", "12", + "17", "18", "21", "22", + "23", "24", "25", "26", + "37", "38", "39", "40", + "E1", "E2", "E3", "E4", + "E9", "E10", + }; + const vector button_mode_ids = { + "2", "6", "3", "7", + "4", "8", "9", "10", + "21", "22", "25", "26", + "27", "37", "33", "34", + "35", "36", "39", "40", + "E1", "E2", "E3", "E4", + "E11", "E12", }; - const vector rotation_exits_id = {"1", "2", "3", "4"}; - vector rotation_maps; - vector rotation_exits; + const vector trap_mode_ids = { + "2", "6", "9", "10", + "19", "20", "21", "22", + "23", "24", "25", "27", + "31", "32", "39", "40", + "16", "S3", + "E1", "E2", "E3", "E4", + "E9", "E10", + }; + vector pool_maps; + vector pool_exits; vector maps; vector exits; + vector special_maps; + + UnitMaps() = default; - UnitMaps(const int32_t mode) + UnitMaps(const BlockMode mode, std::mt19937& gen, const vector& custom_blocks): g(std::make_unique(gen)) { - std::random_device rd; - g = std::mt19937(rd()); - if (mode == 0) { + if (mode == BlockMode::CLASSIC) { + // 经典-CLASSIC 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); + } else if (mode == BlockMode::TWIST) { + // 幻变-TWIST + SampleBlockPoolsFromIds(twist_mode_ids); + } else if (mode == BlockMode::WILD) { + // 狂野-WILD + 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); + } else if (mode == BlockMode::CRAZY) { + // 疯狂-CRAZY + std::sample(all_special_maps.begin(), all_special_maps.end(), std::back_inserter(special_maps), k_special_num, *g); + maps.insert(maps.end(), special_maps.begin(), special_maps.end()); + std::sample(all_maps.begin(), all_maps.end(), std::back_inserter(maps), k_map_num - k_special_num, *g); + SampleExits(all_exits, k_exit_num / 2); + } else if (mode == BlockMode::BUTTON) { + // 按钮-BUTTON + SampleBlockPoolsFromIds(button_mode_ids); + } else if (mode == BlockMode::TRAP) { + // 陷阱-TRAP + SampleBlockPoolsFromIds(trap_mode_ids); + } else { + // 自定义-CUSTOM + for (const auto& block : custom_blocks) { + if (auto it = std::find_if(all_special_maps.begin(), all_special_maps.end(), [&](const Map& m) { return m.id == block; }); + it != all_special_maps.end()) { + maps.push_back(*it); } - } - 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); + 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); } } - std::sample(rotation_maps.begin(), rotation_maps.end(), std::back_inserter(maps), k_map_num, g); - SampleExits(rotation_exits, k_exit_num / 2); - } else { - std::sample(special_maps.begin(), special_maps.end(), std::back_inserter(maps), 2, g); - std::sample(all_maps.begin(), all_maps.end(), std::back_inserter(maps), k_map_num - 2, g); - SampleExits(all_exits, k_exit_num / 2); } - origin_pos = pos; + } + + void SampleBlockPoolsFromIds(const vector& mode_ids) + { + pool_maps.clear(); + pool_exits.clear(); + for (const auto& map_id : mode_ids) { + const bool is_exit = !map_id.empty() && map_id[0] == 'E'; + const bool is_special = !map_id.empty() && map_id[0] == 'S'; + const std::string id = is_exit ? map_id.substr(1) : map_id; + + const auto& source = is_exit ? all_exits : (is_special ? all_special_maps : all_maps); + auto it = std::find_if(source.begin(), source.end(), [&id](const Map& m) { return m.id == id; }); + + if (it != source.end()) { + if (is_exit) pool_exits.push_back(*it); + else pool_maps.push_back(*it); + } + } + + std::sample(pool_maps.begin(), pool_maps.end(), std::back_inserter(maps), k_map_num, *g); + SampleExits(pool_exits, k_exit_num / 2); + + std::sort(maps.begin(), maps.end(), [](const Map& a, const Map& b) { return CompareMapId(a.id, b.id); }); + std::sort(exits.begin(), exits.end(), [](const Map& a, const Map& b) { return CompareMapId(a.id, b.id); }); } void SampleExits(const vector& exits_pool, const int k_exit_pair) @@ -275,7 +205,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) { @@ -284,16 +214,40 @@ class UnitMaps { } } - vector> FindBlockById(const string id, const bool is_exit, const bool special = false) const + vector> FindBlockById(const string& id, bool is_exit, SpecialEvent event) const { - const vector& search_list = is_exit ? (special ? exits : all_exits) : (special ? maps : all_maps); - auto it = std::find_if(search_list.begin(), search_list.end(), [id](const Map& map) { return map.id == id; }); - if (it != search_list.end()) return it->block; - auto special_it = std::find_if(special_maps.begin(), special_maps.end(), [id](const Map& map) { return map.id == id; }); - if (special_it != special_maps.end()) return special_it->block; + const bool has_event = event != SpecialEvent::NONE; + + const auto& special_list = has_event ? special_maps : all_special_maps; + if (const Map* map = FindMapById(special_list, id)) + return map->block; + + const auto& normal_list = is_exit + ? (has_event ? exits : all_exits) + : (has_event ? maps : all_maps); + if (const Map* map = FindMapById(normal_list, id)) + return map->block; + return InitializeMapTemplate(); } + bool IsBlockExist(const string& id, bool is_exit) const + { + if (FindMapById(all_special_maps, id)) + return true; + + const auto& list = is_exit ? all_exits : all_maps; + return FindMapById(list, id) != nullptr; + } + + const Map* FindMapById(const vector& maps, const string& id) const + { + auto it = std::find_if(maps.begin(), maps.end(), + [&id](const Map& map) { return map.id == id; }); + + return it != maps.end() ? &(*it) : nullptr; + } + static bool MapContainGridType(const vector& maps, const GridType& type) { for (const auto& map: maps) { @@ -306,6 +260,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) { @@ -318,17 +284,24 @@ class UnitMaps { return false; } + static SpecialEvent GetRandomSpecialEvent() + { + static constexpr std::array events = { + SpecialEvent::LAZYGARDENER, + SpecialEvent::OVERGROWTH, + SpecialEvent::RAINSTORY, + }; + return events[rand() % events.size()]; + } + // 特殊事件详情 - static string ShowSpecialEvent(const int type) - { - if (type == 1) { - return "[特殊事件]【怠惰的园丁】树丛将在其区块内随机位置生成(有可能生成在中间)"; - } else if (type == 2) { - return "[特殊事件]【营养过剩】树丛和陷阱将额外向随机1个方向再次生成1个树丛(不可隔墙生长)"; - } else if (type == 3) { - return "[特殊事件]【雨天小故事】地图中所有树丛变成水洼,陷阱会发出啪啪声"; - } else { - return "无"; + static string ShowSpecialEvent(const SpecialEvent event) + { + switch (event) { + case SpecialEvent::LAZYGARDENER: return "[特殊事件]【怠惰的园丁】树丛将在其区块内随机位置生成(有可能生成在中间)"; + case SpecialEvent::OVERGROWTH: return "[特殊事件]【营养过剩】树丛和陷阱将额外向随机1个方向再次生成1个树丛(不可隔墙生长)"; + case SpecialEvent::RAINSTORY: return "[特殊事件]【雨天小故事】地图中所有树丛变成水洼,陷阱会发出啪啪声"; + default: return "[特殊事件]【无】"; } } @@ -363,9 +336,10 @@ class UnitMaps { MarkMaps(all_maps); MarkMaps(all_exits); - MarkMaps(special_maps); + MarkMaps(all_special_maps); ProcessMaps(maps); ProcessMaps(exits); + ProcessMaps(special_maps); } // 特殊事件2——营养过剩:树丛和陷阱将额外向随机1个方向再次生成1个树丛 @@ -397,7 +371,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; @@ -408,9 +382,10 @@ class UnitMaps { MarkMaps(all_maps); MarkMaps(all_exits); - MarkMaps(special_maps); + MarkMaps(all_special_maps); ProcessMaps(maps); ProcessMaps(exits); + ProcessMaps(special_maps); } // 特殊事件3——雨天小故事:地图中所有树丛变成水洼 @@ -430,6 +405,7 @@ class UnitMaps { ProcessMaps(maps); ProcessMaps(exits); ProcessMaps(special_maps); + ProcessMaps(all_special_maps); } // 大地图区块位置随机 @@ -441,7 +417,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) { @@ -465,17 +441,19 @@ class UnitMaps { origin_pos = pos; } - void SetMapBlock(const int x, const int y, vector>& grid_map, const string& map_id, const bool special) const + void SetMapBlock(const int x, const int y, vector>& grid_map, const string& map_id, const SpecialEvent event) const { - SetBlock(x, y, grid_map, FindBlockById(map_id, false, special)); + SetBlock(x, y, grid_map, FindBlockById(map_id, false, event)); } - void SetExitBlock(const int x, const int y, vector>& grid_map, const string& exit_id, const bool special) const + void SetExitBlock(const int x, const int y, vector>& grid_map, const string& exit_id, const SpecialEvent event) const { - SetBlock(x, y, grid_map, FindBlockById(exit_id, true, special)); + SetBlock(x, y, grid_map, FindBlockById(exit_id, true, event)); } 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 +504,7 @@ class UnitMaps { return map; } + /* ========== 常规地图 ========== */ static vector> Map1() { auto map = InitializeMapTemplate(); @@ -1039,7 +1018,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 +1027,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 +1035,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 +1049,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 +1074,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 +1088,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 +1108,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 +1130,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 +1151,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).SetGrowable(true); + 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).SetGrowable(true); 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).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", "", "", ""}); + 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).SetGrowable(true); + + 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).SetGrowable(true); + map[0][2].SetWall(Wall::NORMAL, Wall::DOOROPEN, Wall::DOOR, Wall::EMPTY); + + map[1][0].SetWall(Wall::DOOR, Wall::DOOROPEN, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::DOOROPEN, Wall::DOOR, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::DOOROPEN, Wall::NORMAL, Wall::EMPTY, Wall::DOOR); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOR, Wall::DOOROPEN).SetGrowable(true); + map[2][2].SetWall(Wall::DOOR, Wall::NORMAL, Wall::DOOROPEN, Wall::EMPTY); return map; } - // static vector> Map35() + 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).SetGrowable(true); + + map[2][0].SetWall(Wall::DOOROPEN, Wall::NORMAL, Wall::NORMAL, Wall::DOOR); + map[2][1].SetWall(Wall::DOOROPEN, Wall::EMPTY, Wall::DOOR, Wall::EMPTY).SetGrowable(true); + map[2][2].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + 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).SetGrowable(true); + map[0][1].SetWall(Wall::EMPTY, Wall::DOOROPEN, Wall::EMPTY, Wall::DOOR).SetGrowable(true); + 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).SetGrowable(true); + + 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).SetGrowable(true); + + 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).SetGrowable(true); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOROPEN, Wall::DOOR).SetGrowable(true); + map[0][2].SetWall(Wall::DOOR, Wall::DOOROPEN, Wall::DOOR, Wall::DOOROPEN).SetGrowable(true); + + map[1][0].SetWall(Wall::DOOR, Wall::DOOROPEN, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::DOOROPEN, Wall::DOOR, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::DOOROPEN, Wall::DOOR, Wall::DOOROPEN, Wall::DOOR).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOR, Wall::DOOROPEN).SetGrowable(true); + map[2][2].SetWall(Wall::DOOR, Wall::DOOROPEN, Wall::DOOROPEN, Wall::DOOR).SetGrowable(true); + + 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).SetGrowable(true); + map[0][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOROPEN, Wall::DOOROPEN).SetGrowable(true); + map[0][2].SetWall(Wall::DOOR, Wall::DOOR, Wall::DOOROPEN, Wall::DOOROPEN).SetGrowable(true); + + map[1][0].SetWall(Wall::DOOR, Wall::DOOR, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + map[1][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::EMPTY, Wall::EMPTY); + map[1][2].SetWall(Wall::DOOR, Wall::DOOR, Wall::EMPTY, Wall::EMPTY).SetGrowable(true); + + map[2][0].SetWall(Wall::DOOR, Wall::DOOR, Wall::DOOROPEN, Wall::DOOROPEN).SetGrowable(true); + map[2][1].SetWall(Wall::EMPTY, Wall::EMPTY, Wall::DOOROPEN, Wall::DOOROPEN).SetGrowable(true); + map[2][2].SetWall(Wall::DOOR, Wall::DOOR, Wall::DOOROPEN, Wall::DOOROPEN).SetGrowable(true); + + return map; + } + + // static vector> Map43() // { // auto map = InitializeMapTemplate(); // map[0][0].SetType(GridType::WATER); @@ -1227,6 +1388,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,8 +1656,50 @@ class UnitMaps { return map; } - // 特殊地图 - static vector> SMap1() + 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> MapS1() { auto map = InitializeMapTemplate(); map[0][0].SetType(GridType::WATER); @@ -1478,14 +1727,14 @@ class UnitMaps { return map; } - static vector> SMap2() + static vector> MapS2() { 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); @@ -1502,14 +1751,14 @@ class UnitMaps { return map; } - static vector> SMap3() + static vector> MapS3() { 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); @@ -1526,18 +1775,18 @@ class UnitMaps { return map; } - static vector> SMap4() + static vector> MapS4() { 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 8c0f50d2..1ea8c372 100644 --- a/games/long_night/mygame.cc +++ b/games/long_night/mygame.cc @@ -4,8 +4,7 @@ #include #include -#include -#include +#include #include "game_framework/stage.h" #include "game_framework/util.h" @@ -13,8 +12,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,22 +29,122 @@ 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[5] = { + 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", + + R"EOF(【开局区块随机】 +游戏开始时,将根据不同是游戏模式从不同的区块池抽取区块。 +默认情况下,逃生舱数量为本局玩家人数的一半。但12*12地图逃生舱固定为4个 +【自定义模式】 +因自定义模式逃生舱池可能为任意数量,根据不同情况采用如下的随机方案: +- 情况1:逃生舱数量不足[默认数量],使用全部逃生舱 +- 情况2:逃生舱数量充足,同时区块数量充足,逃生舱为[默认数量] +- 情况3:当逃生舱为[默认数量]时,普通区块数量不足。使用全部普通区块,并抽取额外的逃生舱补足至需要的区块总数)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}, {"区块", 4}})), }; -const std::vector k_rule_commands = {}; bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const GenericOptions& generic_options_readonly, MutableGenericOptions& generic_options) { - if (GET_OPTION_VALUE(game_options, 特殊事件) == -1) { - GET_OPTION_VALUE(game_options, 特殊事件) = rand() % 3 + 1; + 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, 模式) = BlockMode::CUSTOM; + std::sort(vaild_blocks.begin(), vaild_blocks.end(), CompareMapId); + custom_blocks = vaild_blocks; + } + + if (GET_OPTION_VALUE(game_options, 特殊事件) == SpecialEvent::RANDOM) { + GET_OPTION_VALUE(game_options, 特殊事件) = UnitMaps::GetRandomSpecialEvent(); } if (generic_options_readonly.PlayerNum() > 6 && GET_OPTION_VALUE(game_options, 边长) < 12) { GET_OPTION_VALUE(game_options, 边长) = 12; @@ -51,43 +153,126 @@ bool AdaptOptions(MsgSenderBase& reply, CustomOptions& game_options, const Gener return true; } +enum class InitOption { + // ===== 区块模式 ===== + BLOCK_CLASSIC, + BLOCK_TWIST, + BLOCK_WILD, + BLOCK_CRAZY, + BLOCK_BUTTON, + BLOCK_TRAP, + + // ===== 边长 ===== + BOARD_10, + BOARD_12, + + // ===== 特殊事件 ===== + EVENT_RANDOM, + EVENT_LAZYGARDENER, + EVENT_OVERGROWTH, + EVENT_RAINSTORY, + + // ===== 游戏模式 ===== + MODE_BATTLEROYALE, + MODE_HIDDEN_TURN, + MODE_HIDDEN_STEP, + MODE_POINTKILL, + MODE_NON_POINTKILL, + MODE_PLANNING, + MODE_BOMBER, + + // ===== BOSS ===== + BOSS_MINOTAUR, + BOSS_BANGBANG, + + // ===== 其他配置 ===== + TARGET_PREVIOUS, + TARGET_NEXT, + STOP_PRIVATE, + TEXTURE_RETRO, + + // ===== 启动模式 ===== + SINGLE_USER, +}; + 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& init_options) { - 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 (const InitOption& option : init_options) { + switch (option) { + case InitOption::BLOCK_CLASSIC: GET_OPTION_VALUE(game_options, 模式) = BlockMode::CLASSIC; break; + case InitOption::BLOCK_TWIST: GET_OPTION_VALUE(game_options, 模式) = BlockMode::TWIST; break; + case InitOption::BLOCK_WILD: GET_OPTION_VALUE(game_options, 模式) = BlockMode::WILD; break; + case InitOption::BLOCK_CRAZY: GET_OPTION_VALUE(game_options, 模式) = BlockMode::CRAZY; break; + case InitOption::BLOCK_BUTTON: GET_OPTION_VALUE(game_options, 模式) = BlockMode::BUTTON; break; + case InitOption::BLOCK_TRAP: GET_OPTION_VALUE(game_options, 模式) = BlockMode::TRAP; break; + + case InitOption::BOARD_10: GET_OPTION_VALUE(game_options, 边长) = 10; break; + case InitOption::BOARD_12: GET_OPTION_VALUE(game_options, 边长) = 12; break; + + case InitOption::EVENT_RANDOM: GET_OPTION_VALUE(game_options, 特殊事件) = SpecialEvent::RANDOM; break; + case InitOption::EVENT_LAZYGARDENER: GET_OPTION_VALUE(game_options, 特殊事件) = SpecialEvent::LAZYGARDENER; break; + case InitOption::EVENT_OVERGROWTH: GET_OPTION_VALUE(game_options, 特殊事件) = SpecialEvent::OVERGROWTH; break; + case InitOption::EVENT_RAINSTORY: GET_OPTION_VALUE(game_options, 特殊事件) = SpecialEvent::RAINSTORY; break; + + case InitOption::MODE_BATTLEROYALE: GET_OPTION_VALUE(game_options, 大乱斗) = true; break; + case InitOption::MODE_HIDDEN_TURN: GET_OPTION_VALUE(game_options, 隐匿) = HideMode::TURN; break; + case InitOption::MODE_HIDDEN_STEP: GET_OPTION_VALUE(game_options, 隐匿) = HideMode::STEP; break; + case InitOption::MODE_POINTKILL: GET_OPTION_VALUE(game_options, 点杀) = true; break; + case InitOption::MODE_NON_POINTKILL: GET_OPTION_VALUE(game_options, 点杀) = false; break; + case InitOption::MODE_PLANNING: GET_OPTION_VALUE(game_options, 谋定后动) = true; break; + case InitOption::MODE_BOMBER: GET_OPTION_VALUE(game_options, 炸弹) = 1; break; + + case InitOption::BOSS_MINOTAUR: GET_OPTION_VALUE(game_options, BOSS) = BossType::MINOTAUR; break; + case InitOption::BOSS_BANGBANG: GET_OPTION_VALUE(game_options, BOSS) = BossType::BANGBANG; break; + + case InitOption::TARGET_PREVIOUS: GET_OPTION_VALUE(game_options, 捕捉目标) = Target::PREVIOUS; break; + case InitOption::TARGET_NEXT: GET_OPTION_VALUE(game_options, 捕捉目标) = Target::NEXT; break; + case InitOption::STOP_PRIVATE: GET_OPTION_VALUE(game_options, 停止私信) = true; break; + case InitOption::TEXTURE_RETRO: GET_OPTION_VALUE(game_options, 纹理) = Texture::RETRO; break; + + case InitOption::SINGLE_USER: 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{ + {"经典", InitOption::BLOCK_CLASSIC}, + {"幻变", InitOption::BLOCK_TWIST}, + {"狂野", InitOption::BLOCK_WILD}, + {"疯狂", InitOption::BLOCK_CRAZY}, + {"按钮", InitOption::BLOCK_BUTTON}, + {"陷阱", InitOption::BLOCK_TRAP}, + + {"10*10", InitOption::BOARD_10}, + {"12*12", InitOption::BOARD_12}, + + {"随机", InitOption::EVENT_RANDOM}, + {"怠惰的园丁", InitOption::EVENT_LAZYGARDENER}, + {"营养过剩", InitOption::EVENT_OVERGROWTH}, + {"雨天小故事", InitOption::EVENT_RAINSTORY}, + + {"大乱斗", InitOption::MODE_BATTLEROYALE}, + {"回合隐匿", InitOption::MODE_HIDDEN_TURN}, + {"单步隐匿", InitOption::MODE_HIDDEN_STEP}, + {"点杀", InitOption::MODE_POINTKILL}, + {"关闭点杀", InitOption::MODE_NON_POINTKILL}, + {"谋定后动", InitOption::MODE_PLANNING}, + {"炸弹人", InitOption::MODE_BOMBER}, + + {"米诺陶斯", InitOption::BOSS_MINOTAUR}, + {"邦邦", InitOption::BOSS_BANGBANG}, + + {"上家", InitOption::TARGET_PREVIOUS}, + {"下家", InitOption::TARGET_NEXT}, + {"停止私信", InitOption::STOP_PRIVATE}, + {"复古", InitOption::TEXTURE_RETRO}, + + {"单机", InitOption::SINGLE_USER}, })), }; @@ -105,7 +290,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]; } @@ -116,6 +301,9 @@ class MainStage : public MainGameStage int round_; // 地图 Board board; + + // 多人全员停止/超时结束游戏判定 + bool all_active_stop = true; // 无逃生舱最后生还胜利判定 bool withoutE_win_ = false; // 无逃生舱最后生还判定 @@ -128,13 +316,13 @@ class MainStage : public MainGameStage CompReqErrCode BlockInfo_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) { auto sender = reply(); - if (GAME_OPTION(特殊事件) > 0) { + if (GAME_OPTION(特殊事件) != SpecialEvent::NONE) { sender << UnitMaps::ShowSpecialEvent(GAME_OPTION(特殊事件)) << "\n"; } 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,24 +330,32 @@ 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.MapPreview(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); // 用于生成全部地图列表 srand((unsigned int)time(NULL)); + // 调试:用于生成全部地图列表 + // Global().SaveMarkdown(board.GetAllBlocksInfo(SpecialEvent::NONE, true, BlockMode::CRAZY), ((GRID_SIZE + WALL_SIZE) * 16 + 40) * 2); + // board.unitMaps.SampleBlockPoolsFromIds(board.unitMaps.twist_mode_ids); // [幻变]模式 + // Global().SaveMarkdown(board.GetAllBlocksInfo(SpecialEvent::NONE, true, BlockMode::TWIST), ((GRID_SIZE + WALL_SIZE) * 16 + 40)); + // board.unitMaps.SampleBlockPoolsFromIds(board.unitMaps.button_mode_ids); // [按钮]模式 + // Global().SaveMarkdown(board.GetAllBlocksInfo(SpecialEvent::NONE, true, BlockMode::BUTTON), ((GRID_SIZE + WALL_SIZE) * 16 + 40)); + // board.unitMaps.SampleBlockPoolsFromIds(board.unitMaps.trap_mode_ids); // [陷阱]模式 + // Global().SaveMarkdown(board.GetAllBlocksInfo(SpecialEvent::NONE, true, BlockMode::TRAP), ((GRID_SIZE + WALL_SIZE) * 16 + 40)); + auto sender = Global().Boardcast(); - if (!GAME_OPTION(捉捕目标)) { - sender << "【捉捕顺位】本局玩家的捉捕顺位为相反顺序,捉捕目标变更为上家\n\n"; + if (GAME_OPTION(捕捉目标) == Target::NEXT) { + sender << "【捕捉顺位】本局玩家的捕捉顺位为相反顺序,捕捉目标变更为下家\n\n"; } - if (GAME_OPTION(特殊事件) > 0) { // 特殊事件 + if (GAME_OPTION(特殊事件) != SpecialEvent::NONE) { // 特殊事件 switch (GAME_OPTION(特殊事件)) { - case 1: board.unitMaps.SpecialEvent1(); break; - case 2: board.unitMaps.SpecialEvent2(); break; - case 3: board.unitMaps.SpecialEvent3(); break; + case SpecialEvent::LAZYGARDENER: board.unitMaps.SpecialEvent1(); break; + case SpecialEvent::OVERGROWTH: board.unitMaps.SpecialEvent2(); break; + case SpecialEvent::RAINSTORY: board.unitMaps.SpecialEvent3(); break; } sender << UnitMaps::ShowSpecialEvent(GAME_OPTION(特殊事件)) << "\n\n"; } @@ -167,7 +363,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,28 +375,32 @@ class MainStage : public MainGameStage if (GAME_OPTION(点杀)) { // 点杀 sender << "【点杀模式】捕捉改为仅在回合结束时触发,路过不会触发捕捉\n"; } - if (GAME_OPTION(隐匿) == 1) { // 隐匿 + if (GAME_OPTION(隐匿) == HideMode::TURN) { // 隐匿 sender << "【隐匿模式】回合隐匿:隐匿效果持续1回合,**仅可使用1次**,隐匿后当回合的行动转为私聊进行,不会发出声响,不会触发捕捉。\n"; - } else if (GAME_OPTION(隐匿) == 2) { - sender << "【隐匿模式】单步隐匿:隐匿效果仅作用于下一步,**可使用" << hide_limit << "次**,隐匿后在私聊行动一步,不会发出声响,不会触发捕捉。\n"; + } else if (GAME_OPTION(隐匿) == HideMode::STEP) { + 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(隐匿) == HideMode::TURN) { // 隐匿 board.players[pid].hide_remaining = 1; - } else if (GAME_OPTION(隐匿) == 2) { - board.players[pid].hide_remaining = hide_limit; + } else if (GAME_OPTION(隐匿) == HideMode::STEP) { + board.players[pid].hide_remaining = HIDE_LIMIT; } + if (GAME_OPTION(炸弹) > 0) board.players[pid].bomb = GAME_OPTION(炸弹); // 炸弹人 } - board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); + board.UpdatePlayerTarget(GAME_OPTION(捕捉目标)); // 出口数 if (GAME_OPTION(边长) != 12) { if (Global().PlayerNum() > 1) { @@ -212,37 +412,32 @@ class MainStage : public MainGameStage // 单机模式 if (Global().PlayerNum() == 1) board.players[0].target = 100; // 初始化地图 - board.Initialize(GAME_OPTION(BOSS)); + board.Initialize(); + board.exit_num = board.TypeCount(GridType::EXIT); - 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) != BossType::NONE) { + board.boss.BossInitialize(GAME_OPTION(BOSS)); // 初始化BOSS + board.boss.InitBossStartRecord(); + sender << "【BOSS】" + board.boss.GetBossStartInfo() + "\n"; + sender << "当前 BOSS 锁定的玩家为 " << At(board.boss.target) << "\n"; } - sender << "[提示] 本局游戏人数为 " << board.playerNum << " 人,逃生舱数量为 " << board.exit_num << " 个。请留意私信发送的开局墙壁信息"; + board.SaveGameStartMap(); // 保存初始盘面 - // 开局私信墙壁信息 - for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { - auto [info, md] = board.GetSurroundingWalls(pid); - board.players[pid].private_record = "【开局】\n您所在位置的四周墙壁信息,按照 上下左右 顺序分别是:\n" + info; - Global().Tell(pid) << board.players[pid].private_record << "\n" << Markdown(md, 110); - } - - string mode_str[4] = {"标准", "狂野", "幻变", "疯狂"}; - sender << "\n\n【区块模式】" + mode_str[GAME_OPTION(模式)]; - if (GAME_OPTION(模式) > 0) { - sender << ":本局可能出现的区块种类详见下图所示:\n"; - sender << Markdown(board.GetAllBlocksInfo(GAME_OPTION(特殊事件)), 1000); - } + sender << "[提示] 本局游戏人数为 " << board.playerNum << " 人,逃生舱数量为 " << board.exit_num << " 个。请留意私信发送的开局墙壁信息\n\n"; + sender << "【区块模式】" << mode_str[static_cast(GAME_OPTION(模式))] << "\n"; + sender << "本局可能出现的区块详见下图所示:\n"; + sender << Markdown(board.GetAllBlocksInfo(GAME_OPTION(特殊事件), GAME_OPTION(炸弹) > 0), (GRID_SIZE + WALL_SIZE) * 16 + 40); setter.Emplace(*this, ++round_); } void NextStageFsm(RoundStage& sub_stage, const CheckoutReason reason, SubStageFsmSetter setter) { - bool game_end = (Alive_() <= 1 && Global().PlayerNum() > 1) || ((Alive_() <= 0 || board.ExitCount() == 0) && Global().PlayerNum() == 1); - if (!game_end && round_ < 20) { + bool game_end = + ((Alive_() <= 1 || all_active_stop) && Global().PlayerNum() > 1) || + ((Alive_() <= 0 || board.TypeCount(GridType::EXIT) == 0) && Global().PlayerNum() == 1); + if (!game_end && round_ < GAME_OPTION(回合数)) { setter.Emplace(*this, ++round_); return; } @@ -253,16 +448,34 @@ class MainStage : public MainGameStage if (game_end) { if (Global().PlayerNum() > 1) { - Global().Boardcast() << "玩家剩余 " + to_string(Alive_()) + " 人,游戏结束!"; + if (all_active_stop) { + Global().Boardcast() << "所有玩家均主动停止,游戏结束!"; + } else { + Global().Boardcast() << "玩家剩余 " + to_string(Alive_()) + " 人,游戏结束!"; + } } else if (board.players[0].out == 2) { Global().Boardcast() << "经过 " + to_string(round_) + " 回合,您成功抵达了逃生舱!"; } } else { Global().Boardcast() << "回合数到达上限,游戏结束"; } - Global().Boardcast() << "完整行动轨迹:\n" << Markdown(board.GetAllRecord(), 500); + Global().Boardcast() << "完整行动轨迹:\n" << Markdown(board.GetAllRecordHtml(-1, false), 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,38 +486,43 @@ 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; + main_stage.all_active_stop = true; + 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; + is_acting = false; + door_modified = 0; } // 当前行动玩家 PlayerID currentPlayer; - // 步数 - int step; - // 隐匿状态 - bool hide; - // 主动停止或超时 - bool active_stop; + // 玩家行动临时变量 + int step; // 行动步数 + bool hide; // 隐匿状态 + bool active_stop; // 主动停止或超时 + bool is_acting; // 玩家是否开始行动(时限/挂机判定) - // 记录门的状态发生过改变 - 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 +531,81 @@ 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!!!【重要提醒】!!! 请注意:当前版本主动停止或超时将无法得知四周墙壁信息!"; + // 开局私信墙壁信息 + for (PlayerID pid = 0; pid < Global().PlayerNum(); ++pid) { + auto [info, md] = Main().board.GetSurroundingWalls(pid); + Main().board.players[pid].private_record = "【开局】\n您所在位置的四周墙壁信息,按照 上下左右 顺序分别是:\n" + info; + Global().Tell(pid) << Main().board.players[pid].private_record << "\n" << Markdown(md, (GRID_SIZE + WALL_SIZE * 2) + 40); + } + // [BOSS-米诺陶斯] 开局声音 + if (GAME_OPTION(BOSS) == 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); + } + + void ActivatePlayerMovingTimer(const PlayerID pid) + { + // 解除挂机状态 + Main().board.players[pid].hook_status = false; + // 如果是当前玩家,视为开始行动 + if (pid == currentPlayer && !is_acting) { + is_acting = true; + Global().StartTimer(TimerLeft() + GAME_OPTION(行动时限)); + } + } + + bool CheckCommon(const PlayerID pid, MsgSenderBase& reply) { Player& player = Main().board.players[pid]; + ActivatePlayerMovingTimer(pid); 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() << "[错误] 请全程在公屏进行行动"; + if (GAME_OPTION(隐匿) == HideMode::NONE) { + reply() << "[错误] 多人游戏下,请全程在公屏进行行动"; return StageErrCode::FAILED; } if (!hide) { @@ -345,8 +613,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,199 +620,127 @@ 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 (step == 1) { - sender << "请继续行动(行动总时限为 " << GAME_OPTION(行动时限) << " 秒)"; - Global().StartTimer(GAME_OPTION(行动时限)); - } else { - int time = std::chrono::duration_cast(*Global().TimerFinishTime() - std::chrono::steady_clock::now()).count(); - sender << "请继续行动(剩余时间 " << time << " 秒)"; + if (GAME_OPTION(谋定后动)) { + step++; + active_stop = true; + return StageErrCode::READY; } + + // 继续行动 + sender << "\n\n请继续行动(剩余时间 " << TimerLeft() << " 秒)"; // 单步隐匿:消除隐匿状态 - if (GAME_OPTION(隐匿) == 2 && hide) { hide = false; } + if (GAME_OPTION(隐匿) == HideMode::STEP && 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(隐匿) == HideMode::STEP) { + 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; + + 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) { + if (GAME_OPTION(隐匿) == HideMode::NONE) { reply() << "[错误] 请全程在公屏进行行动"; } else { reply() << "[错误] 您未使用隐匿技能,请在公屏进行行动"; } return StageErrCode::FAILED; } - if (!hide) player.NewContentRecord("(停止)"); + + step++; active_stop = true; - reply() << "您选择主动停止行动,本回合结束!主动停止无法获得四周墙壁信息"; + if (!hide) player.NewContentRecord("(停止)"); + reply() << "[第 " << step << " 步] 您选择主动停止行动,本回合结束!" + << (GAME_OPTION(停止私信) ? "请留意机器人私信发送的四周墙壁信息" : "主动停止无法获得四周墙壁信息"); return StageErrCode::READY; } AtomReqErrCode Hide_(const PlayerID pid, const bool is_public, MsgSenderBase& reply) { - if (GAME_OPTION(隐匿) == 0) { + if (GAME_OPTION(隐匿) == HideMode::NONE) { 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; @@ -562,140 +756,114 @@ class RoundStage : public SubGameStage<> hide = true; player.hide_remaining--; - if (GAME_OPTION(隐匿) == 1) { - player.NewContentRecord("【隐匿行动】"); + if (GAME_OPTION(隐匿) == HideMode::TURN) { + player.NewContentRecord("<隐匿行动>", "hide"); reply() << "使用隐匿技能,本回合剩余时间转为私聊行动,不会发出声响,不会触发捕捉。剩余次数:" << player.hide_remaining; - } else if (GAME_OPTION(隐匿) == 2) { - player.NewContentRecord("�"); + } else if (GAME_OPTION(隐匿) == HideMode::STEP) { + 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) { + ActivatePlayerMovingTimer(pid); 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) { + int query_pid_current = currentPlayer == pid ? -1 : pid.Get(); 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(query_pid_current, is_public); } } - sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].GetMoveRecord(); + sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].move_record.GetMoveRecord(query_pid_current, is_public); } 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) { + ActivatePlayerMovingTimer(pid); auto sender = reply(); - if (GAME_OPTION(特殊事件) > 0) { + if (GAME_OPTION(特殊事件) != SpecialEvent::NONE) { sender << UnitMaps::ShowSpecialEvent(GAME_OPTION(特殊事件)) << "\n"; } if (GAME_OPTION(边长) > 9) { sender << "本局游戏地图为 " << GAME_OPTION(边长) << "x" << GAME_OPTION(边长) << "\n"; } - sender << Markdown(Main().board.GetAllRecord(), 500); - sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].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 = pid.Get(); + if (show_image) { + sender << Markdown(Main().board.GetAllRecordHtml(query_pid, is_public), 550); + } else { + sender << Main().board.GetAllRecordString(query_pid, is_public); } - 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 = currentPlayer == pid ? -1 : pid.Get(); + sender << "\n[" << currentPlayer.Get() << "号]正在行动中:\n" << Main().board.players[currentPlayer].move_record.GetMoveRecord(query_pid_current, is_public); + 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; - if (step == 0) { - Main().board.players[currentPlayer].hook_status = true; - Global().Boardcast() << "玩家 " << At(PlayerID(currentPlayer)) << " 超时未行动,已进入挂机状态,再次行动前仅有30秒等待时间"; + Player& player = Main().board.players[currentPlayer]; + if (!is_acting) { + 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(); } @@ -711,7 +879,7 @@ class RoundStage : public SubGameStage<> auto sender = Global().Boardcast(); sender << "玩家 " << At(pid) << " 退出游戏"; if (Main().Alive_() > 1) { - Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 + Main().board.UpdatePlayerTarget(GAME_OPTION(捕捉目标)); // 捕捉顺位变更 sender << ",捕捉目标顺位发生变更!\n"; sender << Markdown(Main().board.GetPlayerTable(Main().round_)); return StageErrCode::CONTINUE; @@ -727,114 +895,88 @@ 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, true); // 记录历史行动轨迹 - player.all_record += "
【第 " + to_string(Main().round_) + " 回合】
" + player.GetMoveRecord(); + player.all_record.push_back(player.move_record); + // 更新全员停止状态(全员停止/超时结束游戏) + if (!active_stop) Main().all_active_stop = false; // 仅剩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; active_stop = false; + is_acting = false; // 下一个玩家行动 do { currentPlayer = (currentPlayer + 1) % Global().PlayerNum(); if (currentPlayer == 0) break; } while (Main().board.players[currentPlayer].out > 0); if (currentPlayer != 0) { - uint32_t think_time = Main().board.players[currentPlayer].hook_status ? 30 : GAME_OPTION(思考时限); - Global().Boardcast() << "请 " << At(currentPlayer) << " 在公屏选择方向移动(第一次行动前有 " << think_time << " 秒思考时间,行动开始后总时限为 " << GAME_OPTION(行动时限) << " 秒)"; + 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) != BossType::NONE) { + 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)) { @@ -846,14 +988,373 @@ class RoundStage : public SubGameStage<> auto sender = Global().Boardcast(); sender << "笨笨的机器人退出了游戏"; if (Main().Alive_() > 1) { - Main().board.UpdatePlayerTarget(GAME_OPTION(捉捕目标)); // 捕捉顺位变更 + Main().board.UpdatePlayerTarget(GAME_OPTION(捕捉目标)); // 捕捉顺位变更 sender << ",捕捉目标顺位发生变更!\n"; sender << Markdown(Main().board.GetPlayerTable(Main().round_)); } 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.TypeCount(GridType::EXIT); + 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()) { + step++; // 成就【守株待兔】:强制停止计入额外步数 + player.NewContentRecord("(陷阱)"); + sender << prefix << GetRandomHint(trap_hints) << "\n\n移动触发【陷阱】,本回合被强制停止行动!"; + return true; + } + } + /* ========== Sound ========== */ + Sound sound = Main().board.GetSound(grid, GAME_OPTION(特殊事件)); + 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; + } + } + // [热源] + string step_info, heat_message; + if (Main().board.HeatNotice(player.pid)) { + step_info = "[热浪(第" + to_string(step) + "步)]"; + heat_message = GetRandomHint(heat_wave_hints) + "\n移动进入【热浪范围】,当前位置附近存在热源"; + player.NewExtraPriContent("热浪", "heat-wave"); + } + if (grid.Type() == GridType::HEAT) { + if (player.heated) { + step++; // 成就【守株待兔】:强制停止计入额外步数 + 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移动进入【热源】!请注意,在下一次进入热源时,将公开热源并强制停止行动"; + player.UpdateExtraPriContent("热源", "heat-core"); + } + } + if (heat_message != "") { + Global().Tell(player.pid) << step_info << heat_message; + } + + // 非点杀模式检测玩家捕捉(隐匿状态不能捕捉) + 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 (step == 1) player.achievement.catch_without_moving = true; // 成就【守株待兔】 + + 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 (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.TypeCount(GridType::EXIT) == 0) Main().withoutE_win_ = true; + // 有逃生舱但死斗取胜判定 + if (Main().board.TypeCount(GridType::EXIT) > 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 == 3) 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 ffbd8901..c8487fb0 100644 --- a/games/long_night/options.h +++ b/games/long_night/options.h @@ -1,15 +1,48 @@ -EXTEND_OPTION("每局游戏先根据模式从区块池抽取12+4组成随机池,再抽取地图区块
" - "【标准】仅从**经典区块**中抽取区块   【幻变】将从**轮换区块**中随机抽取
" - "【狂野】将从**所有区块**中随机抽取   【疯狂】随机池包含2个**特殊区块**", 模式, - (AlterChecker({{"标准", 0}, {"狂野", 1}, {"幻变", 2}, {"疯狂", 3}})), 2) -EXTEND_OPTION("捉捕目标:设置游戏中玩家的捉捕顺序", 捉捕目标, (BoolChecker("下家", "上家")), true) -EXTEND_OPTION("设置游戏地图边长", 边长, (AlterChecker({{"9", 9}, {"10", 10}, {"12", 12}})), 9) -EXTEND_OPTION("点杀模式:捕捉仅在回合结束时触发,路过不会触发捕捉", 点杀, (BoolChecker("开启", "关闭")), false) -EXTEND_OPTION("隐匿模式:隐匿后私聊行动,可选回合和单步模式", 隐匿, (AlterChecker({{"关闭", 0}, {"回合", 1}, {"单步", 2}})), 0) -EXTEND_OPTION("大乱斗模式:逃生舱改为随机传送", 大乱斗, (BoolChecker("开启", "关闭")), false) -EXTEND_OPTION("在游戏中生成BOSS牛头怪米诺陶斯,在回合结束时追击玩家", BOSS, (BoolChecker("开启", "关闭")), false) -EXTEND_OPTION("设置游戏特殊事件", 特殊事件, (AlterChecker({ - {"随机", -1}, {"无", 0}, {"怠惰的园丁", 1}, {"营养过剩", 2}, {"雨天小故事", 3} -})), 0) -EXTEND_OPTION("行动前思考的时间限制", 思考时限, (ArithChecker(30, 3600, "超时时间(秒)")), 180) -EXTEND_OPTION("开始行动后的总时间限制", 行动时限, (ArithChecker(60, 3600, "超时时间(秒)")), 360) +#include "constants.h" + +EXTEND_OPTION("[区块] 先根据模式从区块池抽取 12+4 组成随机池,再抽取地图区块
" + "【经典】仅从**经典区块**中抽取区块   【幻变】将从**轮换区块**中随机抽取
" + "【狂野】将从**所有区块**中随机抽取   【疯狂】随机池包含2个**特殊区块**
" + "【主题模式】从**主题区块**中抽取12+4组成随机池,区块池具有不同的风格", 模式, +(AlterChecker(std::map{ + {"经典", BlockMode::CLASSIC}, + {"幻变", BlockMode::TWIST}, + {"狂野", BlockMode::WILD}, + {"疯狂", BlockMode::CRAZY}, + {"按钮", BlockMode::BUTTON}, + {"陷阱", BlockMode::TRAP}, +})), BlockMode::TWIST) +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("[常规] 捕捉目标:设置游戏中玩家的捕捉顺序", 捕捉目标, + (AlterChecker(std::map{{"上家", Target::PREVIOUS}, {"下家", Target::NEXT}})), Target::PREVIOUS) +EXTEND_OPTION("[常规] 停止私信:玩家主动停止或超时,得知私信墙壁信息", 停止私信, (BoolChecker("开启", "关闭")), false) + +EXTEND_OPTION("[事件] 设置游戏特殊事件", 特殊事件, (AlterChecker(std::map{ + {"无", SpecialEvent::NONE}, + {"随机", SpecialEvent::RANDOM}, + {"怠惰的园丁", SpecialEvent::LAZYGARDENER}, + {"营养过剩", SpecialEvent::OVERGROWTH}, + {"雨天小故事", SpecialEvent::RAINSTORY}, +})), SpecialEvent::NONE) + +EXTEND_OPTION("[模式] BOSS:具体规则详见「#规则 漫漫长夜 BOSS」", BOSS, (AlterChecker(std::map{ + {"无", BossType::NONE}, + {"米诺陶斯", BossType::MINOTAUR}, + {"邦邦", BossType::BANGBANG}, +})), BossType::NONE) +EXTEND_OPTION("[模式] 点杀:捕捉改为仅在回合结束时触发,路过不会触发捕捉", 点杀, (BoolChecker("开启", "关闭")), true) +EXTEND_OPTION("[模式] 隐匿:隐匿后私聊行动,可选回合和单步模式", 隐匿, + (AlterChecker(std::map{{"关闭", HideMode::NONE}, {"回合", HideMode::TURN}, {"单步", HideMode::STEP}})), HideMode::NONE) +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(std::map{{"经典", Texture::CLASSIC}, {"复古", Texture::RETRO}})), Texture::CLASSIC) diff --git a/games/long_night/player.h b/games/long_night/player.h new file mode 100644 index 00000000..2200a0e4 --- /dev/null +++ b/games/long_night/player.h @@ -0,0 +1,534 @@ + +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; // 声音向其他所有玩家的传播方向(私信完整赛况) + // string extra_pub_content; // 移动时的额外公屏信息(暂不使用) + pair extra_pri_content; // 移动时的额外私信信息(私信完整赛况) + 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) {} + Move(pair extra_pri_content, string style) + : direct(-1), sound(Sound::NONE), extra_pri_content(extra_pri_content), style(style) {} + + void UpdateExtraPriContent(const string& content, const bool has_dir, const string& style) + { + this->extra_pri_content.first = content; + this->extra_pri_content.second = has_dir; + this->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_public, 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.extra_pri_content.first.empty() && !next.extra_pri_content.second && (query_pid != -1 || is_public)) { + j++; continue; + } + // 计数判断:方向相同、无声响、非[无方向]content信息、无私信隐藏信息或私信隐藏信息[含有方向] + if (next.direct == mv.direct && next.sound == Sound::NONE && next.content.first.empty() && + (next.extra_pri_content.first.empty() || next.extra_pri_content.second)) { + 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_public, is_html); + i++; + } + return result; + } + + private: + static string formatSingle(const Move& mv, const int query_pid, const bool is_public, const bool is_html) + { + string d = dirSymbol(mv.direct); + + // 查询pid非-1(非自己),获取私信完整赛况声响方向 + string sound_d; + if (mv.sound != Sound::NONE && query_pid != -1 && !is_public) { + 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 { // 普通移动 + if (!mv.extra_pri_content.first.empty()) { + if (query_pid == -1 && !is_public) { + // 私信额外内容(仅查询自己-1) + if (is_html) { + return mv.extra_pri_content.second + ? "" + d + mv.extra_pri_content.first + "" // 带方向信息 + : "" + mv.extra_pri_content.first + ""; // 不带方向信息 + } + return mv.content.second ? "[" + d + mv.extra_pri_content.first + "]" : "[" + mv.extra_pri_content.first + "]"; + } else { + string direct_str = is_html ? "" + d + "" : d; + return mv.content.second ? direct_str : ""; + } + } 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}; } + + void NewExtraPriContent(const string& content, const string& style) { if (!move_record.empty()) move_record.push_back({{content, false}, style}); } + void UpdateExtraPriContent(const string& content, const string& style) { if (!move_record.empty()) move_record.back().UpdateExtraPriContent(content, true, style); } + + string GetAllMoveRecord(const int query_pid, const int is_public, 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 == pid ? -1 : query_pid, is_public, is_html); // -1代表是自己查询 + } + 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 a6b77feb..00000000 Binary files a/games/long_night/resource/box.png and /dev/null differ diff --git a/games/long_night/resource/button.png b/games/long_night/resource/button.png deleted file mode 100644 index ae01153b..00000000 Binary files a/games/long_night/resource/button.png and /dev/null differ diff --git a/games/long_night/resource/classic/bomb.png b/games/long_night/resource/classic/bomb.png new file mode 100644 index 00000000..d9c2e35b Binary files /dev/null and b/games/long_night/resource/classic/bomb.png differ diff --git a/games/long_night/resource/classic/box.png b/games/long_night/resource/classic/box.png new file mode 100644 index 00000000..7becba71 Binary files /dev/null and b/games/long_night/resource/classic/box.png differ diff --git a/games/long_night/resource/classic/button.png b/games/long_night/resource/classic/button.png new file mode 100644 index 00000000..1a1b55c4 Binary files /dev/null and b/games/long_night/resource/classic/button.png differ 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 00000000..ad6b2cb0 Binary files /dev/null and b/games/long_night/resource/classic/transparent.png differ 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 00000000..c75a3ef5 Binary files /dev/null and b/games/long_night/resource/classic/walls/corner.png differ 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 00000000..aa0df42c Binary files /dev/null and b/games/long_night/resource/classic/walls/door_col.png differ diff --git a/games/long_night/resource/classic/walls/door_row.png b/games/long_night/resource/classic/walls/door_row.png new file mode 100644 index 00000000..a01ce789 Binary files /dev/null and b/games/long_night/resource/classic/walls/door_row.png differ diff --git a/games/long_night/resource/classic/walls/dooropen_col.png b/games/long_night/resource/classic/walls/dooropen_col.png new file mode 100644 index 00000000..03b88160 Binary files /dev/null and b/games/long_night/resource/classic/walls/dooropen_col.png differ 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 00000000..eb7de4b0 Binary files /dev/null and b/games/long_night/resource/classic/walls/dooropen_row.png differ diff --git a/games/long_night/resource/classic/walls/empty_col.png b/games/long_night/resource/classic/walls/empty_col.png new file mode 100644 index 00000000..3e8dafb3 Binary files /dev/null and b/games/long_night/resource/classic/walls/empty_col.png differ diff --git a/games/long_night/resource/classic/walls/empty_row.png b/games/long_night/resource/classic/walls/empty_row.png new file mode 100644 index 00000000..491c87f0 Binary files /dev/null and b/games/long_night/resource/classic/walls/empty_row.png differ diff --git a/games/long_night/resource/classic/walls/unknown_col.png b/games/long_night/resource/classic/walls/unknown_col.png new file mode 100644 index 00000000..7ec17520 Binary files /dev/null and b/games/long_night/resource/classic/walls/unknown_col.png differ diff --git a/games/long_night/resource/classic/walls/unknown_row.png b/games/long_night/resource/classic/walls/unknown_row.png new file mode 100644 index 00000000..88e9ed5e Binary files /dev/null and b/games/long_night/resource/classic/walls/unknown_row.png differ diff --git a/games/long_night/resource/classic/walls/wall_col.png b/games/long_night/resource/classic/walls/wall_col.png new file mode 100644 index 00000000..34b78ffc Binary files /dev/null and b/games/long_night/resource/classic/walls/wall_col.png differ 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 00000000..3ed98c0d Binary files /dev/null and b/games/long_night/resource/classic/walls/wall_row.png differ 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 00000000..c7168586 Binary files /dev/null and b/games/long_night/resource/retro/bomb.png differ diff --git a/games/long_night/resource/retro/box.png b/games/long_night/resource/retro/box.png new file mode 100644 index 00000000..670e2ce7 Binary files /dev/null and b/games/long_night/resource/retro/box.png differ diff --git a/games/long_night/resource/retro/button.png b/games/long_night/resource/retro/button.png new file mode 100644 index 00000000..b2bf84b1 Binary files /dev/null and b/games/long_night/resource/retro/button.png differ diff --git a/games/long_night/resource/retro/empty.png b/games/long_night/resource/retro/empty.png new file mode 100644 index 00000000..ac6eafaf Binary files /dev/null and b/games/long_night/resource/retro/empty.png differ diff --git a/games/long_night/resource/retro/exit.png b/games/long_night/resource/retro/exit.png new file mode 100644 index 00000000..356516c6 Binary files /dev/null and b/games/long_night/resource/retro/exit.png differ diff --git a/games/long_night/resource/retro/grass.png b/games/long_night/resource/retro/grass.png new file mode 100644 index 00000000..72e5e75a Binary files /dev/null and b/games/long_night/resource/retro/grass.png differ diff --git a/games/long_night/resource/retro/heat.png b/games/long_night/resource/retro/heat.png new file mode 100644 index 00000000..1639124f Binary files /dev/null and b/games/long_night/resource/retro/heat.png differ 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 00000000..233f8236 Binary files /dev/null and b/games/long_night/resource/retro/oneway_portal.png differ diff --git a/games/long_night/resource/retro/portal.png b/games/long_night/resource/retro/portal.png new file mode 100644 index 00000000..ad010030 Binary files /dev/null and b/games/long_night/resource/retro/portal.png differ diff --git a/games/long_night/resource/retro/transparent.png b/games/long_night/resource/retro/transparent.png new file mode 100644 index 00000000..ad6b2cb0 Binary files /dev/null and b/games/long_night/resource/retro/transparent.png differ diff --git a/games/long_night/resource/retro/trap.png b/games/long_night/resource/retro/trap.png new file mode 100644 index 00000000..76d53fda Binary files /dev/null and b/games/long_night/resource/retro/trap.png differ diff --git a/games/long_night/resource/retro/unknown.png b/games/long_night/resource/retro/unknown.png new file mode 100644 index 00000000..025fe623 Binary files /dev/null and b/games/long_night/resource/retro/unknown.png differ 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 00000000..e8806005 Binary files /dev/null and b/games/long_night/resource/retro/walls/corner.png differ diff --git a/games/long_night/resource/retro/walls/door_col.png b/games/long_night/resource/retro/walls/door_col.png new file mode 100644 index 00000000..9bed05e6 Binary files /dev/null and b/games/long_night/resource/retro/walls/door_col.png differ diff --git a/games/long_night/resource/retro/walls/door_row.png b/games/long_night/resource/retro/walls/door_row.png new file mode 100644 index 00000000..3fb768dc Binary files /dev/null and b/games/long_night/resource/retro/walls/door_row.png differ diff --git a/games/long_night/resource/retro/walls/dooropen_col.png b/games/long_night/resource/retro/walls/dooropen_col.png new file mode 100644 index 00000000..218143d4 Binary files /dev/null and b/games/long_night/resource/retro/walls/dooropen_col.png differ diff --git a/games/long_night/resource/retro/walls/dooropen_row.png b/games/long_night/resource/retro/walls/dooropen_row.png new file mode 100644 index 00000000..bd8e8a25 Binary files /dev/null and b/games/long_night/resource/retro/walls/dooropen_row.png differ 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 00000000..751e9fba Binary files /dev/null and b/games/long_night/resource/retro/walls/empty_col.png differ 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 00000000..1c47bea9 Binary files /dev/null and b/games/long_night/resource/retro/walls/empty_row.png differ diff --git a/games/long_night/resource/retro/walls/unknown_col.png b/games/long_night/resource/retro/walls/unknown_col.png new file mode 100644 index 00000000..7ec17520 Binary files /dev/null and b/games/long_night/resource/retro/walls/unknown_col.png differ diff --git a/games/long_night/resource/retro/walls/unknown_row.png b/games/long_night/resource/retro/walls/unknown_row.png new file mode 100644 index 00000000..88e9ed5e Binary files /dev/null and b/games/long_night/resource/retro/walls/unknown_row.png differ diff --git a/games/long_night/resource/retro/walls/wall_col.png b/games/long_night/resource/retro/walls/wall_col.png new file mode 100644 index 00000000..73c62ee7 Binary files /dev/null and b/games/long_night/resource/retro/walls/wall_col.png differ diff --git a/games/long_night/resource/retro/walls/wall_row.png b/games/long_night/resource/retro/walls/wall_row.png new file mode 100644 index 00000000..42254cd3 Binary files /dev/null and b/games/long_night/resource/retro/walls/wall_row.png differ diff --git a/games/long_night/resource/retro/water.png b/games/long_night/resource/retro/water.png new file mode 100644 index 00000000..1a69b893 Binary files /dev/null and b/games/long_night/resource/retro/water.png differ diff --git a/games/long_night/rule.md b/games/long_night/rule.md index dac521ec..eb5ff941 100644 --- a/games/long_night/rule.md +++ b/games/long_night/rule.md @@ -1,8 +1,7 @@ ## 漫漫长夜 -- **游戏人数:** 2-6 +- **游戏人数:** 1-8 - **原作:** 大萝卜姬 -- **详细图片规则可查看群文件:《漫漫长夜》.pdf** ### 游戏简介 - 本游戏适合 4/6 名玩家游玩。你和其他玩家被传送到一座伸手不见五指的迷宫里,你将同时扮演逃生者和狩猎者的身份,在漆黑的迷宫里通过有限的信息到达逃生舱或者捕捉其他玩家,以此获得胜利! @@ -10,14 +9,15 @@ ### 一、地图与行动 - **1.地图:**
 ①全局地图**不可视**,所有行动**公屏**进行。
- ②开局地图由16个区块中随机不重复的9个按照随机顺序排列组成3x3的大地图。*而且,逃生舱区块数量恒定=玩家总人数的1半,大地图的边缘会连通至地图的另一端。*
+ ②开局地图由16个区块中随机不重复的9个按照随机顺序排列组成3x3的大地图。*而且,逃生舱区块数量=玩家总人数的一半,大地图的边缘会连通至地图的另一端。*
 ③开局所有玩家将随机进入地图某一位置,且所有玩家均不知所有玩家的方位。而且,每个区块仅出生1位玩家;玩家出生点不会在逃生舱区块;捕猎顺序的上下家和所有玩家与逃生舱的距离会尽可能大于等于5格。 - **2.行动与目标:**
 ①开局随机玩家行动顺序,每位玩家按顺序轮流行动。
 ②所有玩家的游戏目标是:**按照行动顺序,捕捉自己下一位玩家,即捕猎顺序和行动顺序一致。**
 ③玩家的每次行动均可移动无限步数 **(移动1个正方形格子为1步)**。若移动中碰到墙壁则立即停止行动,玩家将**秘密得知自己所在位置的四周墙壁信息**,然后轮到下一位玩家行动。可任意时刻自行停止移动,**但主动停止无法获得私信墙壁信息**。
- ④玩家在移动过程中经过下家玩家则视为捕捉成功。玩家在移动过程中经过上家玩家不会被捕捉。
- ⑤若有玩家捕捉成功或从出口逃离,游戏目标顺位更改。**捕捉成功会使行动停止,并且随机传送到地图任一位置。** 剩余玩家的捕猎目标按原顺序顺位更改。游戏将进行到仅剩 1 个玩家。 + ④玩家在行动结束时和目标玩家重合则视为捕捉成功。玩家和抓自己的玩家重合不会被捕捉。**游戏默认开启点杀模式,路过目标玩家并不会触发捕捉**
+ ⑤若有玩家捕捉成功或从出口逃离,游戏目标顺位更改。**捕捉成功会使行动停止,并且随机传送到地图任一位置。** 剩余玩家的捕猎目标按原顺序顺位更改。
+ ⑥**游戏将进行到仅剩 1 个玩家。** 但如果剩余存活玩家在同一回合均主动停止,游戏会提前结束。 ### 二、其他设定 - **1.声响:**
@@ -25,9 +25,9 @@  ②关于除了 正上/正下/正左/正右 外的夹角方向,例如右上,指正上和正右的夹角范围。
 ③因为地图边界联通,裁判私聊声源方向时会优先选择就近的方向。10x10地图时,若声源距离相等,裁判将选择任意1个告知。 - **2.传送门与亚空间:**
- 踩到紫色传送门后,玩家会发出啪啪声,然后视为进入亚空间。出生在传送门时不会进入亚空间,不会发出啪啪声。亚空间内没有墙壁,也没有方向的概念。亚空间内玩家任意移动2步即可到达相同区块的另一个传送门。例如:“→啪(踩到传送门,发出啪啪声,进入亚空间)←→啪(任意2次移动后,离开亚空间,踩到传送门,发出啪啪声)” + 踩到紫色传送门后,玩家会发出啪啪声,然后视为进入亚空间。出生在传送门时不会进入亚空间,不会发出啪啪声。亚空间内没有墙壁,也没有方向的概念。亚空间内玩家任意移动2步即可到达相同区块的另一个传送门。例如:“→啪(踩到传送门,发出啪啪声,进入亚空间)←→啪(任意2次移动后,离开亚空间,踩到出口传送门,发出啪啪声)” - **3.亚空间停留**
- 玩家刚进入亚空间或在亚空间内移动一步,均视作在亚空间内。停留在亚空间内,获得的四周墙壁信息为“空空空空”,**玩家本身依然在传送门入口处,捉捕需要在传送门入口处才能触发** + 玩家刚进入亚空间或在亚空间内移动一步,均视作在亚空间内。停留在亚空间内,获得的四周墙壁信息为“空空空空”,**玩家本身依然在传送门入口处,捕捉需要在传送门入口处才能触发** ### 三、特殊事件 - ①怠惰的园丁:树丛将在其区块内随机位置生成(有可能生成在中间) @@ -36,15 +36,19 @@ ### 四、特殊模式 - ①区块模式:**如果传送门被四周封闭,同区块对应的传送门将会变为水洼** - + 【标准】仅使用游戏最初的**经典区块**组成随机池 + + 【经典】仅使用游戏最初的**经典区块**组成随机池 + 【幻变】将从**轮换区块**中抽取12+4组成随机池,根据更新发生变化 + 【狂野】将从**所有区块**中抽取12+4组成随机池 + 【疯狂】从**所有区块**中抽取10+4,同时包含2个**特殊区块** -- ②点杀:捕捉改为仅在回合结束时触发,路过不会触发捕捉 -- ③反侦察手段:玩家获得技能“隐匿”,可选【回合】和【单步】模式 - 1. 回合隐匿:持续1回合,**仅可使用1次**,当回合的所有行动转为私聊进行,不会发出声响,不会触发捕捉。 + + 【主题】从**主题区块**中抽取12+4,每种区块池都具有不同的风格 + + 【自定义】从**自选区块**中随机抽取,逃生舱数量自动适配 +- ②BOSS:在游戏中添加BOSS。**具体规则详见「#规则 漫漫长夜 BOSS」** +- ③点杀:捕捉改为仅在行动结束时触发,路过不会触发捕捉 +- ④反侦察手段:玩家获得技能“隐匿”,可选【回合】和【单步】模式 + 1. 回合隐匿:持续一回合,**仅可使用1次**,本回合的所有行动转为私聊进行,不会发出声响,不会触发捕捉。 2. 单步隐匿:仅作用于下一步,**可使用4次**,隐匿后在私聊行动下一步,不会发出声响,不会触发捕捉。 -- ④10x10!:本局游戏的大地图将更改为10x10大地图,9个区块随机排列塞满,区块不会重叠,没有区块的空隙将变成普通道路。 -- ⑤大乱斗:所有的逃生舱改为随机传送!但仍会统计逃生分 -- ⑥BOSS:米诺陶斯随机生成在地图中,每回合最后行动,首回合锁定最近玩家为目标(公屏显示),每回合移动步数递增,发现更近玩家则更换目标并重置步数。无视地形,移动结束时会发出巨响,如果玩家在其周围会听到喘息声。BOSS踩到玩家则玩家出局,玩家经过米诺陶斯不会出局。 - +- ⑤10x10!:本局游戏的大地图将更改为10x10大地图,9个区块随机排列塞满,区块不会重叠,没有区块的空隙将变成普通道路。 +- ⑥大乱斗:所有的逃生舱改为随机传送!但仍会统计逃生分 +- ⑦谋定后动:每回合仅能执行一次移动,可使用多步行动指令 +- ⑧炸弹人:玩家可在公屏安置炸弹,任何人经过炸弹并离开会引爆炸弹,使玩家立即出局并-100分。**在炸弹上结束行动可拆除炸弹** + diff --git a/games/long_night/unittest.cc b/games/long_night/unittest.cc index 9d65fa15..c55cae51 100644 --- a/games/long_night/unittest.cc +++ b/games/long_night/unittest.cc @@ -28,6 +28,39 @@ GAME_TEST(2, leave_test2) ASSERT_SCORE(0, -300); } +GAME_TEST(3, all_active_stop1) +{ + START_GAME(); + + ASSERT_PUB_MSG(CONTINUE, 0, "停止"); + ASSERT_PUB_MSG(CONTINUE, 1, "停止"); + ASSERT_PUB_MSG(CHECKOUT, 2, "停止"); + + ASSERT_SCORE(0, 0, 0); +} + +GAME_TEST(3, all_active_stop2) +{ + START_GAME(); + + ASSERT_TIMEOUT(CONTINUE); + ASSERT_TIMEOUT(CONTINUE); + ASSERT_PUB_MSG(CHECKOUT, 2, "停止"); + + ASSERT_SCORE(0, 0, 0); +} + +GAME_TEST(3, all_active_stop3) +{ + START_GAME(); + + ASSERT_TIMEOUT(CONTINUE); + ASSERT_TIMEOUT(CONTINUE); + ASSERT_TIMEOUT(CHECKOUT); + + ASSERT_SCORE(0, 0, 0); +} + } // namespace GAME_MODULE_NAME } // namespace game diff --git a/games/naval_battle/board.h b/games/naval_battle/board.h index d21860a8..a00658c7 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 b643f3c3..f5d0c78d 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 de7edd48..9066f4b0 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 a3681eaa..d4d37ef6 100644 --- a/games/naval_battle/rule.md +++ b/games/naval_battle/rule.md @@ -18,22 +18,24 @@ ### 特殊规则 - 允许飞机重叠:若规则允许飞机重叠时,飞机的机身可任意数量重叠,机头不可与机身重叠 - 仅首“要害”公开:在该规则下,每个玩家命中过1次机头以后,之后再次命中其他机头时,仅告知命中,不提示命中要害,且不具有额外一回合 -- 无“要害”提示:在该规则下,命中机头时,仅告知命中,不再提示命中要害,且不具有额外一回合。 +- 无“要害”提示:在该规则下,命中机头时,仅告知命中,不再提示命中要害,且不具有额外一回合 +- ?要害模式:无要害提示但每人有一次“侦察”机会,可以在回合结束后侦察所有已打击位置是否为要害,若其中有要害,则获得一个额外回合 ### 地图图例 - - - + + + - - - + + + - - + +
 未被打击区域+未被打击机身未被打击飞机头 未被打击区域+未被打击机身未被打击飞机头
 已被打击空地+已被打击机身已被打击飞机头 已被打击空地+已被打击机身已被打击飞机头
-侦察点:空地+侦察点:机身-侦察点:空地+侦察点:机身
+ diff --git a/games/numcomb/mygame.cc b/games/numcomb/mygame.cc index 03b62f99..aed427ee 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 556c3752..caefdbe6 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 f472454b..fcb0df05 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) diff --git a/games/six_nimmt/mygame.cc b/games/six_nimmt/mygame.cc index 28087575..b06cea47 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 eb6b2af1..50988a04 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 46c7a073..8c83c1ad 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 26d37e5e..56fd18f8 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 {