} */ public function getTypeConfig(?string $quizType = null): array { $normalizedQuizType = $this->normalizeQuizType($quizType); $config = GameConfig::forGame(Riddle::TYPE_IDIOM) ?? GameConfig::forGame($normalizedQuizType); $params = $config?->params ?? []; $typeConfig = (array) (($params['type_configs'] ?? [])[$normalizedQuizType] ?? []); $sharedRoomIds = $this->normalizeRoomIds( $params['room_ids'] ?? (($params['room_id'] ?? null) !== null ? [$params['room_id']] : []) ); $roomMode = (string) ($params['room_scope_mode'] ?? ($typeConfig['room_mode'] ?? 'single')); if (! in_array($roomMode, ['all', 'single', 'multiple'], true)) { $roomMode = 'single'; } $roomIds = $sharedRoomIds !== [] ? $sharedRoomIds : $this->normalizeRoomIds($typeConfig['room_ids'] ?? [1]); return [ 'reward_gold' => max(0, (int) ($params['reward_gold'] ?? ($typeConfig['reward_gold'] ?? 50))), 'reward_exp' => max(0, (int) ($params['reward_exp'] ?? ($typeConfig['reward_exp'] ?? 30))), 'expire_minutes' => max(0, (int) ($params['expire_minutes'] ?? ($typeConfig['expire_minutes'] ?? 5))), 'auto_start_interval' => max(0, (int) ($params['auto_start_interval'] ?? ($typeConfig['auto_start_interval'] ?? 0))), 'room_mode' => $roomMode, 'room_ids' => $roomIds, ]; } /** * 方法功能:读取题目有效时长配置,单位分钟。 */ public function getExpireMinutes(?string $quizType = null): int { return $this->getTypeConfig($quizType)['expire_minutes']; } /** * 方法功能:读取自动出题间隔配置,单位分钟。 */ public function getAutoStartInterval(?string $quizType = null): int { return $this->getTypeConfig($quizType)['auto_start_interval']; } /** * 方法功能:读取答题奖励配置。 * * @return array{reward_gold:int,reward_exp:int} */ public function getRewardConfig(?string $quizType = null): array { $typeConfig = $this->getTypeConfig($quizType); return [ 'reward_gold' => $typeConfig['reward_gold'], 'reward_exp' => $typeConfig['reward_exp'], ]; } /** * 方法功能:将外部传入的题型归一化为系统支持值。 */ public function normalizeQuizType(?string $quizType): string { $normalizedType = trim((string) $quizType); return Riddle::isSupportedType($normalizedType) ? $normalizedType : Riddle::TYPE_IDIOM; } /** * 方法功能:返回题型对应的中文名称。 */ public function getQuizTypeLabel(string $quizType): string { return Riddle::labelForType($this->normalizeQuizType($quizType)); } /** * 方法功能:读取自动出题的房间范围模式。 */ public function getRoomScopeMode(?string $quizType = null): string { return $this->getTypeConfig($quizType)['room_mode']; } /** * 方法功能:读取自动出题允许覆盖的房间列表。 * * @return array */ public function getScopedRoomIds(?string $quizType = null): array { $typeConfig = $this->getTypeConfig($quizType); $mode = $typeConfig['room_mode']; $configuredRoomIds = $typeConfig['room_ids']; if ($mode === 'all') { return Room::query()->orderBy('id')->pluck('id')->map(fn (mixed $id): int => (int) $id)->all(); } if ($mode === 'single') { return array_slice($configuredRoomIds !== [] ? $configuredRoomIds : [1], 0, 1); } return $configuredRoomIds !== [] ? $configuredRoomIds : [1]; } /** * 方法功能:判断指定回合是否已经超过有效时长。 */ public function isRoundExpired(RiddleGameRound $round): bool { $expireMinutes = $this->getExpireMinutes($round->quiz_type); if ($expireMinutes <= 0) { return false; } if (! in_array($round->status, ['pending', 'active'], true)) { return false; } if (! $round->started_at) { return false; } return $round->started_at->copy()->addMinutes($expireMinutes)->lte(now()); } /** * 方法功能:结算并结束已过期的回合,必要时发送超时公告。 */ public function expireRound(RiddleGameRound $round, bool $announce = true): bool { if (! $this->isRoundExpired($round)) { return false; } $round->loadMissing('idiom'); // 已过期回合统一落为 ended,防止继续答题或阻塞新开题。 $round->update([ 'status' => 'ended', 'ended_at' => $round->ended_at ?? now(), ]); if ($announce) { $this->pushExpiredRoundMessage($round); } return true; } /** * 方法功能:批量清理指定房间内已超时但仍处于进行中的回合。 */ public function expireActiveRoundsForRoom(int $roomId, bool $announce = true, ?string $quizType = null): int { $expiredCount = 0; $normalizedQuizType = $quizType !== null ? $this->normalizeQuizType($quizType) : null; RiddleGameRound::with('idiom') ->where('room_id', $roomId) ->when( $normalizedQuizType !== null, fn ($query) => $query->where('quiz_type', $normalizedQuizType), ) ->whereIn('status', ['pending', 'active']) ->orderBy('id') ->get() ->each(function (RiddleGameRound $round) use ($announce, &$expiredCount): void { if ($this->expireRound($round, $announce)) { $expiredCount++; } }); return $expiredCount; } /** * 方法功能:手动结束指定房间指定题型的所有进行中回合。 */ public function endActiveRoundsForRoom(int $roomId, ?string $quizType = null): int { $endedCount = 0; $normalizedQuizType = $quizType !== null ? $this->normalizeQuizType($quizType) : null; RiddleGameRound::query() ->where('room_id', $roomId) ->when( $normalizedQuizType !== null, fn ($query) => $query->where('quiz_type', $normalizedQuizType), ) ->whereIn('status', ['pending', 'active']) ->orderBy('id') ->get() ->each(function (RiddleGameRound $round) use (&$endedCount): void { // 手动出题覆盖旧题时,直接结束旧回合,不再额外发超时公告。 $round->update([ 'status' => 'ended', 'ended_at' => $round->ended_at ?? now(), ]); $endedCount++; }); return $endedCount; } /** * 方法功能:为指定房间和题型创建一轮新题。 */ public function startRound(int $roomId, ?string $quizType = null): ?RiddleGameRound { $normalizedQuizType = $this->normalizeQuizType($quizType); if (! $this->isGameEnabled($normalizedQuizType)) { return null; } // 先清理同房间同题型的过期回合,避免旧记录卡住新题。 $this->expireActiveRoundsForRoom($roomId, true, $normalizedQuizType); if ($this->findActiveRound($roomId, $normalizedQuizType)) { return null; } $idiom = $this->pickRandomQuestion($normalizedQuizType); if (! $idiom) { return null; } $rewardConfig = $this->getRewardConfig($normalizedQuizType); // 新回合显式记录 quiz_type,保证房间与题型维度都能独立判定。 $round = RiddleGameRound::create([ 'room_id' => $roomId, 'idiom_id' => $idiom->id, 'quiz_type' => $normalizedQuizType, 'status' => 'active', 'reward_gold' => $rewardConfig['reward_gold'], 'reward_exp' => $rewardConfig['reward_exp'], 'started_at' => now(), ]); $round->setRelation('idiom', $idiom); $this->broadcastStartedRound($round); return $round; } /** * 方法功能:按配置范围自动为各房间各题型尝试开题。 */ public function autoStartEligibleRounds(): int { $startedCount = 0; foreach (Riddle::supportedTypes() as $quizType) { $interval = $this->getAutoStartInterval($quizType); if ($interval <= 0) { continue; } foreach ($this->getScopedRoomIds($quizType) as $roomId) { // 房间与题型维度独立结算过期回合,互不干扰。 $this->expireActiveRoundsForRoom($roomId, true, $quizType); if ($this->findActiveRound($roomId, $quizType)) { continue; } if (! $this->hasReachedAutoStartInterval($roomId, $quizType, $interval)) { continue; } if (! $this->pickRandomQuestion($quizType)) { continue; } if ($this->startRound($roomId, $quizType)) { $startedCount++; } } } return $startedCount; } /** * 方法功能:查询指定房间指定题型的进行中回合。 */ public function findActiveRound(int $roomId, ?string $quizType = null): ?RiddleGameRound { return RiddleGameRound::query() ->with('idiom') ->where('room_id', $roomId) ->where('quiz_type', $this->normalizeQuizType($quizType)) ->whereIn('status', ['pending', 'active']) ->first(); } /** * 方法功能:随机抽取一条启用中的题目。 */ public function pickRandomQuestion(?string $quizType = null): ?Riddle { return Riddle::query() ->where('type', $this->normalizeQuizType($quizType)) ->where('is_active', true) ->inRandomOrder() ->first(); } /** * 方法功能:生成答题奖励日志文案。 */ public function buildRewardDescription(RiddleGameRound $round): string { $quizTypeLabel = $this->getQuizTypeLabel($round->quiz_type); return "猜谜活动{$quizTypeLabel}答对「{$round->idiom?->answer}」奖励"; } /** * 方法功能:向公屏推送回合超时公告。 */ public function pushExpiredRoundMessage(RiddleGameRound $round): void { $answer = $round->idiom?->answer ?? '未知答案'; $quizTitle = Riddle::activityLabelForType($round->quiz_type); $message = [ 'id' => $this->chatState->nextMessageId($round->room_id), 'room_id' => $round->room_id, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => "⏳ 【{$quizTitle}】第 #{$round->id} 题已超时结束!正确答案:{$answer}", 'is_secret' => false, 'font_color' => '#d97706', 'action' => '', 'quiz_type' => $this->normalizeQuizType($round->quiz_type), 'quiz_type_label' => $this->getQuizTypeLabel($round->quiz_type), 'quiz_round_id' => $round->id, 'quiz_round_ended_id' => $round->id, 'quiz_answer' => $answer, 'idiom_game_round_ended_id' => $round->id, 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($round->room_id, $message); broadcast(new MessageSent($round->room_id, $message)); } /** * 方法功能:广播新回合开始事件并同步写入公屏消息。 */ public function broadcastStartedRound(RiddleGameRound $round): void { $round->loadMissing('idiom'); broadcast(new RiddleGameStarted( roomId: $round->room_id, quizType: $round->quiz_type, hint: $round->idiom?->hint ?? '', roundId: $round->id, rewardGold: $round->reward_gold, rewardExp: $round->reward_exp, )); $message = [ 'id' => $this->chatState->nextMessageId($round->room_id), 'room_id' => $round->room_id, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $this->buildStartMessage($round->quiz_type, $round->id, $round->idiom?->hint ?? ''), 'is_secret' => false, 'font_color' => '#b91c1c', 'action' => '', 'quiz_type' => $this->normalizeQuizType($round->quiz_type), 'quiz_type_label' => $this->getQuizTypeLabel($round->quiz_type), 'quiz_round_id' => $round->id, 'quiz_hint' => $round->idiom?->hint ?? '', 'quiz_reward_gold' => $round->reward_gold, 'quiz_reward_exp' => $round->reward_exp, 'idiom_game_round_id' => $round->id, 'idiom_reward_gold' => $round->reward_gold, 'idiom_reward_exp' => $round->reward_exp, 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($round->room_id, $message); broadcast(new MessageSent($round->room_id, $message)); } /** * 方法功能:判断指定房间指定题型是否已到自动开题间隔。 */ private function hasReachedAutoStartInterval(int $roomId, string $quizType, int $interval): bool { $lastRound = RiddleGameRound::query() ->where('room_id', $roomId) ->where('quiz_type', $this->normalizeQuizType($quizType)) ->latest() ->first(); if (! $lastRound) { return true; } $lastTime = $lastRound->ended_at ?? $lastRound->started_at ?? $lastRound->created_at; return ! $lastTime || $lastTime->diffInMinutes(now()) >= $interval; } /** * 方法功能:把 room_ids 配置归一化为整型数组。 * * @return array */ private function normalizeRoomIds(mixed $roomIds): array { $items = is_array($roomIds) ? $roomIds : preg_split('/[\s,,]+/u', (string) $roomIds, -1, PREG_SPLIT_NO_EMPTY); return collect($items) ->map(fn (mixed $roomId): int => (int) $roomId) ->filter(fn (int $roomId): bool => $roomId > 0) ->unique() ->values() ->all(); } /** * 方法功能:返回题型对应的活动标题。 */ private function buildStartMessage(string $quizType, int $roundId, string $hint): string { $normalizedQuizType = $this->normalizeQuizType($quizType); $quizLabel = $this->getQuizTypeLabel($normalizedQuizType); $icon = $normalizedQuizType === Riddle::TYPE_BRAIN_TEASER ? '🧠' : '🧩'; return "{$icon} 【猜谜活动·{$quizLabel}】第 #{$roundId} 题开始!题面:{$hint}"; } /** * 方法功能:判断猜谜活动总开关是否处于启用状态。 */ private function isGameEnabled(?string $quizType = null): bool { $normalizedQuizType = $this->normalizeQuizType($quizType); $config = GameConfig::forGame($normalizedQuizType) ?? GameConfig::forGame(Riddle::TYPE_IDIOM); return (bool) $config?->enabled; } }