PHP递归函数嵌套while失效,能否用循环实现瘟疫桌游爆发逻辑?
解决《瘟疫危机》爆发逻辑的递归问题与非递归实现
嘿,我来帮你排查递归失效的问题,同时给你一个基于循环的非递归实现方案!
先说说你递归代码的问题
你的递归逻辑失效,大概率不是因为嵌套在while循环里,而是这几个关键点没处理好:
- 缺少「同一次连锁爆发中城市仅爆发一次」的限制
瘟疫危机的规则里,同一个城市在一轮连锁爆发中只能触发一次爆发(不然会无限循环,比如A连B、B连A的情况,爆发后会互相反复触发)。你的代码里没有标记哪些城市已经在本次连锁中处理过,递归调用时会反复触发同一城市的爆发,导致逻辑混乱甚至死循环。 - INSERT语句的逻辑隐患
你先尝试INSERT新记录,如果失败才去更新已有记录。但递归调用时,连接城市大概率已经有记录,INSERT会失败进入更新逻辑,若没开启mysqli错误报告,可能隐藏执行错误。另外这种先插后更的写法不够严谨,建议用INSERT ... ON DUPLICATE KEY UPDATE替代,更高效可靠。 - 没有处理事务一致性
爆发是连锁原子操作,若不用数据库事务包裹整个流程,可能出现部分城市更新成功、部分失败的情况,导致游戏状态不一致。
非递归实现方案(队列+while循环)
不用递归的话,我们可以用队列模拟连锁爆发流程,更直观也更容易调试控制。核心思路是:把需要处理的城市加入队列,逐个取出处理;若触发爆发,就把未处理过的连接城市加入队列,直到队列为空。
完整代码示例
function AddInfectionCubes($target_city, $initial_cubes, $game_id) { // 开启事务,保证爆发流程原子性 mysqli_begin_transaction($conn); try { // 记录本次连锁中已处理的城市,避免重复触发 $processed_cities = []; // 队列存储待处理的城市与要添加的立方体数量 $queue = [[$target_city, $initial_cubes]]; while (!empty($queue)) { // 取出队列头部的任务 list($city_name, $n_cubes) = array_shift($queue); // 跳过已处理过的城市 if (in_array($city_name, $processed_cities)) { continue; } $processed_cities[] = $city_name; // 获取当前城市的立方体数量(无记录则默认0) $sql_get = "SELECT disease_cubes FROM game_city WHERE city_name = ? AND id_game = ?"; $stmt = mysqli_prepare($conn, $sql_get); mysqli_stmt_bind_param($stmt, "si", $city_name, $game_id); mysqli_stmt_execute($stmt); $result = mysqli_stmt_get_result($stmt); $current_cubes = 0; if ($row = mysqli_fetch_assoc($result)) { $current_cubes = $row['disease_cubes']; } mysqli_stmt_close($stmt); // 计算添加后的总立方体数 $new_cubes = $current_cubes + $n_cubes; if ($new_cubes > 3) { // 触发爆发:城市最多保留3个立方体 $final_cubes = 3; // 更新爆发标记 $stmt = mysqli_prepare($conn, "UPDATE Game SET outbreak_marker = outbreak_marker + 1 WHERE id = ?"); mysqli_stmt_bind_param($stmt, "i", $game_id); mysqli_stmt_execute($stmt); mysqli_stmt_close($stmt); // 获取当前城市的所有连接城市 $stmt = mysqli_prepare($conn, "SELECT city_to FROM connections WHERE city_from = ?"); mysqli_stmt_bind_param($stmt, "s", $city_name); mysqli_stmt_execute($stmt); $result = mysqli_stmt_get_result($stmt); // 将未处理的连接城市加入队列,每个添加1个立方体 while ($row = mysqli_fetch_assoc($result)) { $connected_city = $row['city_to']; if (!in_array($connected_city, $processed_cities)) { $queue[] = [$connected_city, 1]; } } mysqli_stmt_close($stmt); } else { // 未触发爆发,直接设置新的立方体数 $final_cubes = $new_cubes; } // 插入或更新城市记录(用ON DUPLICATE KEY UPDATE保证原子性) $stmt = mysqli_prepare($conn, " INSERT INTO game_city(id_game, city_name, disease_cubes) VALUES(?, ?, ?) ON DUPLICATE KEY UPDATE disease_cubes = ? "); mysqli_stmt_bind_param($stmt, "isii", $game_id, $city_name, $final_cubes, $final_cubes); mysqli_stmt_execute($stmt); mysqli_stmt_close($stmt); } // 所有操作完成,提交事务 mysqli_commit($conn); } catch (Exception $e) { // 出错回滚事务 mysqli_rollback($conn); error_log("Infection cube error: " . $e->getMessage()); throw $e; } } // 调用示例 // AddInfectionCubes("Atlanta", 2, 1);
这个方案的优势
- 避免栈溢出:深层连锁爆发时递归可能触发PHP栈溢出,队列+循环无此问题。
- 易调试:可随时打印队列和已处理城市状态,快速排查问题。
- 状态一致:事务保证整个爆发流程要么全部成功,要么全部回滚,不会出现游戏状态错乱。
- 符合规则:严格限制同一连锁中每个城市仅处理一次,避免无限循环。
额外优化建议
- 加联合唯一索引:给
game_city表的id_game和city_name加联合唯一索引,确保INSERT ... ON DUPLICATE KEY UPDATE正常工作。 - 限制爆发次数:瘟疫危机中爆发次数达阈值会游戏失败,可在代码中添加判断,达到阈值时终止游戏逻辑。
- 持续使用预处理语句:避免SQL注入,同时提升数据库执行效率。
内容的提问来源于stack exchange,提问作者David Leites
相关产品推荐
相关产品推荐

