From b924b6e864afc5ae4ff41bbbdea68f55e8f13c94 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sun, 23 Aug 2026 14:59:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8C=E4=B8=83=E7=8E=8B=EF=BC=9A=E7=AC=AC?= =?UTF-8?q?=E5=85=AB=E8=BD=AE=E6=A0=B8=E5=AF=B9=EF=BC=88=E6=B5=81=E7=A8=8B?= =?UTF-8?q?/=E7=89=8C=E5=9E=8B/=E7=8E=A9=E6=B3=95/=E7=AE=97=E5=88=86?= =?UTF-8?q?=EF=BC=89+=20=E6=96=B0=E5=A2=9E=E9=98=B6=E6=AE=B5=E6=9C=BA?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E7=9F=A9=E9=98=B5=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 规则方复述「开局庄家先叫分、不允许不叫分;第一局无庄家时默认座位0开始叫分」, 据此按四个维度重新取证,未发现不一致。 新增此前从未做过的取证:**阶段机迁移矩阵**。前几轮每个 handler 只测了 1~2 条 代表性失败路径,从没系统验证过「某个请求在别的阶段会不会被误放行、会不会静默 丢弃」。本轮把 8 个 RPC × 5 个阶段做成 40 格矩阵逐格驱动,全部符合:每个 RPC 只在应允阶段受理,其余一律回 STEP 失败包,无静默丢弃。 叫分起始者规则逐条取证,与规则方复述一致: - 第一局 firstseat=0、待叫者=0 - 之后每局起始 = 暂定庄家:连打 6 局逐局比对「上局 banker + result」 - 庄赢连庄:随机对局几乎打不出 result=0,用「叫70分 + 首家出最大牌」专门构造 - 庄输/投降顺延下家 - 首家不允许「不叫」:第一局与其后每局(含连庄局)发 call=0 一律回 RULE, 且叫分过程不变、仍轮到他自己 固化为 test/test_flow.js(7 项聚合断言,0.3s)。断言里带 cells===40,防止驱动 失败导致整行「跳过」而假绿。变异检验:去掉 chupai/maipai 的阶段校验、把 jiaofen 的阶段校验改成静默丢弃 —— 三条全部转红。 全套单测 540 → 547 项全绿。 Co-Authored-By: Claude Opus 5 (1M context) --- .../docs/compliance/01-design合规逐节核对.md | 42 ++++ server/games/erqiwang/test/README.md | 1 + server/games/erqiwang/test/test_flow.js | 195 ++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 server/games/erqiwang/test/test_flow.js diff --git a/server/games/erqiwang/docs/compliance/01-design合规逐节核对.md b/server/games/erqiwang/docs/compliance/01-design合规逐节核对.md index 3b62e2c..8935fcf 100644 --- a/server/games/erqiwang/docs/compliance/01-design合规逐节核对.md +++ b/server/games/erqiwang/docs/compliance/01-design合规逐节核对.md @@ -250,6 +250,48 @@ design §4(暗牌只有庄家可见 / 70 分亮 3 秒)、§9(查牌模式 --- +## 第八轮核对(按 流程 / 牌型 / 玩法 / 算分 四个维度复核) + +> 规则方复述了一条叫分规则:「开局庄家先叫分,不允许不叫分;第一局在没有庄家的情况下,默认座位号 0 玩家开始叫分」。据此按四个维度重新取证,**未发现不一致**。 + +**新增取证(此前从未做过):阶段机迁移矩阵。** 前几轮每个 handler 只测了 1~2 条代表性失败路径,从没系统验证过「某个请求在**别的**阶段会不会被误放行、会不会静默丢弃」。本轮把 8 个 RPC × 5 个阶段做成 **40 格矩阵**逐格驱动: + +| | step1 叫分 | step2 选主/投降 | step3 埋牌 | step5 出牌 | step6 结算 | +| --- | --- | --- | --- | --- | --- | +| jiaofen | ✓ 受理 | × STEP | × STEP | × STEP | × STEP | +| xuanzhu | × STEP | ✓ 受理 | × STEP | × STEP | × STEP | +| touxiang | × STEP | ✓ 受理 | × STEP | × STEP | × STEP | +| maipai | × STEP | × STEP | ✓ 受理 | × STEP | × STEP | +| chupai | × STEP | × STEP | × STEP | ✓ 受理 | × STEP | +| mingpai | × STEP | × STEP | × STEP | ✓ 受理 | × STEP | +| tishi | × STEP | × STEP | × STEP | ✓ 受理 | × STEP | +| zhunbei | × STEP | × STEP | × STEP | × STEP | ✓ 受理 | + +**40 格全部符合**,无误放行、无静默丢弃。(`mingpai` 在 step5 还需「已有人报无主」这一前置,首轮矩阵曾把它标红——是期望表漏了前置条件,不是缺陷。) + +**叫分起始者规则逐条取证**(`design §4.2 + §4.6`,与规则方复述一致): + +| 规则 | 取证 | 结果 | +| --- | --- | --- | +| 第一局默认座位 0 开始叫分 | `makewar → do_new_paiju(0)` → `firstseat=0`、待叫者=0 | ✅ | +| 之后每局由**暂定庄家**先叫 | 连打 6 局,每局 `firstseat` 与「上局 banker + result」逐局比对 | ✅ | +| 庄赢 → **连庄**(起始仍是该庄家) | 随机对局几乎打不出庄赢,用「叫 70 分 + 首家出最大牌」专门构造出 `result=0` 局 | ✅ | +| 庄输 / 投降 → 起始顺延到下家 | 6 局中逐局验证 | ✅ | +| **首家不允许"不叫"** | 第一局与其后每一局(含连庄局)的首家发 `call=0`,一律回 `RULE` 且叫分过程不变 | ✅ | + +四个维度的现有取证汇总: + +| 维度 | 取证手段 | 规模 | +| --- | --- | --- | +| **流程** | 阶段机 40 格矩阵、叫分序列 DFS 80 条终局、多局大局(6 局/12 局)、轮庄含连庄、中途解散、端到端整局 | 本轮新增矩阵 | +| **牌型** | §3 等级序列逐级、§5.3 相邻链(副牌同花色 / 主牌不限花色 / 同档位不相邻)、§5.2 跟牌穷举差分 11 万组 | `test_arith`/`test_followdiff` | +| **玩法** | §5.4 甩牌最大性 6000 例差分、甩错惩罚、§5.4.4 分量拆解、整局带牌型模糊每墩胜者差分、§9 查牌门控、下发面泄露审计 1.8 万次判定 | `test_fuzz`/`test_leak` | +| **算分** | §7 算子全量对拍 7308 格 + design 表 141 格、§6.3 扣底、§8.1 算奖、§8.4 分配公式、结算零和 | `test_calc`/`test_paiju` | + +**固化产物**:新增 `test/test_flow.js`(7 项聚合断言,0.3s)。变异检验:去掉 `chupai`/`maipai` 的阶段校验、把 `jiaofen` 的阶段校验改成静默丢弃——三条全部转红。全套单测 **540 → 547 项**全绿。 + +--- + ## 结论摘要 > **注**:下表是**首轮核对**的初始发现快照(保留以记录起点)。其中 §4.4、§5.4、§6.3、§7、§8.1-8.4、§9、§10、§11 等标 🟥/🟧/🟨 的项**后续均已修复并验证**——当前逐节状态以本文档后面的分节详情及全套单测(270 项全绿)为准。 diff --git a/server/games/erqiwang/test/README.md b/server/games/erqiwang/test/README.md index d975190..f088095 100644 --- a/server/games/erqiwang/test/README.md +++ b/server/games/erqiwang/test/README.md @@ -44,6 +44,7 @@ node server/games/erqiwang/test/test_arith.js # 单独跑某一组 - `test_calc.js`:**§7 算子全量对拍**。参考实现逐字转写自 §7.1/§7.2/§7.3 的判定表,与 `get_base_bycall` / `get_qvalue` / `get_upgrade` 比对 **2 种算子模式 × 14 个叫分档 × grade 0~260 = 7308 格**(每格比 base/Q/判定倍率/最终子数四项),另按 design 的 17 张表逐格抽查 141 个"写死的最终子数"。断言按(模式,叫分档)与表名分组,失败能直接定位到档位。 - `test_followdiff.js`:**§5.2 跟牌强制层级穷举差分**。参考裁定按 §5.2 原文写(缺门/不足/够牌 × 单张/对子/N 连对 + 最长拖拉机优先的档案比较),对每手牌**枚举全部 C 张出牌组合**逐个比对"参考允许 vs `can_followcard` 允许",任何一侧多放行或多拒绝都会被抓到(约 11 万组)。 +- `test_flow.js`:**阶段机迁移矩阵 + 叫分起始者**(design §4.2/§4.6/§12.1)。① 8 个 RPC × 5 个阶段 = **40 格**逐格验证「该受理的受理、不该受理的一律回 `STEP` 失败包」——此前每个 handler 只测了 1~2 条代表性失败路径,没系统覆盖「某请求在别的阶段会不会被误放行 / 会不会静默丢弃」;断言里带 `cells === 40`,防止驱动失败导致整行"跳过"而假绿。② 第一局起始叫分者 = 座位 0、之后每局 = 暂定庄家(**庄赢连庄 / 庄输下家**)、每局首家都不能"不叫";连庄分支(`result=0`)随机打不出来,用「叫 70 分 + 首家出最大牌」专门构造。 - `test_leak.js`:**下发面泄露审计**(server 红线「发全 ≠ 发多,按可见性下发」+ design §4/§9/§11)。跑 6 局(可查牌/不查牌 × 叫 65/70/5),对每一个「服务器 → 某座位」的下发面——含各 RPC 的逐座位包、各阶段重连快照、明牌应答——深度扫描出其中所有牌 id,逐个判定该座位此刻**是否有权知道这张牌**(自己的牌 / 已打出 / 庄家的暗牌与底牌 / 70 分亮 3 秒 / 结束亮底牌 / §9 明牌)。约 1800 个下发面、1.8 万次判定。此前各处门控只有逐条手工核对,没有系统验证。 - `test_fuzz.js`:**整局模糊 + 每墩胜者差分**。`test_endgame.js` 的驱动器只会出单张,所以对子/拖拉机/甩牌/甩错在「整局链路」里从未跑过——单测逐个覆盖了 `can_playcard`/`can_followcard`,但它们与 `do_playcard`/`mod.chupai`/结算的**集成**是空白。本文件用带牌型的对局补上(20 局约 480 墩,四种房间选项组合轮换),并对**每一墩**用独立参考实现重算"谁最大"与服务端比对(不是抽查,每墩都比),另独立重算闲家捡分、扣底倍数、算奖与捡分子数的分配。提交时还会把牌按**最坏顺序**摆(`[K,K,Q,Q]→[K,Q,K,Q]`,把每个对子拆开),压服务端判定的入参顺序无关性。 diff --git a/server/games/erqiwang/test/test_flow.js b/server/games/erqiwang/test/test_flow.js new file mode 100644 index 0000000..80695db --- /dev/null +++ b/server/games/erqiwang/test/test_flow.js @@ -0,0 +1,195 @@ +// 流程完整性(design §4.2 / §4.6 / §12.1 阶段机) +// +// ① **阶段机迁移矩阵**:8 个 RPC × 5 个阶段 = 40 格,逐格验证「该受理的受理、不该受理的 +// 一律回 STEP 失败包」。此前每个 handler 只测了 1~2 条代表性失败路径,没有系统覆盖 +// 「某个请求在别的阶段会不会被误放行 / 会不会静默丢弃」。 +// ② **叫分起始者**:第一局默认座位 0;之后每局起始叫分者 = 暂定庄家(庄赢连庄 / 庄输下家); +// 每一局的首家都不能"不叫"。连庄分支单独构造(叫 70 分 + 首家出最大牌)才走得到。 +// +// 随机源与发牌都接固定种子,结果可复现。 +const R = require('./_rpc.js'); const mod = R.mod; +const P = global.cls_youle_erqiwang_paiju, A = global.cls_youle_erqiwang_arith; +const D = require('../class.desk.js'); +const t = require('./_assert')(); +let seed = 0x5A5A01; +const rnd = n => { seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; seed >>>= 0; return seed % n; }; +global.min_ontimeout = fn => fn(); +global.min_random = (a, b) => a + rnd(b - a + 1); +const pk = (s, d) => ({ conmode: 0, fromid: s, data: Object.assign({ agentid: 1, playerid: s, gameid: 1, roomcode: 1, seat: s }, d || {}) }); +const ERR = mod.ERR; + +function mkRoom(rt, asetcount) { + const sent = []; + const o_room = { + roomtype: rt, asetcount: asetcount || 6, roomcode: 1, createtime: 'T', makewartime: 'T', + seatlist: [0, 1, 2].map(i => ({ conmode: 0, fromid: i, playerid: 100 + i, nickname: 'P', avatar: '', gameinfo: {} })), + method: { sendpack_toother: m => sent.push(JSON.parse(JSON.stringify(m))) } + }; + const desk = D.new(o_room); o_room.o_desk = desk; + global.youle_erqiwang.app = { SendPack: m => sent.push(JSON.parse(JSON.stringify(m))) }; + global.youle_erqiwang.import = { check_player: () => o_room, deduct_roomcard: () => { }, save_grade: () => { } }; + mod.import = global.youle_erqiwang.import; mod.app = global.youle_erqiwang.app; + return { o_room, desk, sent }; +} +// 把牌局驱动到指定阶段。call=70 时保留投降资格 +function driveTo(step, call) { + const c = mkRoom('00000'); + D.do_new_paiju(c.desk, 0); + const pj = c.desk.method.curr_paiju(); + if (step >= 2) { + mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: call || 65 })); + if (pj.step === 1) mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 })); + if (pj.step === 1) mod.jiaofen(pk(pj.method.get_callgrade_seat(), { call: 0 })); + } + if (step >= 3) mod.xuanzhu(pk(pj.banker, { flower: 1 })); + if (step >= 5) mod.maipai(pk(pj.banker, { cards: P.get_seat_cards(pj, pj.banker).slice(-8) })); + if (step >= 6) { + let g = 0; + while (pj.step === 5 && ++g < 400) { + const s = pj.playproc.currseat, h = P.get_seat_cards(pj, s); + let p = null; + if (s === pj.playproc.start) p = [h[h.length - 1]]; + else for (const x of h) if (A.can_followcard(pj.flower, h, [x], pj.playproc.startcount, pj.playproc.startflower, pj.playproc.starttype).result) { p = [x]; break; } + if (!p) break; + mod.chupai(pk(s, { cards: p })); + } + } + return { c, pj }; +} +// 各 RPC 在「本阶段的正确发起者 + 正确入参」下发一次;返回是否被受理 +const FIRE = { + jiaofen: (c, pj) => { const s = pj.step === 1 ? pj.method.get_callgrade_seat() : 0; mod.jiaofen(pk(s, { call: 65 })); }, + xuanzhu: (c, pj) => mod.xuanzhu(pk(pj.banker >= 0 ? pj.banker : 0, { flower: 1 })), + touxiang: (c, pj) => mod.touxiang(pk(pj.banker >= 0 ? pj.banker : 0)), + maipai: (c, pj) => { const b = pj.banker >= 0 ? pj.banker : 0; const h = P.get_seat_cards(pj, b); mod.maipai(pk(b, { cards: h.slice(-8) })); }, + chupai: (c, pj) => { + const s = (pj.playproc && pj.playproc.currseat >= 0) ? pj.playproc.currseat : (pj.banker >= 0 ? pj.banker : 0); + const h = P.get_seat_cards(pj, s); + mod.chupai(pk(s, { cards: h.length ? [h[h.length - 1]] : [0] })); + }, + mingpai: (c, pj) => { + // design §9:明牌按钮在【有人报无主之后】才出现,故 step5 还要先满足这个前置 + if (pj.step === 5) { const s2 = (pj.banker + 1) % 3; for (const cd of P.get_seat_zhucards(pj, s2)) pj.cards[cd].playround = 1; pj.seatlist[s2][4][0] = 0; } + mod.mingpai(pk(pj.banker >= 0 ? (pj.banker + 1) % 3 : 1)); + }, + tishi: (c, pj) => mod.tishi(pk(pj.banker >= 0 ? (pj.banker + 1) % 3 : 1, { tip: 1 })), + zhunbei: (c, pj) => mod.zhunbei(pk(0)) +}; +// design 允许的 (rpc, step):其余阶段一律应回 STEP +const ALLOWED = { jiaofen: [1], xuanzhu: [2], touxiang: [2], maipai: [3], chupai: [5], mingpai: [5], tishi: [5], zhunbei: [6] }; +const STEPS = [1, 2, 3, 5, 6]; + +const bad = []; +let cells = 0; // 实际比对的格数,防止驱动失败导致整行"跳过"而假绿 +console.log('阶段机迁移矩阵(行=RPC,列=阶段;✓受理 ×拒(STEP) ?拒(其他码))'); +console.log(' step1 step2 step3 step5 step6'); +for (const rpc in ALLOWED) { + const row = []; + for (const st of STEPS) { + // touxiang 需要 call=70 才有资格;其余用 65 + const { c, pj } = driveTo(st, rpc === 'touxiang' ? 70 : 65); + if (pj.step !== st) { row.push(' 跳过'); continue; } + c.sent.length = 0; + FIRE[rpc](c, pj); + const fails = c.sent.filter(m => m.data && m.data.success === false); + const accepted = c.sent.length > 0 && fails.length === 0; + const stepRejected = fails.length > 0 && fails[0].data.errcode === ERR.STEP; + const should = ALLOWED[rpc].indexOf(st) >= 0; + let mark; + if (accepted) mark = ' ✓ '; + else if (stepRejected) mark = ' × '; + else if (fails.length) mark = ' ?' + fails[0].data.errcode + ' '; + else mark = ' 无包 '; + row.push(mark); cells++; + if (should && !accepted) bad.push(`${rpc} 在 step${st} 应受理却未受理(${mark.trim()})`); + if (!should && accepted) bad.push(`${rpc} 在 step${st} 不应受理却受理了`); + if (!should && !accepted && !stepRejected && fails.length) bad.push(`${rpc} 在 step${st} 被拒但错误码不是 STEP(${fails[0].data.errcode})`); + if (!should && !fails.length) bad.push(`${rpc} 在 step${st} 既没受理也没回失败包(静默丢弃)`); + } + console.log(rpc.padEnd(10) + row.join('')); +} +t.eq('§12.1 阶段机迁移矩阵(8 RPC × 5 阶段 = 40 格)无异常', bad.slice(0, 3), []); +t.eq('阶段机矩阵覆盖 40 格', cells, 40); + +// ===================== 叫分起始者规则 ===================== +console.log('\n叫分起始者规则(design §4.2 + §4.6)'); +const errs2 = []; +const c2 = mkRoom('00000'); +D.do_new_paiju(c2.desk, 0); +let pj = c2.desk.method.curr_paiju(); +if (pj.firstseat !== 0) errs2.push('第一局起始叫分者应为座位0,实为 ' + pj.firstseat); +if (pj.method.get_callgrade_seat() !== 0) errs2.push('第一局待叫者应为座位0'); +// 首家不能不叫 +c2.sent.length = 0; mod.jiaofen(pk(0, { call: 0 })); +if (!(c2.sent.length === 1 && c2.sent[0].data.success === false && c2.sent[0].data.errcode === ERR.RULE)) errs2.push('首家"不叫"未被按 RULE 拒'); +if (pj.method.get_callgrade_seat() !== 0) errs2.push('首家被拒后仍应轮到他自己'); +t.eq('§4.6 第一局起始叫分者 = 座位 0', [pj.firstseat, 0], [0, 0]); +// 跨局:起始叫分者 = 暂定庄家(庄赢连庄 / 庄输下家) +const seen = { 0: 0, 1: 0, 2: 0 }; +for (let r = 1; r <= 8; r++) { + const cur = c2.desk.method.curr_paiju(); + if (cur.method.get_callgrade_seat() !== cur.firstseat) errs2.push('第' + r + '局待叫者≠firstseat'); + // 该局首家不叫必须被拒 + c2.sent.length = 0; mod.jiaofen(pk(cur.firstseat, { call: 0 })); + if (!(c2.sent.length === 1 && c2.sent[0].data.errcode === ERR.RULE)) errs2.push('第' + r + '局首家不叫未被拒'); + // 正常打完这一局 + mod.jiaofen(pk(cur.method.get_callgrade_seat(), { call: 65 })); + if (cur.step === 1) mod.jiaofen(pk(cur.method.get_callgrade_seat(), { call: 0 })); + if (cur.step === 1) mod.jiaofen(pk(cur.method.get_callgrade_seat(), { call: 0 })); + const b = cur.banker; seen[b]++; + mod.xuanzhu(pk(b, { flower: 1 })); + mod.maipai(pk(b, { cards: P.get_seat_cards(cur, b).slice(-8) })); + let g = 0; + while (cur.step === 5 && ++g < 400) { + const s = cur.playproc.currseat, h = P.get_seat_cards(cur, s); + let p = null; + if (s === cur.playproc.start) p = [h[h.length - 1]]; + else for (const x of h) if (A.can_followcard(cur.flower, h, [x], cur.playproc.startcount, cur.playproc.startflower, cur.playproc.starttype).result) { p = [x]; break; } + if (!p) break; + mod.chupai(pk(s, { cards: p })); + } + const expNext = (cur.result === 0) ? b : (b + 1) % 3; + if (r < 6) { + mod.zhunbei(pk(0)); mod.zhunbei(pk(1)); mod.zhunbei(pk(2)); + const nx = c2.desk.method.curr_paiju(); + if (nx === cur) { errs2.push('第' + r + '局后未开新局'); break; } + if (nx.firstseat !== expNext) errs2.push('第' + (r + 1) + '局起始叫分者应为 ' + expNext + ',实为 ' + nx.firstseat); + } else break; +} +t.eq('§4.2/§4.6 叫分起始者与首家必叫(连打 6 局)无异常', errs2.slice(0, 3), []); + +// ===================== 连庄分支(result=0):庄赢时起始叫分者不变 ===================== +console.log('\n连庄分支(design §4.6 庄赢→暂定庄家仍为该玩家)'); +let got = false; +for (let attempt = 0; attempt < 60 && !got; attempt++) { + const c3 = mkRoom('00000'); + D.do_new_paiju(c3.desk, 0); + const p3 = c3.desk.method.curr_paiju(); + // 叫 70:闲家要捡满 70 分才算达标,庄家最易守;首家出最大牌以多赢墩 + mod.jiaofen(pk(p3.method.get_callgrade_seat(), { call: 70 })); + if (p3.step === 1) mod.jiaofen(pk(p3.method.get_callgrade_seat(), { call: 0 })); + if (p3.step === 1) mod.jiaofen(pk(p3.method.get_callgrade_seat(), { call: 0 })); + const b = p3.banker; + mod.xuanzhu(pk(b, { flower: 1 })); + mod.maipai(pk(b, { cards: P.get_seat_cards(p3, b).slice(-8) })); + let g = 0; + while (p3.step === 5 && ++g < 400) { + const s = p3.playproc.currseat, h = P.get_seat_cards(p3, s); + let p = null; + if (s === p3.playproc.start) p = [h[0]]; + else for (let i = h.length - 1; i >= 0; i--) if (A.can_followcard(p3.flower, h, [h[i]], p3.playproc.startcount, p3.playproc.startflower, p3.playproc.starttype).result) { p = [h[i]]; break; } + if (!p) break; + mod.chupai(pk(s, { cards: p })); + } + if (p3.result !== 0) continue; + got = true; + mod.zhunbei(pk(0)); mod.zhunbei(pk(1)); mod.zhunbei(pk(2)); + const nx = c3.desk.method.curr_paiju(); + t.eq('§4.6 庄赢(result=0) → 下一局起始叫分者仍是该庄家(连庄)', nx.firstseat, b); + // 连庄局的首家同样不能不叫 + c3.sent.length = 0; mod.jiaofen(pk(nx.firstseat, { call: 0 })); + t.eq('§4.2 连庄局的首家同样不能"不叫"', c3.sent.length === 1 && c3.sent[0].data.errcode === ERR.RULE, true); +} +t.eq('§4.6 连庄分支已覆盖到(构造出 result=0 的庄赢局)', got, true); + +process.exit(t.done('flow') ? 0 : 1);