牌桌右边一列开关里有这三颗钮。它们只决定这一盘哪些事不用问你、哪些事替你按,不改牌规。 下面先分别讲,再讲组合起来会怎样,最后有一个可以自己点的模拟器。
点「鸣牌」,旁边弹出七格小菜单,可以多选。再点一次「鸣牌」或点屏幕别处就收起。勾的项当场生效,桌上正好有一问的话会按新设置重新摆按钮。
| 选项 | 勾上之后 | 什么时候管 |
|---|---|---|
| 不吃 | 上家打出你能吃的牌,不摆「吃」 | 别人打牌时 |
| 不碰 | 别人打出你能碰的牌,不摆「碰」 | 别人打牌时 |
| 不明杠 | 别人打出你手里有三张的牌,不摆「杠」 | 别人打牌时 |
| 不暗杠 | 自己摸齐四张,不摆「杠」 | 自己摸牌后 |
| 不补杠 | 碰过的牌又摸到第四张,不摆「杠」 | 自己摸牌后 |
| 不补花 | 摸到花牌不自动补,改为摆一颗「补」钮由你自己点(默认是自动补花) | 摸到花时 |
| 不战鸣 | 你点了「过」之后别人叫了更低档、战鸣再问你一次时,不弹提示也不摆钮,直接替你「过」(连和一起过) | 战鸣再问时 |
鸣牌不管和,和不和由「牌设」管。(「不战鸣」例外:战鸣那一问整个替你过,里面有和也一起过。)
点「牌设」,弹出选牌浮窗。每点一下都当场生效,点浮窗外面或再点一次「牌设」收起。每次打开都落在「设置A」那一页。
| 选项 | 别人打出的牌 | 自己摸到能和 |
|---|---|---|
| 只询问选中牌 | 选中的才问,别的替你过 | 摸到选中的才问 |
| 不询问选中牌 | 选中的不问,别的照问 | 不管,照问 |
| 不自摸 | 不管 | 整页所有牌都不问(妙手回春、杠上开花照问) |
| 仅自摸 | 能和不问,吃碰杠照问;抢杠和照问 | 照问 |
开局默认是哪种模式,在「系统设置 → 牌设默认」里选;没选过就是「只询问选中牌」。
能用一页就用一页。想让某几张只自摸,在一页里「不询问」它们就行(例 4)。只有某张不许自摸、另一张又要自摸时,一页写不下 ——「不自摸」管整页 —— 才分两页。
只用 A 一页的话,「不自摸」会把 6万 的自摸也关掉,6万 又不在询问范围,点炮也不问。
点「代打」打开,再点一次关掉。打开之后:
点开代打时,旁边会弹出一个「自动和」勾选框,勾不勾随你,点屏幕别处就收起。勾上之后:
记住一句话就能推出所有组合:鸣牌和牌设先决定摆哪些按钮,代打再看摆出来的按钮行事。被藏掉的按钮,对代打来说就是不存在。
想「只等点炮」,这样搭配正好;自摸也不想错过,就别勾不自摸。
挂代打时一般别勾不补花,否则每摸到一张花都会停。
效果与「只询问 」一样:不够番的那张不会被替你错和。
左边设好,右边看桌上会摆哪些按钮。判断用的是牌桌上同一份代码。
多半是选了牌、又在「只询问选中牌」模式下,这时只有选中的牌会问你。打开牌设点「全部重置」,再点浮窗外面让它生效。
跟没设一样,什么都照常问。两页叠加是任何一页说问就问:别人打 3万,A 说问;打别的牌,B 说问;自摸也都有一页说问。两页正好互补,合起来就是全部都问,等于白设。
三颗钮都是「这一盘怎么打」的临时决定,每盘开局自动拨回。只有牌设开局用哪种模式,可以在「系统设置 → 牌设默认」里长期保存。
桌上摆着按钮在等你(吃、碰、杠、和、补),或者刚吃碰完要你自己打。看一眼按钮条就知道。
会。自动和只替你按和钮,不替你算番。国标要满 8 番才算和,不够就是错和,照规则罚分。知道哪张不够番,就在「牌设」里「不询问」它(或「只询问」够番的那几张),自动和就不会替你和那张,见组合 7。
本平台采用如下发牌算法。 发牌全程只由一样东西决定: 每盘一个 128 字节的随机种子。 同一个种子, 必然洗出同一副牌墙、掷出同一组骰子、发出同样的四手牌。
用线上发牌的同一份代码连洗 100 万副(共 1.44 亿张牌), 统计每一种可能的偏向。 另设一组对照: 洗牌步骤完全相同, 只把随机数换成操作系统自带的密码学随机数(公认的"真随机"标准)。 我们的结果与对照组处在同一水平, 就说明两者分辨不出差别。
怎么读: 每项算出一个偏离度 z。完全随机时 z 本来就会在 ±1 附近来回晃; |z| 超过 3 才值得怀疑, 超过 5 判定为有问题。z 贴着 0 不动反而可疑, 所以"不是越小越好"。
| 检验 | z | 结论 |
|---|---|---|
| 一、每种牌落在每个位置的次数 144 个位置 × 42 种牌 · 查某种牌是否总扎堆在牌墙某一段 |
−0.06 | 通过 |
| 对照组: 系统密码学随机数 同一项, 作参照 | −0.83 | 通过 |
| 二、四家起手的花色构成 查某个座位是否总拿到更多某种花色 |
−1.41 | 通过 |
| 三、随机取数本身 2 选 1 / 7 选 1 / 144 选 1, 各抽 200–288 万次 · 查取余数造成的偏差 |
−1.02 +1.34 +0.60 | 通过 |
| 四、三家闲家的起手向听 查某个座位的起手牌是否总是更好或更差 |
−0.98 | 通过 |
四家起手手牌里各类牌的占比(庄家 14 张, 其余 13 张):
| 座位 | 万 | 条 | 筒 | 字牌 | 花牌 |
|---|---|---|---|---|---|
| 东家(庄) | 24.98% | 25.01% | 25.00% | 19.45% | 5.56% |
| 南家 | 24.99% | 25.01% | 25.00% | 19.45% | 5.56% |
| 西家 | 25.01% | 25.00% | 24.99% | 19.44% | 5.56% |
| 北家 | 24.98% | 25.01% | 25.01% | 19.43% | 5.56% |
一副牌里万、条、筒各 36 张(各占 25%), 字牌 28 张(19.44%), 花牌 8 张(5.56%)。 四家与理论值、四家彼此之间, 都只差百分之零点零几。
起手向听数(离听牌还差几步, 越小牌越好; 0 至 6+ 各列为该向听数所占的百分比):
| 座位 | 平均 | 0 | 1 | 2 | 3 | 4 | 5 | 6+ |
|---|---|---|---|---|---|---|---|---|
| 东家(庄) | 3.485 | 0.0 | 1.3 | 12.5 | 36.9 | 35.8 | 12.3 | 1.1 |
| 南家 | 3.874 | 0.0 | 0.3 | 5.7 | 27.6 | 41.9 | 21.6 | 2.9 |
| 西家 | 3.875 | 0.0 | 0.3 | 5.7 | 27.5 | 41.9 | 21.6 | 2.9 |
| 北家 | 3.875 | 0.0 | 0.3 | 5.7 | 27.5 | 42.0 | 21.6 | 2.9 |
庄家多拿一张牌, 向听自然低一些, 这不是偏袒; 三家闲家的平均向听是 3.874、3.875、3.875, 几乎完全一样。
这一节证明了什么、没证明什么: 它证明的是洗牌算法本身不偏 —— 随机数经 SHAKE256(美国国家标准技术研究院的标准算法)展开, 取数时先用拒绝采样去掉会造成偏差的那一小段, 再取余数; 这套检验随代码一起保存, 发牌代码一改就重跑。 它证明不了"服务端有没有挑选种子" —— 算法再均匀, 挑种子照样能偏向某个人, 事后统计也看不出来。 回答这个问题要靠另一套机制(发牌前先公布种子的指纹、局后公开种子, 让每个人都能自己核对), 那是另一件事。 测试日期 2026-09-27。
private static List<int> BuildWall()
{
var wall = new List<int>(144);
foreach (var baseVal in new[] { 10, 20, 30 }) // 万 11-19, 条 21-29, 筒 31-39
for (int n = 1; n <= 9; n++)
for (int c = 0; c < 4; c++) wall.Add(baseVal + n);
for (int n = 1; n <= 7; n++) // 东南西北中发白 41-47
for (int c = 0; c < 4; c++) wall.Add(40 + n);
for (int n = 0; n < 8; n++) wall.Add(61 + n); // 春夏秋冬梅兰竹菊 61-68, 各 1 张
return wall; // 共 144 张
}
种子由系统的密码学随机数生成, 外部传不进来。
public static MahjongGame NewGame(int seatCount, int dealerSeat = 0)
=> NewGame(seatCount, SeedStream.NewSeed(), dealerSeat); // RandomNumberGenerator.GetBytes(128)
public static MahjongGame NewGame(int seatCount, byte[] seed, int dealerSeat = 0)
{
var dice = SeedStream.RollDice(seed); // 骰子也由种子定
var game = new MahjongGame(seatCount, seed) { Style = DealStyle.Real, Dice = dice };
game.Deal(seatCount, dealerSeat);
return game;
}
// 构造时:
_wall = BuildWall();
SeedStream.Shuffle(_wall, seed);
InitialWall = _wall.ToArray(); // 归档的就是这副洗好的牌
public sealed class SeedStream
{
public const int SeedBytes = 128;
private const int BlockBytes = 1024;
private readonly byte[] _seed, _domain;
private byte[] _buf = [];
private int _pos;
private long _chunk;
public SeedStream(byte[] seed, string domain = "")
{
_domain = domain.Length == 0 ? [] : Encoding.ASCII.GetBytes(domain);
_seed = seed;
}
public static byte[] NewSeed() => RandomNumberGenerator.GetBytes(SeedBytes);
public static string CommitmentOf(byte[] seed) => Convert.ToHexString(SHA256.HashData(seed));
// 每块 = SHAKE256(种子 ‖ 域标签 ‖ 大端 8 字节块号), 挤 1024 字节
private void Refill()
{
var input = new byte[_seed.Length + _domain.Length + 8];
_seed.CopyTo(input, 0);
_domain.CopyTo(input, _seed.Length);
BinaryPrimitives.WriteInt64BigEndian(input.AsSpan(_seed.Length + _domain.Length), _chunk++);
_buf = Shake256.HashData(input, BlockBytes);
_pos = 0;
}
private ulong NextU64()
{
if (_pos + 8 > _buf.Length) Refill();
ulong v = BinaryPrimitives.ReadUInt64BigEndian(_buf.AsSpan(_pos));
_pos += 8;
return v;
}
// [0, n) 均匀整数, 拒绝采样(不取模, 免得分布歪)
public int Next(int maxExclusive)
{
if (maxExclusive <= 1) return 0;
ulong bound = (ulong)maxExclusive;
ulong limit = ulong.MaxValue - ulong.MaxValue % bound;
while (true) { ulong v = NextU64(); if (v < limit) return (int)(v % bound); }
}
// 骰子走独立的域 "dice", 不和洗牌共用一条流
public static int[] RollDice(byte[] seed)
{
var rng = new SeedStream(seed, "dice");
return [rng.Next(6) + 1, rng.Next(6) + 1, rng.Next(6) + 1, rng.Next(6) + 1];
}
// Fisher-Yates
public static void Shuffle<T>(IList<T> list, byte[] seed)
{
var rng = new SeedStream(seed);
for (int i = list.Count - 1; i > 0; i--)
{
int j = rng.Next(i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
}
}
// s1 = 第一轮两颗之和(找风位), s2 = 第二轮两颗之和; 数墩, 一墩两张
public static int DealStartOf(int s1, int s2, int seatCount, int dealerSeat, int wallCount)
{
int per = wallCount / seatCount;
int k = ((s1 - 1) % seatCount + seatCount) % seatCount;
int seat = ((dealerSeat + k) % seatCount + seatCount) % seatCount;
return (seat * per + (s1 + s2) * 2) % wallCount;
}
// Deal(): 牌墙从断点转到下标 0, 然后从庄家起逆时针, 一次一墩四张
for (int round = 0; round < 3; round++)
for (int k = 0; k < seatCount; k++)
{
int s = (dealerSeat + k) % seatCount;
for (int c = 0; c < 4; c++) _hands[s].Add(_wall[_wallIndex++]);
}
for (int k = 0; k < seatCount; k++)
_hands[(dealerSeat + k) % seatCount].Add(_wall[_wallIndex++]);
Draw(DealerSeat); // 庄家第 14 张
这能证明的是: 牌谱里那副牌墙, 就是这个种子洗出来的, 事后没有被改动过。
你身上只有两个数: PT 决定段位, R 决定跟谁打。 段位是履历, 走得慢; R 是当前实力, 反应快。R 掉了不会掉段, 只是暂时进不了高档场。
每局按名次加减, 按国标 4-2-1-0 换算。攒够升段线升一段, 跌破 0 掉一段。
从 1500 起, 打得比"该打的"好就涨。管匹配、管高档场准入, 不参与升降段。连败有缓冲。
打 4 局, 按这 4 局的名次之和直接发一个起点级位。这期间照常算 R。
PT 下限是 0, 永远不会掉级。升级线就等于该级 1位 的分值 —— 拿一个第一, 升一级。
初段至三段每局略有补贴, 帮你成长; 四至六段同段同桌一桌 PT 之和为 0; 七段起每局略扣, 越往上越要靠实力。
查下面那张级位表取名次分值即可。唯一的变数是连胜: 第 2 把起, 1位 的分值翻倍(只翻这一次, 不随连胜数继续叠)。
一桌合计: 初段至三段为正(成长区), 四至六段为 0, 七段起为负。各段的具体分值见下面速查表。
同分并列: 并列的人平分所占名次的分值(PT 与 R 都是), 例如二、三位并列, 两人各拿二位与三位的平均。缺席者仍一律按 4 位。
同桌两人相差 2 段以上, 而高段的名次在低段之后: 高段转 PT 给这位低段 —— 高段是三段及以下转 1 PT(每局最多 3), 四段起转 2 PT(每局最多 6)。相差 1 段不转。
白话说: E 是按双方 R 算出的"你这局该拿多少", 打得比它好就涨、比它差就跌。 赢了比你强的人涨得多, 赢了比你弱的人涨得少; 输给比你弱的人扣得多。 连续拿一位没有额外奖励 —— 你的 R 越高, 同样拿一位涨得越少, R 会自己停在你真实水平附近。
| 谁 | 第 1 局 | 第 10 局 | 10 局后 R |
|---|---|---|---|
| 新号(前 20 局) | +24.0 | +13.3 | 1500 → 1680 |
| 老玩家(100~299 局) | +14.0 | +9.8 | 1500 → 1618 |
| 老玩家, 本身 R 1700 | +6.7 | +5.3 | 1700 → 1759 |
连败缓冲: 一局 ΔR 为负算一败, 连续的败按下表打折或加重; 只要有一局 ΔR 不为负就清零。前 20 局不启用。R 没有下限。
| 连续第几败 | 第 1 败 | 第 2 败 | 第 3 败 | 第 4 败起 |
|---|---|---|---|---|
| R 扣分乘以 | ×0.8 | ×1.1 | ×1.25 | ×1.3 |
| 级位 | 1位 | 2位 | 3位 | 4位 | 升级线 | 连胜 1位 |
|---|---|---|---|---|---|---|
| 9级 | +20 | +10 | 0 | 0 | 20 | +40 |
| 8级 | +20 | +10 | 0 | 0 | 20 | +40 |
| 7级 | +20 | +10 | 0 | −5 | 20 | +40 |
| 6级 | +25 | +12 | 0 | −8 | 25 | +50 |
| 5级 | +25 | +12 | 0 | −12 | 25 | +50 |
| 4级 | +25 | +12 | 0 | −16 | 25 | +50 |
| 3级 | +30 | +15 | 0 | −20 | 30 | +60 |
| 2级 | +30 | +15 | 0 | −25 | 30 | +60 |
| 1级 | +30 | +15 | 0 | −30 | 30 | +60 |
整条级位一共 225 PT。溢出的 PT 保留并立即再判一次, 所以连胜时一局可以跨 2 级 —— 理论最快 6 局连胜从 9级 打到初段。1级 升初段那一下溢出丢弃, 连胜清零。
| 段位 | 1位 | 2位 | 3位 | 4位 | 一桌合计 | 入段 | 升段线 | 连续几个1位 | PT 区间 |
|---|---|---|---|---|---|---|---|---|---|
| 初段 | +34 | +6 | −8 | −23 | +9 | 200 | 400 | 6 | |
| 二段 | +52 | +8 | −14 | −37 | +9 | 400 | 800 | 8 | |
| 三段 | +63 | +8 | −19 | −47 | +5 | 600 | 1200 | 10 | |
| 四段 | +69 | +8 | −23 | −54 | 0 | 800 | 1600 | 12 | |
| 五段 | +75 | +8 | −25 | −58 | 0 | 1000 | 2000 | 14 | |
| 六段 | +79 | +9 | −26 | −62 | 0 | 1200 | 2400 | 16 | |
| 七段 | +81 | +7 | −29 | −66 | −7 | 1400 | 2800 | 18 | |
| 八段 | +84 | +8 | −31 | −69 | −8 | 1600 | 3200 | 20 | |
| 九段 | +84 | +6 | −33 | −73 | −16 | 1800 | 3600 | 22 | |
| 十段 | +84 | +6 | −33 | −73 | −16 | 2000 | 不封顶 | — | |
| 天元 | 由九段、十段玩家参加的赛事产生。因为很稀缺, 暂时为永久称号。 | ||||||||
| 4 局名次之和 | 相当于平均名次 | 发到 |
|---|---|---|
| 4 ~ 8 | ≤ 2.00 | 6级 |
| 9 ~ 10 | ≤ 2.50 | 7级 |
| 11 ~ 12 | ≤ 3.00 | 8级 |
| 13 ~ 16 | > 3.00 | 9级 |
定级只是给个起点, 真正的校准由 R 承担(前 20 局 K=48, 涨得快)。 缺席局照样按 4位 计入定级 —— 否则可以靠缺席洗一个好起点。
| 已打局数 | K | M(单局上限) | 含义 |
|---|---|---|---|
| < 20 | 48 | ±45 | 新号快速找位置 |
| 20 ~ 49 | 40 | ±40 | 还在校准 |
| 50 ~ 99 | 32 | ±32 | 基本成型 |
| 100 ~ 299 | 28 | ±28 | 稳定 |
| ≥ 300 | 24 | ±24 | 老玩家, 分数迟钝 |
| 场次 | 段位 | R |
|---|---|---|
| 普通场 | 新人 ~ 三段 | < 1800 |
| 中级场 | 初段 ~ 十段 | < 1900 |
| 高级场 | 四段 以上 | ≥ 1800 |
| 职业场 | 七段 以上 | ≥ 2000 |
| 天元场 | 九段 以上 | ≥ 2000 |
中级场只看 R: 谁和谁同一个房间由实力决定, 不会因为升了一段就突然被分进强手池。 同一个房间里优先和段位相近的人同桌 —— 每升一段, 对手就强一点。