# sanguosha **Repository Path**: TXandJL/sanguosha ## Basic Information - **Project Name**: sanguosha - **Description**: sanguosha -简洁版 - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-19 - **Last Updated**: 2026-10-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 三国杀 · 精简版(2~8 人在线) 与斗地主同架构的 PHP 在线三国杀:**1V1 / 2V2 对战 + 4~8 人身份局**。 PHP + 文件状态 + 轮询,零数据库、零新环境,整个文件夹丢进站点根目录就能跑。 --- ## 一、模式 | 模式 | 人数 | 阵营 | |---|---|---| | 1V1 对战 | 2 | A 队 vs B 队 | | 2V2 对战 | 4 | 对角同队(座位 0/2 一队,1/3 一队) | | 身份局 4 人 | 4 | 1 主公 1 忠臣 1 反贼 1 内奸 | | 身份局 5 人 | 5 | 1 主公 1 忠臣 2 反贼 1 内奸 | | 身份局 6 人 | 6 | 1 主公 1 忠臣 3 反贼 1 内奸 | | 身份局 7 人 | 7 | 1 主公 1 忠臣 3 反贼 2 内奸 | | 身份局 8 人 | 8 | 1 主公 2 忠臣 4 反贼 1 内奸 | - 身份随机洗牌分配,主公开局公开,其余暗置;主公先手 - 胜负:主公死 → 反贼胜(若场上只剩内奸则内奸胜);反贼+内奸全灭 → 主忠胜;组队模式一方全灭判负 - 击杀反贼摸 3 张;主公误杀忠臣弃光手牌装备 - 阵亡亮身份,不影响对局(托管代打) ## 一点二、机器人(电脑玩家)—— 一个人也能开局 **所有 7 个模式**在等待页都能加电脑:房主点「+ 添加电脑」加一个,或点「一键补满(+N)」直接坐满, 人数够了就能开局(1V1 加 1 个、8 人局加 7 个,机制完全一样)。**只有房主能加/删,只有开局前能动。** | 行为 | 规则 | |---|---| | 加电脑 | `addbot {n}`:加 n 个,**自动截断**到坐满(要 9 个也不会多塞);已满报「座位已满」 | | 删电脑 | `delbot`:移除**最后一个**电脑(等待页每个电脑行尾有「移除」);没有电脑时报错 | | 开局后 | 加/删一律拒绝(座位表与角色/手牌按下标绑定,中途挪人整局错位) | | 真人加入满员房 | 末尾是电脑 → **顶替**它(返回 `swapped:true`);没有电脑可顶替才算真满 | | 房主离开 | 房主权限交给**下一个真人**(交给电脑 = 这间房再也开不了局) | | 再来一局 | 电脑留在原座位继续打 | 机器人是**一等座位**:与真人共用同一份座位结构(`new_seat()`),只多一个 `bot:true`。因此 - **永远代打**:机器人不轮询、没有心跳,`sgs_seat_hosted()` 对它恒为真(**先于**手动托管判断放行) - **有「思考」节奏**:两次动作之间隔 `SGS_BOT_SEC` 秒(默认 1)——否则一次轮询就把整个回合闪完, 玩家看不见它出牌。真人手动托管**不受**这个节奏限制(人开的托管要立刻见效) - **不会替真人出牌**:没托管、没掉线的真人永远被等(只有掉线/手动托管才由 AI 接) - **自动选将**:`phase=picking` 里机器人自己从候选池挑(日志写「自动选择」),真人只需要选自己那张 - **uid 不外泄也冒充不了**:`seats[i]` 不下发 uid;`seat_of()`(api.php)与 `sgs_public()`(engine.php) 找座位时**都跳过机器人座位**——否则猜到 uid 就能读到电脑手牌、替它出牌。**两处都要跳过**: `sgs_public` 并不经过 `seat_of`,它自己又扫了一遍 uid → `mySeat`(漏了那条会下发电脑手牌) - `SGS_BOT_SEC=0` 可关掉节奏(回归测试用,保证确定性);节奏本身由 `tests/sgs-bot.php` B 组断言 > ⚠️ 「结束出牌」**不等于**回合结束:手牌超过体力上限时引擎会转进**同一个人的**弃牌阶段, > 那时 `turnIdx` 仍是自己、`pending` 仍是 null。所以"回合有没有交出去"只能看 `turnIdx`, > "机器人有没有真的开回合"看日志里那句 `—— 电脑·甲 的回合 ——`(`sgs_start_turn` 无条件写)。 > 写测试断言时把这两个信号搞混,会得到一条**恒真**的假断言(详见文末批次记录)。 ## 一点三、电脑玩家「怎么想」—— AI 决策三层 电脑与托管共用同一套决策(`sgs_ai_*`,全在 `engine.php`)。分三层,后一层只在前一层给出的 候选里挑,**不做随机平手**: | 层 | 函数 | 干什么 | |---|---|---| | ① 阵营 | `sgs_ai_side` / `sgs_ai_hostility` | 我是哪边的、他是敌是友(`0` 自己人 / `1` 中立 / `2` 明确敌人) | | ② 价值 | `sgs_ai_threat` / `sgs_ai_card_value` | 谁最该打(残血、能斩、主公枢纽)、哪张牌最该留 | | ③ 决策 | `sgs_ai_turn` / `sgs_ai_choice` | 出什么牌、给谁、要不要响应 | **① 阵营 —— 反贼不再互相残杀** - 身份局按身份分边:主公/忠臣 = `loyal`、反贼 = `fan`、内奸 = `nei`;组队模式按 `team` 比 - 内奸:**谁势大打谁**(反贼多就打反贼、忠臣多就打忠臣),主公**留到最后**——只有 `(反贼数 + 忠臣数) === 0` 才把主公当敌人;反贼全灭后主公/忠臣必须把内奸升级为敌人, 否则收尾变成"主公单方面挨打",一局跑到步数上限也打不完 - ⚠️ 判"是不是主公"必须看 `role`(`=== 'zhu'`):`sgs_ai_side()` 把主公归进 `loyal`, 写成 `$b === 'zhu'` **永远不成立** → 内奸开局就咬主公(A4/A5/B7 就是这么抓出来的) **② 目标 —— 有理由,且不掷骰子** `残血 × 3` + 能斩 `+24` + 主公枢纽 `+12` + 明确敌人 `+4`;同分**按座位序取第一**。 旧版平手时 `random` 挑一个 → 一会儿打这个、一会儿打那个,观感就是"没有想法"。 **③ 出牌 —— 顺序有讲究,每步有门槛**(不值得就不出) 桃(受伤就吃)→ 无中生有/五谷(纯赚,先摸牌再决定)→ 装备(空位必填,武器按射程+技能比大小换) → 武将主动技(苦肉/裸衣/据守)→ 控制(乐给行动力强的、兵粮给手牌多的、拆优先拆武器、 顺手限距离 1、借刀只借敌人的刀)→ 输出(桃园只在"我方伤得更重"时开;杀优先射程内能斩的; 决斗要有第二张杀撑着或对手残血)。 **AOE(南蛮/万箭)三道门槛**:① 敌人不比队友少;② 自己手上有对应响应牌或血厚; ③ **队友没人只剩 1 血** —— 这一张的 1 点伤害对残血队友就是致命伤。③ 只看体力(明牌), 不看手牌,**不算偷看**;旧版只比人数,敌 3 友 3 时照样开,一刀下去顺手把自己人带走。 **响应的取舍**(`sgs_ai_choice`) - 桃:自救必出;救别人只救自己人,**内奸还要保主公**(主公一死反贼就赢);自己只剩一张桃 且血量告急时留着 - 无懈:**三态取值** —— `-1` 生效有害就掐掉、`+1` 被掐掉的正是我们想要的结果就救回、 `0` 与我们无关(无中生有这种小利)就省着不花。统一成一个式子: `$want = ($parity === 0) ? ($val < 0) : ($val > 0)` - 五谷:从亮牌池里挑**手牌价值最高**的那张;贯石斧/青龙偃月刀/麒麟弓只在目标是敌人且 代价划算时才发动 **暗牌不偷看**:装备区是明牌(按价值挑/拆),手牌只能随机抽 —— `sgs_ai_pick_resolve()` 里 "有装备挑装备、没装备才随机抽手牌"就是这个约束。棋子之间**只能看公开信息**(体力、手牌数、 装备、判定区),这条纪律决定了 AOE 门槛③只能看体力。 ## 一点五、武将(33 名 / 37 技能,开局自选) 魏国:曹操、司马懿、夏侯惇、张辽、许褚、郭嘉、甄姬、曹仁、夏侯渊 蜀国:刘备、关羽、张飞、诸葛亮、赵云、马超、黄月英、黄忠、魏延 吴国:孙权、甘宁、吕蒙、黄盖、周瑜、大乔、陆逊、孙尚香、周泰 群雄:吕布、华佗、貂蝉、小乔、张角、于吉 **开局选将**:房主点「开始游戏」后进入 `phase=picking` 的选将阶段—— - 系统给**每名玩家随机发 5 名候选武将**(体力/势力/性别/技能都显示在选将面板里) - **所有玩家的候选池互不重复**(洗牌后按座位切片),所以无论怎么选,**场上武将必然全局唯一** - 每人点选 1 名 → 点「确认选择」(双击卡片可直接确认);全房选满后自动开打,主公先手 - 别人选走的武将会实时显示在面板下方,序号进度 `已选 x/y` 可见 - 选将超时 30s(`SGS_PICK_SEC`)→ 系统自动选(挑候选里体力最高的); **掉线/退出或开了托管的座位立即代选**,绝不会被一个人卡住整局 - 候选数受武将总量约束:33 名武将时 **1~6 人局每人 5 名,7~8 人局每人 4 名**(8×5=40 > 33) - 体力在选定时定稿:按武将体力,身份局主公 +1(1v1/2v2 不加) | 类型 | 武将(技能) | |---|---| | 视作类 | 关羽·武圣(红牌当杀)、赵云·龙胆(杀闪互换)、华佗·急救(红牌当桃)、甘宁·奇袭(黑牌当拆桥)、于吉·蛊惑(声明基本牌/普通锦囊,质疑真假) | | 攻防类 | 张飞·咆哮(无限杀)、马超·铁骑(杀后判定为红锁闪)、黄忠·烈弓(目标手牌≥你体力或≤你攻击范围则不可闪)、吕布·无双(杀需 2 闪、决斗对方需 2 杀)、夏侯渊·神速(限一次弃 1 张当杀,不计次数) | | 摸牌类 | 周瑜·英姿(摸牌+1)、张辽·突袭(摸牌改为夺两家各 1 张)、甄姬·洛神(**回合开始先问要不要发动,黑色判定牌本身到手并可继续,红色结束,上限 8**)、黄月英·集智(用普通锦囊摸 1,延时锦囊不触发)+奇才(锦囊无距离)、曹仁·据守(**结束阶段询问:摸 3 张,然后跳过自己下一个回合;摸到的牌本回合用不出去**)、黄盖·苦肉(可多次:摸 2 张失 1 体力)、许褚·裸衣(限一次弃 1 张,本回合伤害+1) | | 回合类 | 吕蒙·克己(本回合未出杀免弃牌)、貂蝉·闭月(回合结束摸 1)、孙权·制衡(限一次弃任意张换等量)、刘备·仁德(限一次送牌,给≥2 张且已受伤回 1 点) | | 受伤/濒死类 | 曹操·奸雄(获得造成伤害的牌)、司马懿·反馈(拿伤害来源 1 张牌)、郭嘉·遗计(每 1 点伤害摸 2)、夏侯惇·刚烈(判定非红→来源弃 2 张,不足则受 1 伤)、周泰·不屈(濒死翻牌点数不同则回到 1 点)、小乔·天香(弃红桃转移伤害给他人,该角色摸 1) | | 响应/防御类 | 诸葛亮·空城(空手不可被杀/决斗)、陆逊·谦逊(不能被顺手牵羊/乐不思蜀指定)+连营(失去最后手牌摸 1)、大乔·流离(弃 1 张牌把杀转移给他人)、孙尚香·结姻(限一次弃 2 张,与受伤男性各回 1 点)+枭姬(失去装备摸 2)、张角·雷击(出闪判定黑桃→来源受 2 伤)+鬼道(黑牌替换判定牌,原牌归手牌)、魏延·狂骨(对距离 1 以内造成伤害后回 1 点) | - 体力按武将(3~4),身份局主公 +1;武将与技能对所有人公开(选定时即公开) - 视作技能前端出按钮:选中红牌出「当【杀】使用」/「当【桃】使用」/「当【过河拆桥】使用」, 于吉·蛊惑则声明基本牌/普通锦囊(真假只看牌名,假时质疑者摸牌、真时质疑者掉血) - 出牌阶段主动技按钮(都受服务端回合标记约束,用过即消失): 孙权「制衡」/ 刘备「仁德」/ 孙尚香「结姻」/ 黄盖「苦肉」; 许褚「裸衣」与夏侯渊「神速」需先选中一张手牌 - 结束阶段询问技:曹仁「据守」——弃牌结束后弹「发动 / 不发动」, 发动则摸 3 张并跳过自己**下一个**回合(摸到的牌本回合用不出去) - 多选类技能统一手势:点手牌多选 → 再点目标座位 → 「确认」(制衡只需选牌;仁德要选牌+目标; 结姻固定 2 张牌 + 一名已受伤男性角色) - 流离:成为【杀】目标时先弹面板问「发动【流离】」——选了就点 1 张手牌 + 一名其他角色转移; 放弃则回到正常【杀】响应(出闪或挨打) - 目标合法性同步服务端:空城(手牌为 0 的诸葛亮不能被【杀】【决斗】指定)、 谦逊(陆逊不能被【顺手牵羊】【乐不思蜀】指定)、结姻限男性且已受伤,不合法座位置灰并写明原因 - 受伤类技能仅存活时触发;刚烈的反制伤害不再连锁触发其他受伤技(防死锁) - 托管 AI 会使用视作牌(武圣当杀/急救当桃/龙胆互换)与主动技(苦肉 / 裸衣 / 据守) **技能「看得见」(服务端权威 + 前端显性反馈)** 技能判定 100% 在服务端(`sgs_skill()` / `sgs_convert()`),前端只负责展示。为了让"技能到底有没有生效" 一眼可见(而不是只能翻战斗记录数牌),牌桌上有四层反馈: - **武将徽标 + 技能芯片**:顶栏武将名旁列出本人技能,每个座位卡下方列出该玩家的技能; 悬停芯片显示技能说明 - **被动技能常驻提示**:出牌区上方固定一条「**被动技能(自动生效)**」,列出本人所有非主动技能 (英姿 / 奸雄 / 遗计 / 集智 / 奇才 / 空城 / 谦逊 / 连营 / 枭姬 / 狂骨 / 鬼道 / 天香 / 刚烈 / 反馈 / 铁骑 / 烈弓 / 无双 / 咆哮 / 克己 / 闭月 / 不屈 …)及其说明—— 这类技能没有按钮,会在对应时机自动触发 - **一问一步的「过程技能」**:洛神这类会连着做很多步的技能,**不在回合开始一次性结算**, 而是每做一步就回到询问(按钮写清已得几张),玩家随时收手 —— 见下方「洛神为什么改成逐次询问」 - **发动飘字**:任何人触发技能时,牌桌右上角弹出「XX 发动【技能名】」气泡,对应座位卡同时高亮闪一下; 由日志里 `发动【X】` 的单调 `seq` 增量驱动,只播**新**日志、不重播历史 - **选将面板标注**:候选武将的每个技能后面直接标 `主动` / `被动`,选人前就知道要不要手动点 > 被动提示的判定按技能表 `ACTIVE_SK` 过滤,**不按「按钮当前是否可点」判断**: > 黄盖的【苦肉】在 1 血时按钮会隐藏,但它依然是主动技(早期版本据此把它错标成被动)。 ## 二、牌库(116 张精简包) | 类别 | 牌 | |---|---| | 基础 | 杀 ×30、闪 ×22、桃 ×10 | | 锦囊 | 无中生有、决斗、过河拆桥、顺手牵羊、南蛮入侵、万箭齐发、桃园结义 | | 锦囊·无懈 | 无懈可击 ×4(任意锦囊生效前全场依次询问;无懈也能被无懈反制) | | 锦囊·延时 | 乐不思蜀 ×3(判定 ♥ 失效,否则跳过**出牌阶段**)、兵粮寸断 ×3(判定 ♣ 生效,跳过**摸牌阶段**)——两者均**无视距离**(2026-09-21 改,U1c 断言守住)、闪电 ×2(判定 ♠ 为 3 点雷电伤害,否则**移至下家**判定区) | | 锦囊·其他 | 借刀杀人 ×2(两个目标:持刀者 → 被杀者;令其出【杀】,否则交出武器)、五谷丰登 ×2(亮出与人数等量的牌,每人依次挑一张) | | 武器 | 诸葛连弩(不限杀)、青釭剑(无视八卦)、青龙偃月刀/贯石斧/方天画戟/麒麟弓(攻击范围 2~5) | | 防具/马 | 八卦阵 ×2(放弃出闪时自动判定,红色视作闪;判定牌会亮给全场)、+1 马 ×3、-1 马 ×3 | 延时锦囊走**判定区**(`seats[i].judged`,座位卡"其他"栏画单字徽标 乐 / 兵 / ⚡,全名进 title): 回合开始的判定阶段逐张结算,每张判定牌都广播给全场,且**每张生效前全场可问【无懈可击】** (被抵消 = 移出判定区进弃牌堆)。 ⚠️ **无懈的时机是"判定牌翻开、效果即将落实"的那一刻**(2026-09-22 修正): 置入判定区的瞬间还没有任何效果发生,那时就问无懈 = 让全场为一件没发生的事表态。 `sgs_wuxie_offer()` 只在 `sgs_delay_step()` 里调用;置入时只写日志 「【乐不思蜀】置入 … 的判定区(回合判定时才会结算)」。 旧的两条 effect 死路径 `delay_placed` / `delay_place_end` 已删(判定阶段被抵消的收尾在 `delay_after`)。 ## 三、规则实现 - 回合:**判定**(判定区的延时锦囊逐张结算;每张前全场可问【无懈可击】)→ 摸 2 张 (【兵粮寸断】生效则跳过)→ 出牌(每回合限 1 杀,连弩不限;【乐不思蜀】生效则跳过) → 弃牌(手牌上限 = 当前体力,可多选一次弃完) - 状态机:`waiting → picking(选将)→ playing → finished`;选将阶段不产生 pending / turnSub, 只接受 `pickhero`;候选池只发给本人(别人的候选是秘密,选定后武将才公开) - 距离:存活角色环上最短距离;-1 马进攻减距、+1 马防守加距;杀受武器攻击范围限制 - 目标合法性:前端只允许点合法目标(【顺手牵羊】距离必须为 1、【杀】须在攻击范围内), 不合法的座位置灰并显示距离/原因,点击会提示原因而不再静默失败;座位卡上显示「距 N」 - 到手反馈:牵羊/五谷等获得的牌会弹「获得【X】」提示并在手牌区高亮 4 秒 - 响应链:杀→闪、决斗→轮流出杀、南蛮→杀、万箭→闪、濒死→从濒死者起顺位求桃 - 拆/牵羊:目标有 2 张以上可选牌时**由出牌者亲自选**(手牌面朝下、装备正面显示); 只有唯一一张时直接结算;开启托管时由 AI 随机选(不会卡住) - 手动托管:顶栏「托管」按钮开关(AI 立即代打,跨回合持久;亲自操作或重连自动取消) - 打牌特效(**全场每个人都能看清「谁 对谁 出了什么」**):任何人出牌/响应(含闪/杀/救桃)时, 所有玩家的屏幕上同时出现三样东西,持续约 2.8s: ① **顶部横幅**「<出牌者> 对 <目标> 使用【牌名】」一行大字(响应类改为「<出牌者> 打出【闪】响应 <对方>」, 视作牌额外注明「(用【实体牌】当)」,错过牌面也不会漏看); ② 牌桌中央弹出**大牌面**(牌名 + 花色点数 + 指向文字,红/黑分色); ③ 出牌者 → 每个目标画**红色虚线箭头**(虚线流动),出牌者座位橙色脉动、目标座位红色脉动; 群体锦囊(南蛮/万箭/桃园)对每个目标各画一条箭头 - 掉线托管:对局中掉线/退出(15s 无心跳)的座位自动由 AI 代打,对局不卡住 - 真人玩家不操作不会被判超时,可以无限期思考 - 八卦阵在玩家选择"放弃"时自动发动判定(青釭剑无视);**判定牌本身会广播给全场** (`fx.skill='八卦阵'` + `fx.ok`,成败都发),且响应面板会提前写明「放弃=发动【八卦阵】」。 ## 三点五、界面 - 战斗记录默认收起,顶栏「战斗记录」按钮点击展开左侧抽屉(新日志有未读数字提示,Esc 关闭) - 牌桌中央改为局况卡片(牌堆剩余 / 局数 / 模式 / 当前回合) - 对局结束 → 结算面板提供「查看战斗记录」复盘;抽屉内「返回结算」可回到结算面板 - 退出房间入口四处统一:顶栏、等待房间弹窗、结算弹窗、结算后操作区(确认后回大厅,对局中退出由托管接替) - 手机端(≤980px)顶栏自动收纳:隐藏模式芯片/邀请按钮,按钮文案缩短(记录 / 退出),防溢出 - 技能可视化:座位卡下方的技能芯片、出牌区上方的「被动技能(自动生效)」条、 右上角的「发动【X】」飘字(座位同步高亮),选将面板里技能带 `主动`/`被动` 标记 - 打牌可视化:顶部「谁 对谁 出了什么」横幅 + 中央大牌面 + 指向箭头 + 出牌者/目标双向高亮 - 手牌**平铺**显示(相邻只留 6px 缝),一行放不下才自动折叠(见第九节); 牌面文字一律不换行(4 个字的「过河拆桥」、5 个字的「青龙偃月刀」都缩字号单行); 提示条独占一行并可折行,浮层提示(toast / 技能飘字 / 中央牌面副标)都有最大宽度 - **对手区 = 环形三段**(顶排横排 + 左列竖排 + 右列竖排,中央单列一格留给战场, 见第十二点五节);座位卡按需求精简成五层信息 ## 三点六、身份标记(「我判·反贼」)—— 只有自己看得见 身份局里除了主公,别人的身份都是暗的。玩家心里其实一直在猜("这人肯定反贼"), 但猜完没地方放,过两轮就忘了。座位卡上那个 `?` 就是放判断的地方: - 点座位卡上的 **`?`** → 弹出小浮层,选 **忠臣 / 反贼 / 内奸**(已标过的还能「清除」) - 选完 `?` 变成 **`我判·反贼`**(**虚线**框 —— 真实身份徽标是实线,两者会同屏出现,必须一眼可分) - **不占任何操作**:点身份徽标不会把这个人选成目标(身份不明时正是要打他的时候)。 这条由 `sgs-ui-wiring.php` ★X6(分支顺序)与 `sgs-ui-runtime.js` U9b(点完不该出现 `picked`)双向钉住 - 身份一公开(阵亡亮身份 / 终局 / 主公本来就亮)→ 我的判断**自动收起**: 真身份才是准的,同屏出现「真身份 + 我的猜测」会看混 - **开新局(第 N 局变化)自动作废**:身份重新洗牌,留着上一局的推断只会误导自己 ### 为什么不做服务端存储 「其他人看不到」这件事,**只有本地存储能靠结构保证**。做服务端存储就得在**每一个**下发出口 加过滤(公开状态 / 旁观 / 聊天 / 日志 / 结算 / 断线重连…),漏一处就是全场标记泄露; 放 `localStorage` 则从根上不存在「别人能看到」这条路径。 - 键:`sgs_marks:<房间号>:<我的 uid>`。**uid 是必须的** —— 少了它,同一台机器换个人登录 就能读到上一个人的推断;少了房间号会串到别的房间 - 值:`{"r": <第几局>, "m": {"2": "fan"}}`;`r` 就是上面那条「开新局作废」的判据 - 写入口**只有** `localStorage.setItem(markKey())`:标记**不产生任何请求** (U3c 直接数请求:标记前后 `calls` 数组必须一模一样) **测试**:`sgs-ui-wiring.php` X 组 15 条(含 ★X11:`engine.php` / `api.php` 里**连 `marks` 这个词都不许出现**;★X12:标记没有 act/api 出口)+ `sgs-ui-runtime.js` U 组 23 条 (点 `?` 开浮层 / 只写本地 / 换 uid 读不到 / 开新局作废 / 身份公开后收掉 / 点徽标不误选目标 / 点别处收起浮层)。 **Edge 真实浏览器冒烟(19 断言,全绿)**:DOM 桩测不到 CSS 与真实点击 —— 补了一趟 headless Edge:点真实的 `?` → 断言浮层**真的弹出且贴在徽标正下方**(boundingBox 对比)、 「我判·反贼」的 `borderTopStyle === 'dashed'`(computedStyle 实测)、 **真实 HTTP 的 state 响应原文里搜不到 `marks` / `我判`**(网络层的防泄露终审)、 刷新重进房间后标记还在(localStorage 持久)、全程零控制台报错。 ## 四、部署(宝塔 / IIS 通用) 1. `sgs/` 整个目录上传到站点根目录(`tests/` 可不上传) 2. `data/` 权限 777(含 .htaccess / web.config 防直读) 3. 打开 `http://你的域名/sgs/` → 选模式 → 建房 / 输房间号加入 要求:PHP 7.0+(`random_int`),无需任何扩展、无需数据库。 ## 五、文件清单 ``` sgs/ ├── index.html 前端(大厅 + 选将面板 + 牌桌 + 聊天抽屉,含全部 CSS/JS) ├── api.php 后端路由(建房/加入/开局/选将/出牌/响应/选牌/弃牌/聊天/再来一局 │ + 制衡/裸衣/据守/神速/仁德/结姻/苦肉/托管 + 加电脑/移除电脑) ├── engine.php 核心引擎(选将阶段 + 回合状态机 + 响应链 + 伤害/濒死/死亡 + 37 技能 │ + AI 托管/机器人代打 + 机器人思考节奏) ├── heroes.php 武将库(33 名武将定义 + 候选池发放 + 自动选将) ├── cards.php 牌库(100 张,顺序随机) ├── data/ 房间状态(需可写) └── tests/ 开发用(可不上传) ``` ## 六、本地/测试 ``` # 极速全托管(测试用,0 秒超时) SGS_PLAY_SEC=0 SGS_RESP_SEC=0 php -S 127.0.0.1:8921 -t . SGS_ORIGIN=http://127.0.0.1:8921 node tests/sgs-flow.js # 端到端 94 断言 # 10 秒节奏服务器(交互类测试用) SGS_PLAY_SEC=10 SGS_RESP_SEC=10 php -S 127.0.0.1:8923 -t . node tests/sgs-ring.js # 顶栏托管按钮稳定性 12 断言 node tests/sgs-discard.js # 弃牌多选 10 断言 node tests/sgs-pick-ui.js # 拆/牵羊选牌面板 11 断言 node tests/sgs-hp.js # 血条可读性 14 断言 # 1 秒节奏服务器(对局快速打完,验证结算流程) SGS_PLAY_SEC=1 SGS_RESP_SEC=1 php -S 127.0.0.1:8925 -t . node tests/sgs-log.js # 战斗记录折叠/复盘 29 断言 node tests/sgs-leave.js # 退出房间入口 20 断言(含窄屏顶栏可见性) node tests/sgs-duel-ui.js # 决斗面板/掉血/回合权 18 断言(需 20s 节奏服务器) node tests/sgs-qian-ui.js # 牵羊目标合法性/到手反馈 25 断言(需 60s 节奏服务器) # 引擎单测(无需服务器) php tests/sgs-pick.php # 拆/牵羊选牌逻辑 29 断言 php tests/sgs-dying.php # 濒死求桃/低血弃牌/链恢复 55 断言 php tests/sgs-qian.php # 牵羊/拆桥到手与落点/守恒/全模式压力 88 断言(含牌张守恒 + D 组判据定场景) php tests/sgs-duel.php # 决斗引擎逻辑 20 断言 php tests/sgs-hero.php # 第一批 17 武将技能 70 断言(含洛神逐次询问) php tests/sgs-hero2.php # 第二批 16 武将技能 89 断言(含开局发牌/主公+1 严格契约、33 武将 8 人局压力) php tests/sgs-pickhero.php # 开局选将(候选/去重/超时/掉线)62 断言 php tests/sgs-bot.php # 机器人(电脑玩家)36 断言 # 恒托管 / 思考节奏(1 秒内只动一次,真人手动托管不受限)/ # 全机器人 4 人局与 8 人局能自己打完且牌张守恒(116/116 唯一)/ # 机器人自动选将 / 混编局一定停在真人面前(日志无「超时托管」) php tests/sgs-ai.php # 电脑玩家「决策质量」71 断言(详见「一点三」与文末批次记录) # A 阵营(反贼不互殴/内奸维持平衡)/ B 目标排序(残血、能斩、不随机)/ # C 出牌(顺序、门槛、AOE 队友残血不开、黑名单不空转)/ # D 弃牌选牌(按价值升序、装备是明牌挑最好的)/ # E 响应(救自己人/不救敌人/无懈三态/五谷挑最值)/ # F 全机器人局宏观:**同阵营零攻击** + 动作数 ≥ 15(防空转)+ # 4/8 人局都能打完、不靠拖步数上限、全程牌张守恒 php tests/sgs-fx.php # 打牌特效广播 + 八卦阵判定牌/失败仍可出闪 + 过程队列/洛神逐次判定 + **牌桌文案(动词/归属/无双)** + **五张新锦囊(U 组:延时判定/跳过阶段/闪电传递/借刀两分支/五谷亮牌池/无懈首轮不问使用者)** 227 断言 php tests/sgs-ui-wiring.php # 前端接线契约(无需浏览器)305 断言 # data-act 都有分发分支 / act() 都是真实路由 / # 主动技都有入口 / canConvert 覆盖全部视作 / 回合标记约束 / # 选将面板接线(候选渲染、倒计时、pickhero 路由、选将期托管)/ # 加电脑(房主/名额/截断/开局后拒绝)/ 前端加电脑与移除入口 / # ★seat_of 与 sgs_public 都要跳过机器人 / # ★state.n = 模式需要人数 / ★机器人恒托管先于 hostOn / # 四处动作点都接了机器人节奏 / 加电脑会截断到坐满 # 手牌不再闪的三个前提(高亮到期、动画只播一次、滚动位置保留)/ # 技能可见化(ACTIVE_SK 集合与武将表一致、主动技都有入口、 # 被动提示按 ACTIVE_SK 过滤、飘字挂在 sync 上、#skflash 先于