规则文档理清楚,开始代码层面对比和校对

This commit is contained in:
2026-07-04 09:52:49 +08:00
parent 911cc9cc23
commit 96372d713c
18 changed files with 4624 additions and 67 deletions
+5
View File
@@ -88,6 +88,11 @@ server/<游戏容器目录>/<游戏>/ ← 子游戏接入后**唯一
注意:`server/docs/development-guide/` 是 `docs/server/development-guide/` 的旧版、部分过期的副本(例如它仍写死可编辑目录为 `games2/`,而新版已改为「容器目录名不再框架强制」)。两者冲突时以 `docs/server/development-guide/` 为准。 注意:`server/docs/development-guide/` 是 `docs/server/development-guide/` 的旧版、部分过期的副本(例如它仍写死可编辑目录为 `games2/`,而新版已改为「容器目录名不再框架强制」)。两者冲突时以 `docs/server/development-guide/` 为准。
具体子游戏「二七王」(`server/games/erqiwang/`)另有两份权威文档,改动该子游戏前必读:
- `server/games/erqiwang/docs/design/design.md` —— **二七王玩法规则的唯一权威说明,必须严格遵守**。牌局构成、主牌顺序、叫分坐庄、出牌/跟牌/甩牌、捡分扣底、算子升级、算奖、房间选项等一切玩法规则以此为准;代码实现与该文档冲突时,属于代码缺陷,应改代码去符合规则(除非该规则项标注为「待确认」),**不得反过来改规则去迁就代码**。
- `server/games/erqiwang/docs/protocol/packet_protocol.md` —— 二七王前后端收发包协议与包数据定义,**必须完全符合代码实现(`server/games/erqiwang/*.js`)**。方向与 design.md 相反:协议文档是对既有代码行为的如实记录,两者不一致时以**代码为准**——应修改本协议文档去适配代码;代码新增/删除包或字段时,也要同步补全/删除本文档对应条目。
两套文档中反复强调的硬性规则(文档记载均对应过真实事故): 两套文档中反复强调的硬性规则(文档记载均对应过真实事故):
- 全面严格 ES5(`var`/`function`,继承用 `Object.create`;禁止 `let/const`/箭头函数/模板字符串/`class`/解构/`Promise`)。 - 全面严格 ES5(`var`/`function`,继承用 `Object.create`;禁止 `let/const`/箭头函数/模板字符串/`class`/解构/`Promise`)。
- 服务端 `require` 只能写在文件开头的守卫块内(`if (typeof require !== 'undefined') { var X = require(...); }`)——禁止在函数体内中途 `require`;两个运行时之后统一按同一个全局名引用。 - 服务端 `require` 只能写在文件开头的守卫块内(`if (typeof require !== 'undefined') { var X = require(...); }`)——禁止在函数体内中途 `require`;两个运行时之后统一按同一个全局名引用。
+2 -2
View File
@@ -911,7 +911,7 @@ Desk.send_text=function(_msg){//发送消息
if(!_msg.data.type){ if(!_msg.data.type){
var pobj = Desk.GetPlayerBySeat(_msg.data.seat); var pobj = Desk.GetPlayerBySeat(_msg.data.seat);
GameUI.playTextVoice(_msg.data.text,pobj.sex); GameUI.playTextVoice(_msg.data.text,pobj.sex);
GameUI.ShowChat(Logic.ChangeToStatus(C_Player.seat,_msg.data.seat),_msg.data.text); GameUI.ShowChat(_msg.data.seat,_msg.data.text);
GameUI.addCommunion(Logic.ChangeToStatus(C_Player.seat,_msg.data.seat),1,_msg.data.text,0,null,pobj.sex); GameUI.addCommunion(Logic.ChangeToStatus(C_Player.seat,_msg.data.seat),1,_msg.data.text,0,null,pobj.sex);
}else if(_msg.data.type == 1){//全服公告 }else if(_msg.data.type == 1){//全服公告
GameUI.CloseChat(); GameUI.CloseChat();
@@ -929,7 +929,7 @@ Desk.send_text=function(_msg){//发送消息
var idx = Number(_msg.data.text); var idx = Number(_msg.data.text);
_msg.data.text = Game_Config.Info.TextContent[(idx - 1)%Game_Config.Info.TextContent.length]; _msg.data.text = Game_Config.Info.TextContent[(idx - 1)%Game_Config.Info.TextContent.length];
GameUI.playTextVoice(_msg.data.text); GameUI.playTextVoice(_msg.data.text);
GameUI.ShowChat(Logic.ChangeToStatus(C_Player.seat,_msg.data.seat),_msg.data.text); GameUI.ShowChat(_msg.data.seat,_msg.data.text);
GameUI.addCommunion(Logic.ChangeToStatus(C_Player.seat,_msg.data.seat),1,_msg.data.text,0); GameUI.addCommunion(Logic.ChangeToStatus(C_Player.seat,_msg.data.seat),1,_msg.data.text,0);
} }
} }
+16 -1
View File
@@ -5779,7 +5779,12 @@ GameUI.ShowEmotion = function(v, ind) {
} }
GameUI.ShowChat = function(v, text) { GameUI.ShowChat = function(seat, text) {
if (Game_Modify.ShowChat) {
Game_Modify.ShowChat(seat, text);
return;
}
var v = Logic.ChangeToStatus(C_Player.seat,seat);
text = String(text); text = String(text);
set_self(204, 7, "", 0, 0); set_self(204, 7, "", 0, 0);
if (v == 0) { if (v == 0) {
@@ -6085,6 +6090,7 @@ GameUI.play_voice = function(_msg, type, v) {
function gameui_play_voice(v) { function gameui_play_voice(v) {
v = Number(v); v = Number(v);
var bTemp = false; var bTemp = false;
if (v >= ConstVal.Com_History.content_tag) { if (v >= ConstVal.Com_History.content_tag) {
bTemp = true; bTemp = true;
@@ -6092,6 +6098,10 @@ function gameui_play_voice(v) {
if (bTemp) { if (bTemp) {
play_ani(1, ConstVal.Com_History.f_spid + "add" + v, 43, 1, 3, 0, 600, 0, 0, 0, 0, 0, 0); play_ani(1, ConstVal.Com_History.f_spid + "add" + v, 43, 1, 3, 0, 600, 0, 0, 0, 0, 0, 0);
} else { } else {
if(Game_Modify.gameui_play_voice){
Game_Modify.gameui_play_voice(v);
return;
}
var x = Game_Config.Voice.VoiceLoc[v][0]; var x = Game_Config.Voice.VoiceLoc[v][0];
var y = Game_Config.Voice.VoiceLoc[v][1]; var y = Game_Config.Voice.VoiceLoc[v][1];
var dis_x = Game_Config.Voice.VoiceDis[0]; var dis_x = Game_Config.Voice.VoiceDis[0];
@@ -6128,10 +6138,15 @@ function gameui_stop_voice(v) {
if (v >= ConstVal.Com_History.content_tag) { if (v >= ConstVal.Com_History.content_tag) {
bTemp = true; bTemp = true;
} }
if (bTemp) { if (bTemp) {
play_ani(0, ConstVal.Com_History.f_spid + "add" + v, 43, 1, 3, 0, 600, 0, 0, 0, 0, 0, 0); play_ani(0, ConstVal.Com_History.f_spid + "add" + v, 43, 1, 3, 0, 600, 0, 0, 0, 0, 0, 0);
set_self(ConstVal.Com_History.f_spid + "add" + v, 43, 3, 0, 0); set_self(ConstVal.Com_History.f_spid + "add" + v, 43, 3, 0, 0);
} else { } else {
if(Game_Modify.gameui_stop_voice){
Game_Modify.gameui_stop_voice(v);
return;
}
set_self(612 + v, 37, 0, 0, 0); set_self(612 + v, 37, 0, 0, 0);
set_self(642 + v, 37, 0, 0, 0); set_self(642 + v, 37, 0, 0, 0);
play_ani(0, 642 + v, 43); play_ani(0, 642 + v, 43);
+46
View File
@@ -46,6 +46,52 @@ Game_Modify.CreateRoomData = {
Type_2: 0 Type_2: 0
}; };
// ---------- 玩家信息/头像默认布局(供 02 转发壳 updatePlayerInfoUI 的默认实现使用)----------
// 02_SubGame_Input.js 的 updatePlayerInfoUI 在【无 SubGameHooks 实现】时提供默认头像等 UI 渲染;
// 转发壳不得引用 codes,故把默认实现所需布局放在此处(Game_Modify 下),与子游戏 codes 内
// GameView.updatePlayerInfoUI 使用的布局逻辑保持一致。子游戏若自定义头像 UI,则用 hook 覆盖、此配置不生效。
// POSITIONS[configIndex]:ind0=南(自己)/ind1=东/ind2=北/ind3=西;渲染精灵 346+ind 底框、376+ind 头像框、
// 406+ind 昵称、436+ind 分数、116+ind 头像图、群组 43+ind。坐标基于设计分辨率 1280x720。
Game_Modify.PLAYER_INFO_LAYOUT = {
POSITIONS: {
0: { FRAME_X: 6, FRAME_Y: 592, FRAME_WIDTH: 90, FRAME_HEIGHT: 115,
AVATAR: { OFFSET_X: 5, OFFSET_Y: 5, WIDTH: 80, HEIGHT: 80,
NAME_OFFSET_X: -400, NAME_OFFSET_Y: 888, SCORE_OFFSET_X: 0, SCORE_OFFSET_Y: 88 },
NAME_CENTER_ALIGN: true },
1: { FRAME_X: 1186, FRAME_Y: 85, FRAME_WIDTH: 90, FRAME_HEIGHT: 115,
AVATAR: { OFFSET_X: 5, OFFSET_Y: 5, WIDTH: 80, HEIGHT: 80,
NAME_OFFSET_X: 905, NAME_OFFSET_Y: -888, SCORE_OFFSET_X: 0, SCORE_OFFSET_Y: 88 },
NAME_CENTER_ALIGN: true },
2: { FRAME_X: 1006, FRAME_Y: 5, FRAME_WIDTH: 90, FRAME_HEIGHT: 115,
AVATAR: { OFFSET_X: 5, OFFSET_Y: 5, WIDTH: 80, HEIGHT: 80,
NAME_OFFSET_X: 905, NAME_OFFSET_Y: -888, SCORE_OFFSET_X: 0, SCORE_OFFSET_Y: 88 },
NAME_CENTER_ALIGN: true },
3: { FRAME_X: 7, FRAME_Y: 59, FRAME_WIDTH: 90, FRAME_HEIGHT: 115,
AVATAR: { OFFSET_X: 5, OFFSET_Y: 5, WIDTH: 80, HEIGHT: 80,
NAME_OFFSET_X: -605, NAME_OFFSET_Y: -888, SCORE_OFFSET_X: 0, SCORE_OFFSET_Y: 88 },
NAME_CENTER_ALIGN: true }
},
TEXT_STYLE: { NAME_FONT_SIZE: 14, NAME_MAX_LENGTH: 6, SCORE_FONT_SIZE: 12, CHAR_WIDTH: 9 }
};
// ---------- 桌面聊天/语音气泡默认布局(供 02 转发壳 ShowChat/gameui_play_voice/gameui_stop_voice 使用)----------
// 平台默认按固定显示位序取 Game_Config.Chat/Voice 的固定坐标(固定人数模式);此处提供可变人数版:
// 按 updatePlayerInfoUI 的 configIndex(物理位) 定位,使气泡随人数(2/3/4)贴合各玩家。
// 仅定义每个物理位(0南/1东/2北/3西)的气泡框左上角锚点 X/Y 与朝向 IS_LEFT;气泡内边距/字宽/显示时长等
// 尺寸复用平台 Game_Config.Chat/Voice/Max(与人数无关)。精灵:聊天 背景552+ind/文字582+ind,
// 语音 背景612+ind/内容642+ind。IS_LEFT=true 气泡向右展开(用 IMG_LEFT)、false 向左展开(用 IMG_RIGHT)。
// 坐标基于 1280x720,锚点参考 PLAYER_INFO_LAYOUT 各位底框,可按实际界面微调。
Game_Modify.BUBBLE_LAYOUT = {
IMG_LEFT: 83, // 左向气泡背景图资源ID(气泡向右展开)
IMG_RIGHT: 86, // 右向气泡背景图资源ID(气泡向左展开)
POSITIONS: {
0: { X: 100, Y: 540, IS_LEFT: true }, // 南(自己)
1: { X: 1176, Y: 120, IS_LEFT: false }, // 东
2: { X: 970, Y: 20, IS_LEFT: false }, // 北
3: { X: 120, Y: 70, IS_LEFT: true } // 西
}
};
// ---------- 接口转发壳(A 类:无 hook 即 no-op) ---------- // ---------- 接口转发壳(A 类:无 hook 即 no-op) ----------
Game_Modify.utlmousedown = function (gameid, spid, downx, downy, no1, no2, no3, no4, no5, no6) { Game_Modify.utlmousedown = function (gameid, spid, downx, downy, no1, no2, no3, no4, no5, no6) {
+204 -2
View File
@@ -293,8 +293,210 @@ Game_Modify.getMaxPlayerCount = function (roomtype) {
return (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4; return (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
}; };
// B 类:更新玩家信息 UI,无 hook 返回 false(让平台默认处理) // 内部辅助(非平台接口):把相对显示位序 relPos 映射为 { ind:精灵槽, configIndex:物理布局位 }。
// 2/3 人时 ind≠configIndex;无效返回 null。updatePlayerInfoUI 与桌面气泡默认实现共用此映射,
// 保证头像面板与聊天/语音气泡落在同一玩家位。
Game_Modify._resolveDisplaySlot = function (relPos, playerCount) {
var ind = -1, configIndex = -1;
if (playerCount === 2) {
if (relPos === 0) { ind = 0; configIndex = 0; }
else if (relPos === 1) { ind = 1; configIndex = 2; }
} else if (playerCount === 3) {
if (relPos === 0) { ind = 0; configIndex = 0; }
else if (relPos === 1) { ind = 1; configIndex = 1; }
else if (relPos === 2) { ind = 2; configIndex = 3; }
} else {
if (relPos === 0) { ind = 0; configIndex = 0; }
else if (relPos === 1) { ind = 1; configIndex = 1; }
else if (relPos === 2) { ind = 2; configIndex = 2; }
else if (relPos === 3) { ind = 3; configIndex = 3; }
}
if (ind === -1) return null;
return { ind: ind, configIndex: configIndex };
};
// B 类:更新指定座位的玩家信息 UI(昵称/分数/头像)。
// 有 hook 则委托子游戏;无 hook 时由本模板提供默认头像等 UI 的显示逻辑,其逻辑与子游戏
// codes 内 GameView.updatePlayerInfoUI 保持一致,布局取自 Game_Modify.PLAYER_INFO_LAYOUT(见 01)。
// 精灵操作一律经框架 SpriteManager(setPosition/setSize/setText/show|hideGroup),不直接调
// set_self/set_group 等引擎原语;头像加载走平台 Func.up_imgurl(非精灵原语,SpriteManager 无对应能力)。
// 不引用任何 codes 模块。平台 GameUI.updatePlayerInfoUI 会对每个座位逐一调用本接口,故按单座位渲染。
Game_Modify.updatePlayerInfoUI = function (targetSeat) { Game_Modify.updatePlayerInfoUI = function (targetSeat) {
if (window.SubGameHooks && SubGameHooks.updatePlayerInfoUI) return SubGameHooks.updatePlayerInfoUI(targetSeat); if (window.SubGameHooks && SubGameHooks.updatePlayerInfoUI) return SubGameHooks.updatePlayerInfoUI(targetSeat);
return false;
// ---- 模板默认:与 GameView.updatePlayerInfoUI 一致的单座位渲染 ----
if (typeof Desk === 'undefined' || typeof C_Player === 'undefined' || typeof SpriteManager === 'undefined') return false;
var layout = Game_Modify.PLAYER_INFO_LAYOUT;
if (!layout || !layout.POSITIONS) return false;
if (!Desk.PlayerList || targetSeat < 0 || targetSeat >= Desk.PlayerList.length) return false;
var selfSeat = (typeof C_Player.seat === 'number') ? C_Player.seat : 0;
var playerCount = (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
var relPos = (targetSeat - selfSeat + playerCount) % playerCount;
// 显示槽 ind 与布局位 configIndex(2/3 人时两者不同)
var slot = Game_Modify._resolveDisplaySlot(relPos, playerCount);
if (!slot) return false;
var ind = slot.ind, configIndex = slot.configIndex;
var player = Desk.PlayerList[targetSeat];
// 空座位:隐藏该位玩家信息群组
if (!player || !player.playerid || player.playerid === -1) {
SpriteManager.hideGroup(43 + ind);
return true;
}
var posConfig = layout.POSITIONS[configIndex];
if (!posConfig) return false;
// 底框(346+ind)
if (posConfig.FRAME_X !== undefined) {
SpriteManager.setPosition(346 + ind, posConfig.FRAME_X, posConfig.FRAME_Y);
SpriteManager.setSize(346 + ind, posConfig.FRAME_WIDTH, posConfig.FRAME_HEIGHT);
}
// 头像框(376+ind):基于底框 + 相对偏移
var avatarX = posConfig.FRAME_X + posConfig.AVATAR.OFFSET_X;
var avatarY = posConfig.FRAME_Y + posConfig.AVATAR.OFFSET_Y;
var avatarWidth = posConfig.AVATAR.WIDTH;
var avatarHeight = posConfig.AVATAR.HEIGHT;
var nameOffsetX = posConfig.AVATAR.NAME_OFFSET_X || 0;
var nameOffsetY = posConfig.AVATAR.NAME_OFFSET_Y || 0;
var scoreOffsetX = posConfig.AVATAR.SCORE_OFFSET_X || 0;
var scoreOffsetY = posConfig.AVATAR.SCORE_OFFSET_Y || 0;
SpriteManager.setPosition(376 + ind, avatarX, avatarY);
SpriteManager.setSize(376 + ind, avatarWidth, avatarHeight);
var textStyle = layout.TEXT_STYLE || {};
var maxLen = textStyle.NAME_MAX_LENGTH || 8;
var charWidth = textStyle.CHAR_WIDTH || 7;
// 昵称(406+ind)
var nickname = Func.subString(player.nickname || '', maxLen, true);
SpriteManager.setText(406 + ind, nickname);
var nameX = avatarX + nameOffsetX;
if (posConfig.NAME_CENTER_ALIGN) {
var textWidth = (typeof nickname.gblen === 'function') ? nickname.gblen() * charWidth : nickname.length * charWidth;
nameX = avatarX + avatarWidth / 2 - textWidth / 2;
}
SpriteManager.setPosition(406 + ind, nameX, avatarY + nameOffsetY);
// 分数(436+ind)
var score = (typeof player.score === 'number') ? player.score : 0;
SpriteManager.setText(436 + ind, score);
var scoreX = avatarX + scoreOffsetX;
if (posConfig.NAME_CENTER_ALIGN) {
var scoreWidth = String(score).length * charWidth;
scoreX = avatarX + avatarWidth / 2 - scoreWidth / 2;
}
SpriteManager.setPosition(436 + ind, scoreX, avatarY + scoreOffsetY);
// 头像图(116+ind):Func.up_imgurl 为平台头像加载接口(非引擎精灵原语)
if (player.avatar && typeof Func !== 'undefined' && typeof Func.up_imgurl === 'function') {
Func.up_imgurl(116 + ind, player.avatar);
}
// 显示该座位玩家信息群组
SpriteManager.showGroup(43 + ind);
return true;
}; };
// 桌面文字聊天气泡:有 hook 委托子游戏;无 hook 时按【可变人数】默认渲染,使气泡贴合各玩家。
// seat 为绝对座位;经 SpriteManager 操作精灵(背景552+ind/文字582+ind),位置取自 Game_Modify.BUBBLE_LAYOUT
// 的物理位配置,尺寸(内边距/字宽/显示时长)复用 Game_Config.Chat/Max;不直接调引擎原语、不引用 codes。
Game_Modify.ShowChat = function (seat, text) {
if (window.SubGameHooks && SubGameHooks.ShowChat) return SubGameHooks.ShowChat(seat, text);
GameUI.CloseChat();
if (typeof Desk === 'undefined' || typeof C_Player === 'undefined' || typeof SpriteManager === 'undefined') return;
var layout = Game_Modify.BUBBLE_LAYOUT;
if (!layout || !layout.POSITIONS) return;
var playerCount = (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
var relPos = (seat - C_Player.seat + playerCount) % playerCount;
var slot = Game_Modify._resolveDisplaySlot(relPos, playerCount);
if (!slot) return;
var pos = layout.POSITIONS[slot.configIndex];
if (!pos) return;
var ind = slot.ind;
text = String(text);
var chat = Game_Config.Chat || {};
var dis_x = (chat.ChatDis && chat.ChatDis[0]) || 0;
var dis_y = (chat.ChatDis && chat.ChatDis[1]) || 0;
var text_w = chat.textwidth || 14;
var text_len = (typeof text.gblen === 'function') ? text.gblen() : text.length;
var bgW = text_len * text_w + dis_x * 2;
SpriteManager.setText(582 + ind, text);
if (pos.IS_LEFT) {
SpriteManager.setPosition(552 + ind, pos.X, pos.Y);
SpriteManager.setPosition(582 + ind, pos.X + dis_x, pos.Y + dis_y);
SpriteManager.setImage(552 + ind, layout.IMG_LEFT);
} else {
SpriteManager.setPosition(552 + ind, pos.X - bgW, pos.Y);
SpriteManager.setPosition(582 + ind, pos.X - text_len * text_w - dis_x, pos.Y + dis_y);
SpriteManager.setImage(552 + ind, layout.IMG_RIGHT);
}
SpriteManager.setWidth(552 + ind, bgW);
SpriteManager.show(552 + ind);
SpriteManager.show(582 + ind);
// 自动隐藏:复用平台配置的显示时长(观感与固定人数版一致),按槽位去重定时器
var dur = (Game_Config.Max && Game_Config.Max.ShowChat) || 3000;
Game_Modify._chatHideTimers = Game_Modify._chatHideTimers || {};
if (Game_Modify._chatHideTimers[ind]) { clearTimeout(Game_Modify._chatHideTimers[ind]); }
Game_Modify._chatHideTimers[ind] = setTimeout(function () {
SpriteManager.hide(552 + ind);
SpriteManager.hide(582 + ind);
Game_Modify._chatHideTimers[ind] = null;
}, dur);
};
// 桌面语音气泡(播放):有 hook 委托子游戏;无 hook 时按【可变人数】默认渲染。
// uiIndex 已是相对显示位序(relPos);经 SpriteManager 操作精灵(背景612+ind/内容642+ind),
// 位置取自 Game_Modify.BUBBLE_LAYOUT,气泡宽度依据内容精灵当前宽度;尺寸复用 Game_Config.Voice。
Game_Modify.gameui_play_voice = function (uiIndex) {
if (window.SubGameHooks && SubGameHooks.gameui_play_voice) return SubGameHooks.gameui_play_voice(uiIndex);
if (typeof SpriteManager === 'undefined') return;
var layout = Game_Modify.BUBBLE_LAYOUT;
if (!layout || !layout.POSITIONS) return;
var playerCount = (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
var relPos = ((Number(uiIndex) % playerCount) + playerCount) % playerCount;
var slot = Game_Modify._resolveDisplaySlot(relPos, playerCount);
if (!slot) return;
var pos = layout.POSITIONS[slot.configIndex];
if (!pos) return;
var ind = slot.ind;
var voice = Game_Config.Voice || {};
var dis_x = (voice.VoiceDis && voice.VoiceDis[0]) || 0;
var dis_y = (voice.VoiceDis && voice.VoiceDis[1]) || 0;
var contentSize = SpriteManager.getSize(642 + ind);
var text_len = (contentSize && typeof contentSize.width === 'number') ? contentSize.width : 0;
var bgW = text_len + dis_x * 2;
if (pos.IS_LEFT) {
SpriteManager.setPosition(612 + ind, pos.X, pos.Y);
SpriteManager.setPosition(642 + ind, pos.X + dis_x, pos.Y + dis_y);
SpriteManager.setImage(612 + ind, layout.IMG_LEFT);
} else {
SpriteManager.setPosition(612 + ind, pos.X - bgW, pos.Y);
SpriteManager.setPosition(642 + ind, pos.X - text_len - dis_x, pos.Y + dis_y);
SpriteManager.setImage(612 + ind, layout.IMG_RIGHT);
}
SpriteManager.setWidth(612 + ind, bgW);
SpriteManager.show(612 + ind);
SpriteManager.show(642 + ind);
play_ani(1, 642 + ind, 43, 1, 3, 0, Game_Config.Voice.VoiceTime, 0, 0, 0, 0, 0, 0);
};
// 桌面语音气泡(停止):有 hook 委托子游戏;无 hook 时隐藏对应槽位的语音气泡。
Game_Modify.gameui_stop_voice = function (uiIndex) {
if (window.SubGameHooks && SubGameHooks.gameui_stop_voice) return SubGameHooks.gameui_stop_voice(uiIndex);
if (typeof SpriteManager === 'undefined') return;
var playerCount = (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
var relPos = ((Number(uiIndex) % playerCount) + playerCount) % playerCount;
var slot = Game_Modify._resolveDisplaySlot(relPos, playerCount);
if (!slot) return;
var ind = slot.ind;
SpriteManager.hide(612 + ind);
SpriteManager.hide(642 + ind);
};
@@ -1,36 +1,38 @@
// ============================================================================ // ============================================================================
// 00_SubGame_Config.template.js —— 平台配置项模板(结构由平台定义,子游戏填【值】) // 游戏配置 (Game Configuration)
// 这是配置文件(C 类,非转发壳)。复制后按本游戏界面/玩法调整各项值。
// 严格 ES5。
// ============================================================================ // ============================================================================
var Game_Config = Game_Config || {}; var Game_Config = Game_Config||{};//相关配置
Game_Config.Debugger={//调试配置 Game_Config.Debugger={//调试配置
isDebugger : true,// debugger模式下会将所有收发的包输出到控制台(正式发布改为false) isDebugger : true,// debugger模式下会将所有收发的包输出到控制台(正式发布改为false)
AutoLogin : true,//debugger模式下是否需要记住登录状态自动登录(正式发布改为true) AutoLogin : true,//debugger模式下是否需要记住登录状态自动登录(正式发布改为true)
isSubmitError : false,//是否需要服务器收集错误信息调试时可根据需要(正式发布改为true) isSubmitError : false,//是否需要服务器收集错误信息调试时可根据需要(正式发布改为true)
visitorLogin : true,//隐藏式游客登录 visitorLogin : true,//隐藏式游客登录
visiblePay:true,//审核通过后是否显示支付按钮 visiblePay:true,//审核通过后是否显示支付按钮
serverType:1,//0->正式服务器 1->本地服务器 http://ylyxservice1.0791ts.cn/config/update_json.txt serverType:1,//0->正式服务器 1->本地服务器 http://ylyxservice1.0791ts.cn/config/update_json.txt
gameserver:""//子游戏填自己的更新地址 gameserver:"https://tsgames.daoqi88.cn/config_test/beta_update_jsonv2.txt"+"?"+ifast_random(100000)
}; // gameserver:"https://tsgames.daoqi88.cn/config_test/test_update_jsonv2.txt"+"?"+ifast_random(100000)
//gameserver:"https://gameotherwork.ld2sw.cn/update_json/update_json_haiwai.txt"+"?"+ifast_random(100000)
// gameserver:"https://tsgames.daoqi88.cn/config/update_jsonv2.txt"+"?"+ifast_random(100000)
};
Game_Config.Max = { Game_Config.Max = {
SumOfRoomtype:3,//创建时房间类型roomtype数组长度 SumOfRoomtype:3,//创建时房间类型roomtype数组长度
ShowChat:2000,//聊天停留时间 ShowChat:2000,//聊天停留时间
PlayerCnt:4,//房间最大人数(按房间最大人数设置) PlayerCnt:4,//房间最大人数
group:500,//游戏最大群组号 group:500,//游戏最大群组号
showtime:10,//加载等待最少时间 showtime:10,//加载等待最少时间
reconnecttime:30000,//唤醒游戏间隔(超过间隔断线重连) reconnecttime:30000,//唤醒游戏间隔(超过间隔断线重连)
Mainnickname:10,//主界面玩家昵称显示长度(字符长度) Mainnickname:10,//主界面玩家昵称显示长度(字符长度)
Infonickname:16//个人信息显示长度(字符长度) Infonickname:16//个人信息显示长度(字符长度)
}; };
Game_Config.Combat = { Game_Config.Combat = {
height:80,//战绩页单行高度高度 height:80,//战绩页单行高度高度
up_y:190,//战绩页裁剪y坐标 up_y:190,//战绩页裁剪y坐标
bannerheight:60,//战绩横幅高度 bannerheight:60,//战绩横幅高度
bannery:80,//战绩横幅Y左边(相对48精灵) bannery:80,//战绩横幅Y左边(相对48精灵)
btnbg:50 btnbg:50
@@ -42,46 +44,46 @@ Game_Config.Info = {
otherPositionDefault:true,//其他主界面玩家信息(昵称、积分)是否采用默认对齐方式对齐居中点为头像中点 otherPositionDefault:true,//其他主界面玩家信息(昵称、积分)是否采用默认对齐方式对齐居中点为头像中点
myPositionDefault:true,//其他主界面玩家信息(昵称、积分)是否采用默认对齐方式对齐居中点为头像中点 myPositionDefault:true,//其他主界面玩家信息(昵称、积分)是否采用默认对齐方式对齐居中点为头像中点
myPosition:130,//自己信息对齐点(根据游戏界面自行修改) myPosition:130,//自己信息对齐点(根据游戏界面自行修改)
position:[],//其他主界面玩家信息(昵称、积分)居中对齐的x坐标 注意是其他玩家不包括自己从下家开始 position:[],//其他主界面玩家信息(昵称、积分)居中对齐的x坐标 注意是其他玩家不包括自己从下家开始
//TextContent:["1","2","3","4","5","6","7"],//常用语内容 //TextContent:["1","2","3","4","5","6","7"],//常用语内容
TextContent:["你好","谢谢","再来","快点","不要","加油",":)"],//常用语内容 TextContent:["你好","2","3","4","5","6","7"],//常用语内容
TextContentMp3:["","","","","","",""],//常用语对应音效 TextContentMp3:["","","","","","",""],//常用语对应音效
}; };
Game_Config.Share={//分享参数 Game_Config.Share={//分享参数
appdownload:"",//下载链接(无需配置,从服务器获取) appdownload:"",//下载链接(无需配置,从服务器获取)
title:"游戏名",//(分享标题) title:"Test",//(分享标题)
description:"",//分享描述 description:"hello world",//分享描述
gameTitle:"",//游戏中的分享标题模板工程会自动将游戏名字分享出去、不必写在这个变量里 gameTitle:"",//游戏中的分享标题模板工程会自动将游戏名字分享出去、不必写在这个变量里
gameDescription:""//游戏中分享描述 gameDescription:""//游戏中分享描述
}; };
Game_Config.Chat={//游戏内聊天配置信息 Game_Config.Chat={//游戏内聊天配置信息
LimitLength:40,//聊天最大长度(字节长度) LimitLength:40,//聊天最大长度(字节长度)
textwidth:12.5,//聊天显示文字的宽度(未改动聊天显示文字大小无需改动无需修改) textwidth:12.5,//聊天显示文字的宽度(未改动聊天显示文字大小无需改动无需修改)
ChatDis:[30,12],//聊天气泡与内容的间隔0位置左右间隔(最好不要低于30)1位置上下间隔 ChatDis:[30,12],//聊天气泡与内容的间隔0位置左右间隔(最好不要低于30)1位置上下间隔
isLeft:[1,0,0,1,1],//聊天气泡是否为以左边为基准线(坐标/基准按各游戏界面调整) isLeft:[1,0,0,1,1],//聊天气泡是否为以左边为基准线
ChatLoc:[[35,517],[1100,315],[1100,105],[175,105],[175,315]]//聊天气泡的基准点位置(注意是基准点位置!)(坐标/基准按各游戏界面调整) ChatLoc:[[35,517],[1100,315],[1100,105],[175,105],[175,315]]//聊天气泡的基准点位置(注意是基准点位置!)
}; };
Game_Config.Voice={//游戏内聊天配置信息 Game_Config.Voice={//游戏内聊天配置信息
VoiceTime:600,//播放语音动画的时间一般情况无需无需修改 VoiceTime:600,//播放语音动画的时间一般情况无需无需修改
VoiceDis:[30,12],//语音气泡与内容的间隔0位置左右间隔(最好不要低于30)1位置上下间隔 VoiceDis:[30,12],//语音气泡与内容的间隔0位置左右间隔(最好不要低于30)1位置上下间隔
isLeft:[1,0,0,1,1],//气泡是否为以左边为基准线(坐标/基准按各游戏界面调整) isLeft:[1,0,0,1,1],//气泡是否为以左边为基准线
VoiceLoc:[[35,517],[1100,315],[1100,105],[175,105],[175,315]]//气泡的基准点位置(注意是基准点位置!)(坐标/基准按各游戏界面调整) VoiceLoc:[[35,517],[1100,315],[1100,105],[175,105],[175,315]]//气泡的基准点位置(注意是基准点位置!)
}; };
Game_Config.Setting={ Game_Config.Setting={
Ads1:"本游戏仅供娱乐休闲使用,\n道具及虚拟货币不可兑换\n现金或实物,不代表任何\n真实财富价值。", Ads1:"本游戏仅为娱乐休\n闲使用,道具所有\n 游戏通用\n\n游戏问题请联系微\n 信公众号:\n",
//Ads2:"\n 或客服号:\n", //Ads2:"\n 或客服号:\n",
board:"",//从后台获取设置无效 board:"",//从后台获取设置无效
//info:[" 客服信息","QQ:","",""],//客服QQ提醒 //info:[" 客服信息","QQ:","",""],//客服QQ提醒
info:[" 客服信息","QQ:","手机:","微信:"],//客服QQ提醒 info:[" 客服信息","QQ:","手机:","微信:"],//客服QQ提醒
board_blength:24,//通知页面一行最大字节数(无需设置) board_blength:24,//通知页面一行最大字节数(无需设置)
charge:"充值提示",//充值提示 charge:"关注友乐微信公众号充值",//充值提示
}; };
Game_Config.Protocol={ Game_Config.Protocol={
x:270,//协议图片初始x坐标 x:270,//协议图片初始x坐标
y:70,//协议图片初始y坐标 y:70,//协议图片初始y坐标
h:560,//协议图片显示高度 h:560,//协议图片显示高度
w:729//协议图片显示宽度 w:729//协议图片显示宽度
}; };
Game_Config.Help={//帮助 Game_Config.Help={//帮助
x:300,//帮助图片初始x坐标 x:300,//帮助图片初始x坐标
@@ -89,27 +91,27 @@ Game_Config.Help={//帮助
w:729,//帮助图片显示高度 w:729,//帮助图片显示高度
h:450//帮助图片显示宽度 h:450//帮助图片显示宽度
}; };
Game_Config.Notice={//滚动公告 Game_Config.Notice={//滚动公告
x1:0,//无需设置 x1:0,//无需设置
x2:0,//无需设置 x2:0,//无需设置
y:0,//无需设置 y:0,//无需设置
h:0,//无需设置 h:0,//无需设置
//以上参数用来截取滚动公告的显示,截取位置是与滚动公告底大小位置一致 //以上参数用来截取滚动公告的显示,截取位置是与滚动公告底大小位置一致
speed:0.1,//滚动公告的滚动速度 speed:0.1,//滚动公告的滚动速度
width:15//滚动公告的内容字体的字节长度(汉字是两个字节) width:15//滚动公告的内容字体的字节长度(汉字是两个字节)
}; };
Game_Config.Feedback = {//反馈配置 Game_Config.Feedback = {//反馈配置
maxLen:250 //反馈内容最大长度 maxLen:250 //反馈内容最大长度
}; };
Game_Config.shakeList ={//摇一摇事件的回调ID、需要添加时在此处添加回调事件写在Utl_Input.js中的Utl.shakeEvent()中 Game_Config.shakeList ={//摇一摇事件的回调ID、需要添加时在此处添加回调事件写在Utl_Input.js中的Utl.shakeEvent()中
nil:0, nil:0,
startwar:1 startwar:1
}; };
Game_Config.soundList ={//声音资源名 Game_Config.soundList ={//声音资源名
MenuSceneMusic:"",//大厅界面背景音 MenuSceneMusic:"",//大厅界面背景音
MainSceneMusic:""//游戏主界面背景音 MainSceneMusic:""//游戏主界面背景音
}; };
Game_Config.ClickButton = {//需要设置点击音效的按钮(只有有弹窗的按钮设置此音效有效、按钮已经设置好、子游戏不允许修改) Game_Config.ClickButton = {//需要设置点击音效的按钮(只有有弹窗的按钮设置此音效有效、按钮已经设置好、子游戏不允许修改)
src_1:"",//点击时播放的声音资源文件( 不需要播放则不填,下同) src_1:"",//点击时播放的声音资源文件( 不需要播放则不填,下同)
src_2:""//弹窗时播放的音效 src_2:""//弹窗时播放的音效
} }
@@ -120,11 +122,28 @@ Game_Config.loginButton = {//登录按钮信息
x3:488,//审核版本游客登录按钮x x3:488,//审核版本游客登录按钮x
}; };
Game_Config.sysConfig = { Game_Config.sysConfig = {
mainMenuButton:false,//是否进入主菜单界面隐藏四个按钮 mainMenuButton:false,//是否进入主菜单界面隐藏四个按钮
mainScenePlayerInfo:false,//是否隐藏主界面玩家信息 mainScenePlayerInfo:false,//是否隐藏主界面玩家信息
mainSceneButton:false,//是否隐藏主界面按钮 mainSceneButton:false,//是否隐藏主界面按钮
shareRoom:false,//接收到星星场是否屏蔽平台框架显示 shareRoom:false,//接收到星星场是否屏蔽平台框架显示
changeSeat:false,//是否屏蔽平台自动刷新换座后界面 changeSeat:false,//是否屏蔽平台自动刷新换座后界面
hideNotice:false,//隐藏公告栏 hideNotice:false,//隐藏公告栏
vipInfinite:false,//是否为百人场 vipInfinite:false,//是否为百人场
} }
@@ -46,6 +46,52 @@ Game_Modify.CreateRoomData = {
Type_2: 0 Type_2: 0
}; };
// ---------- 玩家信息/头像默认布局(供 02 转发壳 updatePlayerInfoUI 的默认实现使用)----------
// 02_SubGame_Input.js 的 updatePlayerInfoUI 在【无 SubGameHooks 实现】时提供默认头像等 UI 渲染;
// 转发壳不得引用 codes,故把默认实现所需布局放在此处(Game_Modify 下),与子游戏 codes 内
// GameView.updatePlayerInfoUI 使用的布局逻辑保持一致。子游戏若自定义头像 UI,则用 hook 覆盖、此配置不生效。
// POSITIONS[configIndex]:ind0=南(自己)/ind1=东/ind2=北/ind3=西;渲染精灵 346+ind 底框、376+ind 头像框、
// 406+ind 昵称、436+ind 分数、116+ind 头像图、群组 43+ind。坐标基于设计分辨率 1280x720。
Game_Modify.PLAYER_INFO_LAYOUT = {
POSITIONS: {
0: { FRAME_X: 6, FRAME_Y: 592, FRAME_WIDTH: 90, FRAME_HEIGHT: 115,
AVATAR: { OFFSET_X: 5, OFFSET_Y: 5, WIDTH: 80, HEIGHT: 80,
NAME_OFFSET_X: -400, NAME_OFFSET_Y: 888, SCORE_OFFSET_X: 0, SCORE_OFFSET_Y: 88 },
NAME_CENTER_ALIGN: true },
1: { FRAME_X: 1186, FRAME_Y: 85, FRAME_WIDTH: 90, FRAME_HEIGHT: 115,
AVATAR: { OFFSET_X: 5, OFFSET_Y: 5, WIDTH: 80, HEIGHT: 80,
NAME_OFFSET_X: 905, NAME_OFFSET_Y: -888, SCORE_OFFSET_X: 0, SCORE_OFFSET_Y: 88 },
NAME_CENTER_ALIGN: true },
2: { FRAME_X: 1006, FRAME_Y: 5, FRAME_WIDTH: 90, FRAME_HEIGHT: 115,
AVATAR: { OFFSET_X: 5, OFFSET_Y: 5, WIDTH: 80, HEIGHT: 80,
NAME_OFFSET_X: 905, NAME_OFFSET_Y: -888, SCORE_OFFSET_X: 0, SCORE_OFFSET_Y: 88 },
NAME_CENTER_ALIGN: true },
3: { FRAME_X: 7, FRAME_Y: 59, FRAME_WIDTH: 90, FRAME_HEIGHT: 115,
AVATAR: { OFFSET_X: 5, OFFSET_Y: 5, WIDTH: 80, HEIGHT: 80,
NAME_OFFSET_X: -605, NAME_OFFSET_Y: -888, SCORE_OFFSET_X: 0, SCORE_OFFSET_Y: 88 },
NAME_CENTER_ALIGN: true }
},
TEXT_STYLE: { NAME_FONT_SIZE: 14, NAME_MAX_LENGTH: 6, SCORE_FONT_SIZE: 12, CHAR_WIDTH: 9 }
};
// ---------- 桌面聊天/语音气泡默认布局(供 02 转发壳 ShowChat/gameui_play_voice/gameui_stop_voice 使用)----------
// 平台默认按固定显示位序取 Game_Config.Chat/Voice 的固定坐标(固定人数模式);此处提供可变人数版:
// 按 updatePlayerInfoUI 的 configIndex(物理位) 定位,使气泡随人数(2/3/4)贴合各玩家。
// 仅定义每个物理位(0南/1东/2北/3西)的气泡框左上角锚点 X/Y 与朝向 IS_LEFT;气泡内边距/字宽/显示时长等
// 尺寸复用平台 Game_Config.Chat/Voice/Max(与人数无关)。精灵:聊天 背景552+ind/文字582+ind,
// 语音 背景612+ind/内容642+ind。IS_LEFT=true 气泡向右展开(用 IMG_LEFT)、false 向左展开(用 IMG_RIGHT)。
// 坐标基于 1280x720,锚点参考 PLAYER_INFO_LAYOUT 各位底框,可按实际界面微调。
Game_Modify.BUBBLE_LAYOUT = {
IMG_LEFT: 83, // 左向气泡背景图资源ID(气泡向右展开)
IMG_RIGHT: 86, // 右向气泡背景图资源ID(气泡向左展开)
POSITIONS: {
0: { X: 100, Y: 540, IS_LEFT: true }, // 南(自己)
1: { X: 1176, Y: 120, IS_LEFT: false }, // 东
2: { X: 970, Y: 20, IS_LEFT: false }, // 北
3: { X: 120, Y: 70, IS_LEFT: true } // 西
}
};
// ---------- 接口转发壳(A 类:无 hook 即 no-op) ---------- // ---------- 接口转发壳(A 类:无 hook 即 no-op) ----------
Game_Modify.utlmousedown = function (gameid, spid, downx, downy, no1, no2, no3, no4, no5, no6) { Game_Modify.utlmousedown = function (gameid, spid, downx, downy, no1, no2, no3, no4, no5, no6) {
@@ -1,7 +1,7 @@
// ============================================================================ // ============================================================================
// 02_SubGame_Input.template.js —— 平台输入/回调接口「转发壳」模板(框架维护,子游戏不碰) // 02_SubGame_Input.js —— 平台输入/回调接口「转发壳」(框架维护,子游戏不碰)
// 平台按全局名调用 gameHallImport.* / Game_Modify.*;本文件统一转发到 SubGameHooks 同名 hook。 // 平台按全局名调用 gameHallImport.* / Game_Modify.*;本文件统一转发到 SubGameHooks 同名 hook。
// 子游戏只需在 codes/ 的 SubGameHooks 里实现需要的接口(见 SubGameHooks.template.js)。 // 子游戏只需在 codes/ 的 SubGameHooks 里实现需要的接口(见 codes/SubGameHooks.js)。
// 严格 ES5。接口清单与默认值依据 spec §9。 // 严格 ES5。接口清单与默认值依据 spec §9。
// ============================================================================ // ============================================================================
@@ -293,8 +293,210 @@ Game_Modify.getMaxPlayerCount = function (roomtype) {
return (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4; return (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
}; };
// B 类:更新玩家信息 UI,无 hook 返回 false(让平台默认处理) // 内部辅助(非平台接口):把相对显示位序 relPos 映射为 { ind:精灵槽, configIndex:物理布局位 }。
// 2/3 人时 ind≠configIndex;无效返回 null。updatePlayerInfoUI 与桌面气泡默认实现共用此映射,
// 保证头像面板与聊天/语音气泡落在同一玩家位。
Game_Modify._resolveDisplaySlot = function (relPos, playerCount) {
var ind = -1, configIndex = -1;
if (playerCount === 2) {
if (relPos === 0) { ind = 0; configIndex = 0; }
else if (relPos === 1) { ind = 1; configIndex = 2; }
} else if (playerCount === 3) {
if (relPos === 0) { ind = 0; configIndex = 0; }
else if (relPos === 1) { ind = 1; configIndex = 1; }
else if (relPos === 2) { ind = 2; configIndex = 3; }
} else {
if (relPos === 0) { ind = 0; configIndex = 0; }
else if (relPos === 1) { ind = 1; configIndex = 1; }
else if (relPos === 2) { ind = 2; configIndex = 2; }
else if (relPos === 3) { ind = 3; configIndex = 3; }
}
if (ind === -1) return null;
return { ind: ind, configIndex: configIndex };
};
// B 类:更新指定座位的玩家信息 UI(昵称/分数/头像)。
// 有 hook 则委托子游戏;无 hook 时由本模板提供默认头像等 UI 的显示逻辑,其逻辑与子游戏
// codes 内 GameView.updatePlayerInfoUI 保持一致,布局取自 Game_Modify.PLAYER_INFO_LAYOUT(见 01)。
// 精灵操作一律经框架 SpriteManager(setPosition/setSize/setText/show|hideGroup),不直接调
// set_self/set_group 等引擎原语;头像加载走平台 Func.up_imgurl(非精灵原语,SpriteManager 无对应能力)。
// 不引用任何 codes 模块。平台 GameUI.updatePlayerInfoUI 会对每个座位逐一调用本接口,故按单座位渲染。
Game_Modify.updatePlayerInfoUI = function (targetSeat) { Game_Modify.updatePlayerInfoUI = function (targetSeat) {
if (window.SubGameHooks && SubGameHooks.updatePlayerInfoUI) return SubGameHooks.updatePlayerInfoUI(targetSeat); if (window.SubGameHooks && SubGameHooks.updatePlayerInfoUI) return SubGameHooks.updatePlayerInfoUI(targetSeat);
return false;
// ---- 模板默认:与 GameView.updatePlayerInfoUI 一致的单座位渲染 ----
if (typeof Desk === 'undefined' || typeof C_Player === 'undefined' || typeof SpriteManager === 'undefined') return false;
var layout = Game_Modify.PLAYER_INFO_LAYOUT;
if (!layout || !layout.POSITIONS) return false;
if (!Desk.PlayerList || targetSeat < 0 || targetSeat >= Desk.PlayerList.length) return false;
var selfSeat = (typeof C_Player.seat === 'number') ? C_Player.seat : 0;
var playerCount = (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
var relPos = (targetSeat - selfSeat + playerCount) % playerCount;
// 显示槽 ind 与布局位 configIndex(2/3 人时两者不同)
var slot = Game_Modify._resolveDisplaySlot(relPos, playerCount);
if (!slot) return false;
var ind = slot.ind, configIndex = slot.configIndex;
var player = Desk.PlayerList[targetSeat];
// 空座位:隐藏该位玩家信息群组
if (!player || !player.playerid || player.playerid === -1) {
SpriteManager.hideGroup(43 + ind);
return true;
}
var posConfig = layout.POSITIONS[configIndex];
if (!posConfig) return false;
// 底框(346+ind)
if (posConfig.FRAME_X !== undefined) {
SpriteManager.setPosition(346 + ind, posConfig.FRAME_X, posConfig.FRAME_Y);
SpriteManager.setSize(346 + ind, posConfig.FRAME_WIDTH, posConfig.FRAME_HEIGHT);
}
// 头像框(376+ind):基于底框 + 相对偏移
var avatarX = posConfig.FRAME_X + posConfig.AVATAR.OFFSET_X;
var avatarY = posConfig.FRAME_Y + posConfig.AVATAR.OFFSET_Y;
var avatarWidth = posConfig.AVATAR.WIDTH;
var avatarHeight = posConfig.AVATAR.HEIGHT;
var nameOffsetX = posConfig.AVATAR.NAME_OFFSET_X || 0;
var nameOffsetY = posConfig.AVATAR.NAME_OFFSET_Y || 0;
var scoreOffsetX = posConfig.AVATAR.SCORE_OFFSET_X || 0;
var scoreOffsetY = posConfig.AVATAR.SCORE_OFFSET_Y || 0;
SpriteManager.setPosition(376 + ind, avatarX, avatarY);
SpriteManager.setSize(376 + ind, avatarWidth, avatarHeight);
var textStyle = layout.TEXT_STYLE || {};
var maxLen = textStyle.NAME_MAX_LENGTH || 8;
var charWidth = textStyle.CHAR_WIDTH || 7;
// 昵称(406+ind)
var nickname = Func.subString(player.nickname || '', maxLen, true);
SpriteManager.setText(406 + ind, nickname);
var nameX = avatarX + nameOffsetX;
if (posConfig.NAME_CENTER_ALIGN) {
var textWidth = (typeof nickname.gblen === 'function') ? nickname.gblen() * charWidth : nickname.length * charWidth;
nameX = avatarX + avatarWidth / 2 - textWidth / 2;
}
SpriteManager.setPosition(406 + ind, nameX, avatarY + nameOffsetY);
// 分数(436+ind)
var score = (typeof player.score === 'number') ? player.score : 0;
SpriteManager.setText(436 + ind, score);
var scoreX = avatarX + scoreOffsetX;
if (posConfig.NAME_CENTER_ALIGN) {
var scoreWidth = String(score).length * charWidth;
scoreX = avatarX + avatarWidth / 2 - scoreWidth / 2;
}
SpriteManager.setPosition(436 + ind, scoreX, avatarY + scoreOffsetY);
// 头像图(116+ind):Func.up_imgurl 为平台头像加载接口(非引擎精灵原语)
if (player.avatar && typeof Func !== 'undefined' && typeof Func.up_imgurl === 'function') {
Func.up_imgurl(116 + ind, player.avatar);
}
// 显示该座位玩家信息群组
SpriteManager.showGroup(43 + ind);
return true;
}; };
// 桌面文字聊天气泡:有 hook 委托子游戏;无 hook 时按【可变人数】默认渲染,使气泡贴合各玩家。
// seat 为绝对座位;经 SpriteManager 操作精灵(背景552+ind/文字582+ind),位置取自 Game_Modify.BUBBLE_LAYOUT
// 的物理位配置,尺寸(内边距/字宽/显示时长)复用 Game_Config.Chat/Max;不直接调引擎原语、不引用 codes。
Game_Modify.ShowChat = function (seat, text) {
if (window.SubGameHooks && SubGameHooks.ShowChat) return SubGameHooks.ShowChat(seat, text);
GameUI.CloseChat();
if (typeof Desk === 'undefined' || typeof C_Player === 'undefined' || typeof SpriteManager === 'undefined') return;
var layout = Game_Modify.BUBBLE_LAYOUT;
if (!layout || !layout.POSITIONS) return;
var playerCount = (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
var relPos = (seat - C_Player.seat + playerCount) % playerCount;
var slot = Game_Modify._resolveDisplaySlot(relPos, playerCount);
if (!slot) return;
var pos = layout.POSITIONS[slot.configIndex];
if (!pos) return;
var ind = slot.ind;
text = String(text);
var chat = Game_Config.Chat || {};
var dis_x = (chat.ChatDis && chat.ChatDis[0]) || 0;
var dis_y = (chat.ChatDis && chat.ChatDis[1]) || 0;
var text_w = chat.textwidth || 14;
var text_len = (typeof text.gblen === 'function') ? text.gblen() : text.length;
var bgW = text_len * text_w + dis_x * 2;
SpriteManager.setText(582 + ind, text);
if (pos.IS_LEFT) {
SpriteManager.setPosition(552 + ind, pos.X, pos.Y);
SpriteManager.setPosition(582 + ind, pos.X + dis_x, pos.Y + dis_y);
SpriteManager.setImage(552 + ind, layout.IMG_LEFT);
} else {
SpriteManager.setPosition(552 + ind, pos.X - bgW, pos.Y);
SpriteManager.setPosition(582 + ind, pos.X - text_len * text_w - dis_x, pos.Y + dis_y);
SpriteManager.setImage(552 + ind, layout.IMG_RIGHT);
}
SpriteManager.setWidth(552 + ind, bgW);
SpriteManager.show(552 + ind);
SpriteManager.show(582 + ind);
// 自动隐藏:复用平台配置的显示时长(观感与固定人数版一致),按槽位去重定时器
var dur = (Game_Config.Max && Game_Config.Max.ShowChat) || 3000;
Game_Modify._chatHideTimers = Game_Modify._chatHideTimers || {};
if (Game_Modify._chatHideTimers[ind]) { clearTimeout(Game_Modify._chatHideTimers[ind]); }
Game_Modify._chatHideTimers[ind] = setTimeout(function () {
SpriteManager.hide(552 + ind);
SpriteManager.hide(582 + ind);
Game_Modify._chatHideTimers[ind] = null;
}, dur);
};
// 桌面语音气泡(播放):有 hook 委托子游戏;无 hook 时按【可变人数】默认渲染。
// uiIndex 已是相对显示位序(relPos);经 SpriteManager 操作精灵(背景612+ind/内容642+ind),
// 位置取自 Game_Modify.BUBBLE_LAYOUT,气泡宽度依据内容精灵当前宽度;尺寸复用 Game_Config.Voice。
Game_Modify.gameui_play_voice = function (uiIndex) {
if (window.SubGameHooks && SubGameHooks.gameui_play_voice) return SubGameHooks.gameui_play_voice(uiIndex);
if (typeof SpriteManager === 'undefined') return;
var layout = Game_Modify.BUBBLE_LAYOUT;
if (!layout || !layout.POSITIONS) return;
var playerCount = (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
var relPos = ((Number(uiIndex) % playerCount) + playerCount) % playerCount;
var slot = Game_Modify._resolveDisplaySlot(relPos, playerCount);
if (!slot) return;
var pos = layout.POSITIONS[slot.configIndex];
if (!pos) return;
var ind = slot.ind;
var voice = Game_Config.Voice || {};
var dis_x = (voice.VoiceDis && voice.VoiceDis[0]) || 0;
var dis_y = (voice.VoiceDis && voice.VoiceDis[1]) || 0;
var contentSize = SpriteManager.getSize(642 + ind);
var text_len = (contentSize && typeof contentSize.width === 'number') ? contentSize.width : 0;
var bgW = text_len + dis_x * 2;
if (pos.IS_LEFT) {
SpriteManager.setPosition(612 + ind, pos.X, pos.Y);
SpriteManager.setPosition(642 + ind, pos.X + dis_x, pos.Y + dis_y);
SpriteManager.setImage(612 + ind, layout.IMG_LEFT);
} else {
SpriteManager.setPosition(612 + ind, pos.X - bgW, pos.Y);
SpriteManager.setPosition(642 + ind, pos.X - text_len - dis_x, pos.Y + dis_y);
SpriteManager.setImage(612 + ind, layout.IMG_RIGHT);
}
SpriteManager.setWidth(612 + ind, bgW);
SpriteManager.show(612 + ind);
SpriteManager.show(642 + ind);
play_ani(1, 642 + ind, 43, 1, 3, 0, Game_Config.Voice.VoiceTime, 0, 0, 0, 0, 0, 0);
};
// 桌面语音气泡(停止):有 hook 委托子游戏;无 hook 时隐藏对应槽位的语音气泡。
Game_Modify.gameui_stop_voice = function (uiIndex) {
if (window.SubGameHooks && SubGameHooks.gameui_stop_voice) return SubGameHooks.gameui_stop_voice(uiIndex);
if (typeof SpriteManager === 'undefined') return;
var playerCount = (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
var relPos = ((Number(uiIndex) % playerCount) + playerCount) % playerCount;
var slot = Game_Modify._resolveDisplaySlot(relPos, playerCount);
if (!slot) return;
var ind = slot.ind;
SpriteManager.hide(612 + ind);
SpriteManager.hide(642 + ind);
};
File diff suppressed because it is too large Load Diff
+185
View File
@@ -0,0 +1,185 @@
///////////////////////////////////////////////////
////////// cls_youle_erqiwang_desk: 牌桌 //////////
///////////////////////////////////////////////////
var cls_youle_erqiwang_desk = cls_youle_erqiwang_desk || {
countdown_jiaofen: 15, //叫分倒计时
countdown_xuanzhu: 20, //选主倒计时
countdown_maipai: 25, //埋牌倒计时
countdown_chupai: 30, //出牌倒计时
//创建牌桌对象
new: function(o_room){
var desk = {};
desk.o_room = o_room; //桌所属的房间对象
desk.seatlist = []; //玩家得分
desk.seatlist.push([0, []]); //第一位为累积得分,第二位为每局得分
desk.seatlist.push([0, []]);
desk.seatlist.push([0, []]);
desk.prepare = [0,0,0]; //玩家准备状态
desk.paiju_list = []; //牌局列表
//方法
desk.method = {};
//当前牌局
desk.method.curr_paiju = function(){
return desk.paiju_list[desk.paiju_list.length - 1];
}
//新牌局
desk.method.do_new_paiju = function(firstseat){
cls_youle_erqiwang_desk.do_new_paiju(desk, firstseat);
}
//叫分倒计时
desk.method.get_countdown_jiaofen = function(){
return cls_youle_erqiwang_desk.countdown_jiaofen;
}
//叫主倒计时
desk.method.get_countdown_xuanzhu = function(){
return cls_youle_erqiwang_desk.countdown_xuanzhu;
}
//埋牌倒计时
desk.method.get_countdown_maipai = function(){
return cls_youle_erqiwang_desk.countdown_maipai;
}
//出牌倒计时
desk.method.get_countdown_chupai = function(){
return cls_youle_erqiwang_desk.countdown_chupai;
}
//准备
desk.method.do_prepare = function(seat){
return cls_youle_erqiwang_desk.do_prepare(desk, seat);
}
//获取大局结算包
desk.method.get_desk_account = function(msg){
return cls_youle_erqiwang_desk.get_desk_account(desk, msg);
}
return desk;
},
//新开一局
do_new_paiju: function(o_desk, firstseat){
//恢复玩家准备状态
o_desk.prepare = [0,0,0];
if (o_desk.paiju_list.length > 0){
delete o_desk.paiju_list[o_desk.paiju_list.length - 1].tmp_jiesuan_aset;
}
//新开一局
cls_youle_erqiwang_paiju.new(o_desk, firstseat);
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "fapai";
msg.data = {};
msg.data.asetidx = o_desk.paiju_list.length;
msg.data.asetcount = o_desk.o_room.asetcount;
msg.data.seat = o_desk.method.curr_paiju().method.get_callgrade_seat();
msg.data.countdown = o_desk.method.get_countdown_jiaofen();
for (var i = 0; i < o_desk.o_room.seatlist.length; i++) {
msg.conmode = o_desk.o_room.seatlist[i].conmode;
msg.fromid = o_desk.o_room.seatlist[i].fromid;
msg.data.cards = o_desk.method.curr_paiju().method.get_seat_cards(i);
youle_erqiwang.app.SendPack(msg);
}
},
//准备
do_prepare: function(o_desk, seat){
o_desk.prepare[seat] = 1;
if (o_desk.prepare[0] && o_desk.prepare[1] && o_desk.prepare[2]){
//新开一局
var paiju = o_desk.method.curr_paiju();
var firstseat = paiju.banker;
if (paiju.result == 1 || paiju.result == 2){
firstseat = (firstseat + 1) % 3;
}
cls_youle_erqiwang_desk.do_new_paiju(o_desk, firstseat);
} else {
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "zhunbei";
msg.data = {};
msg.data.seat = seat;
o_desk.o_room.method.sendpack_toother(msg, -1);
}
},
//大局结算包
get_desk_account: function(o_desk, msg){
//保存战绩
var do_save_grade = function(){
var o_gameinfo1 = {};
o_gameinfo1.roomcode = o_desk.o_room.roomcode;
o_gameinfo1.asetcount = o_desk.paiju_list.length;
o_gameinfo1.createtime = o_desk.o_room.createtime;
o_gameinfo1.makewartime = o_desk.o_room.makewartime;
o_gameinfo1.players = [];
for (var i = 0; i < o_desk.o_room.seatlist.length; i++){
var _player = {};
_player.seat = i;
_player.playerid = o_desk.o_room.seatlist[i].playerid;
_player.name = o_desk.o_room.seatlist[i].nickname;
_player.avatar = o_desk.o_room.seatlist[i].avatar;
_player.score = o_desk.seatlist[i][0];
o_gameinfo1.players.push(_player);
}
var o_gameinfo2 = [];
for (var i = 0; i < o_desk.paiju_list.length; i++) {
var _paiju = {};
_paiju.starttime = o_desk.paiju_list[i].starttime;
_paiju.endtime = o_desk.paiju_list[i].endtime;
_paiju.seatlist = [0, 0, 0];
_paiju.seatlist[0] = o_desk.seatlist[0][1][i];
_paiju.seatlist[1] = o_desk.seatlist[1][1][i];
_paiju.seatlist[2] = o_desk.seatlist[2][1][i];
_paiju.callproc = o_desk.paiju_list[i].callproc;
_paiju.banker = o_desk.paiju_list[i].banker;
_paiju.call = o_desk.paiju_list[i].call;
_paiju.flower = o_desk.paiju_list[i].flower;
_paiju.result = o_desk.paiju_list[i].result;
//数据量太多,json转数组后进行存储
_paiju.cards = [];
for (var j = 0; j < o_desk.paiju_list[i].cards.length; j++) {
var _pai = o_desk.paiju_list[i].cards[j];
var _card = [];
_card.push(_pai.id);
_card.push(_pai.flower);
_card.push(_pai.number);
_card.push(_pai.score);
_card.push(_pai.dealowner);
_card.push(_pai.playround);
_card.push(_pai.playindex);
_card.push(_pai.playowner);
_paiju.cards.push(_card);
}
o_gameinfo2.push(_paiju);
}
//累积游戏得分
for (var i = 0; i < o_desk.o_room.seatlist.length; i++) {
o_desk.o_room.seatlist[i].gameinfo.grade = o_desk.seatlist[i][0];
}
//保存战绩
youle_erqiwang.import.save_grade(o_desk.o_room, o_gameinfo1, o_gameinfo2, 1);
}
min_ontimeout(do_save_grade, 1000);
msg.data.account = o_desk.seatlist;
return msg;
}
}
+273
View File
@@ -0,0 +1,273 @@
///////////////////////////////////////////////////
/////// cls_youle_erqiwang_export: 输出接口 ///////
///////////////////////////////////////////////////
var cls_youle_erqiwang_export = cls_youle_erqiwang_export || {
new: function() {
var exp = {};
//解析roomtype获取开房所需房卡
exp.get_needroomcard = function(roomtype){
switch (roomtype[2])
{
case 1: //房主扣卡
switch (roomtype[0])
{
case 1:
return 1;
break;
case 2:
return 2;
break;
default:
return 1;
break;
}
break;
case 2: //每人扣卡
switch (roomtype[0])
{
case 1:
return 1;
break;
case 2:
return 2;
break;
default:
return 1;
break;
}
break;
}
}
//解析roomtype获取总局数
exp.get_asetcount = function(roomtype){
switch (roomtype[0])
{
case 1:
return 2;
break;
case 2:
return 4;
break;
default:
return 4;
break;
}
}
//加入房间时根据roomtype获取需要的房卡数量
exp.get_needroomcard_joinroom = function(roomtype){
switch (roomtype[2])
{
case 1: //房主扣卡
return 0;
break;
case 2: //每人扣卡
switch (roomtype[0])
{
case 1:
return 1;
break;
case 2:
return 2;
break;
default:
return 1;
break;
}
break;
}
}
//开战
exp.makewar = function(o_room){
//牌桌对象
o_room.o_desk = cls_youle_erqiwang_desk.new(o_room);
for (var i = 0; i < o_room.seatlist.length; i++) {
o_room.seatlist[i].gameinfo.isbet = 1;
}
//开始第一局
var do_newpaiju = function(){
o_room.o_desk.method.do_new_paiju(0);
}
min_ontimeout(do_newpaiju, 1000);
}
//获取某个玩家的牌桌整桌当前信息
exp.get_deskinfo = function(o_room, seat){
if (!o_room.o_desk){
return null;
}
var paiju = o_room.o_desk.method.curr_paiju();
var deskinfo = {};
deskinfo.count = o_room.asetcount; //总局数
deskinfo.idx = paiju.idx; //当前局数
deskinfo.PlayerInfo = []; //玩家当前累积得分
for (var i = 0; i < o_room.o_desk.seatlist.length; i++) {
deskinfo.PlayerInfo.push(o_room.o_desk.seatlist[i][0]);
};
deskinfo.step = paiju.step; //牌局状态
deskinfo.MyCards = paiju.method.get_seat_cards(seat); //我手上的牌
switch (paiju.step){
case 1: //发完牌叫分
deskinfo.CallRun = {};
//当前叫分位置
deskinfo.CallRun.seat = paiju.method.get_callgrade_seat();
//叫分倒计时
deskinfo.CallRun.countdown = o_room.o_desk.method.get_countdown_jiaofen();
//当前叫分
deskinfo.CallRun.nowcall = paiju.method.get_callgrade_value();
//当前叫分倍数
deskinfo.CallRun.multiple = cls_youle_erqiwang_arith.get_multiple_bycall(deskinfo.CallRun.nowcall);
//所有玩家的叫分情况
deskinfo.CallRun.call = [null, null, null];
deskinfo.CallRun.call[0] = paiju.method.get_seat_callgrade(0);
deskinfo.CallRun.call[1] = paiju.method.get_seat_callgrade(1);
deskinfo.CallRun.call[2] = paiju.method.get_seat_callgrade(2);
break;
case 2: //选主
deskinfo.ChooseMain = {};
//庄家
deskinfo.ChooseMain.banker = paiju.banker;
//庄家叫分
deskinfo.ChooseMain.call = paiju.call;
//庄家叫分倍数
deskinfo.ChooseMain.multiple = cls_youle_erqiwang_arith.get_multiple_bycall(paiju.call);
//选主倒计时
deskinfo.ChooseMain.countdown = o_room.o_desk.method.get_countdown_xuanzhu();
//底牌
deskinfo.ChooseMain.bottomcards = paiju.method.get_bottomcards();
//是否可投降
if (paiju.call < 65){
deskinfo.ChooseMain.touxiang = 0;
} else {
deskinfo.ChooseMain.touxiang = 1;
}
break;
case 3: //埋牌
deskinfo.BuryCards = {};
//庄家
deskinfo.BuryCards.banker = paiju.banker;
//庄家叫分
deskinfo.BuryCards.call = paiju.call;
//庄家叫分倍数
deskinfo.BuryCards.multiple = cls_youle_erqiwang_arith.get_multiple_bycall(paiju.call);
//埋牌倒计时
deskinfo.BuryCards.countdown = o_room.o_desk.method.get_countdown_maipai();
//底牌
deskinfo.BuryCards.bottomcards = paiju.method.get_bottomcards();
//主牌花色
deskinfo.BuryCards.flower = paiju.flower;
//是否可投降
if (paiju.call < 65){
deskinfo.BuryCards.touxiang = 0;
} else {
deskinfo.BuryCards.touxiang = 1;
}
break;
case 4: //投降
deskinfo.Surrender = {};
//庄家
deskinfo.Surrender.banker = paiju.banker;
//庄家叫分
deskinfo.Surrender.call = paiju.call;
//庄家叫分倍数
deskinfo.Surrender.multiple = cls_youle_erqiwang_arith.get_multiple_bycall(paiju.call);
//出牌倒计时
deskinfo.Surrender.countdown = o_room.o_desk.method.get_countdown_chupai();
//主牌花色
deskinfo.Surrender.flower = paiju.flower;
if (seat == paiju.banker){
//埋的牌
deskinfo.Surrender.bottomcards = paiju.method.get_burycard();
}
//是否可投降
if (paiju.call < 65){
deskinfo.Surrender.touxiang = 0;
} else {
deskinfo.Surrender.touxiang = 1;
}
break;
case 5: //出牌
deskinfo.PushCards = {};
//庄家
deskinfo.PushCards.banker = paiju.banker;
//庄家叫分
deskinfo.PushCards.call = paiju.call;
//庄家叫分倍数
deskinfo.PushCards.multiple = cls_youle_erqiwang_arith.get_multiple_bycall(paiju.call);
//出牌倒计时
deskinfo.PushCards.countdown = o_room.o_desk.method.get_countdown_chupai();
//主牌花色
deskinfo.PushCards.flower = paiju.flower;
//捡分
var jian_grade = paiju.method.get_jian_grade();
if (seat == paiju.banker){
//埋的牌
deskinfo.PushCards.bottomcards = paiju.method.get_burycard();
} else {
//闲家当前的捡分分牌
deskinfo.PushCards.gradecards = jian_grade.cards;
}
//闲家当前的捡分分数
deskinfo.PushCards.grade = jian_grade.grade;
//当前的出牌情况
deskinfo.PushCards.playproc = paiju.playproc;
//座位列表
deskinfo.PushCards.seatlist = paiju.seatlist;
//出牌历史
deskinfo.PushCards.pushlist = [];
for (var i = 0; i < paiju.cards.length; i++) {
var pai = paiju.cards[i];
if (pai.playround > 0){
if (deskinfo.PushCards.pushlist.length < pai.playround){
deskinfo.PushCards.pushlist.length = pai.playround;
}
if (!deskinfo.PushCards.pushlist[pai.playround - 1]){
deskinfo.PushCards.pushlist[pai.playround - 1] = [[], [], []];
}
var owner = pai.dealowner;
if (owner == 0){
owner = paiju.banker + 1;
}
deskinfo.PushCards.pushlist[pai.playround - 1][owner - 1].push(i);
}
}
//按大小排序
for (var i = 0; i < deskinfo.PushCards.pushlist.length; i++) {
for (var j = 0; j < deskinfo.PushCards.pushlist.length; j++) {
deskinfo.PushCards.pushlist[i][j] = cls_youle_erqiwang_arith.order_cards(pai.flower, deskinfo.PushCards.pushlist[i][j]);
}
}
break;
case 6: //结算
deskinfo.Balance = {};
//玩家的准备状态
deskinfo.Balance.readystate = o_room.o_desk.prepare;
if (o_room.o_desk.prepare[seat] == 0){
//结算情况
deskinfo.Balance.aset = paiju.tmp_jiesuan_aset;
}
break;
}
return deskinfo;
}
//解散房间
exp.get_disbandRoom = function(o_room){
var msg = {};
return o_room.o_desk.method.curr_paiju().method.get_paiju_account(2, msg);
}
return exp;
}
}
//对内输出接口
youle_erqiwang.export = cls_youle_erqiwang_export.new();
+30
View File
@@ -0,0 +1,30 @@
///////////////////////////////////////////////////
/////// cls_youle_platform_import: 输入接口 ///////
///////////////////////////////////////////////////
var cls_youle_erqiwang_import = cls_youle_erqiwang_import || {
new: function() {
var imp = {};
//检查玩家
imp.check_player = function(agentid, gameid, roomcode, seat, playerid, conmode, fromid){
return youle_erqiwang.app.youle_room.export.check_player(agentid, gameid, roomcode, seat, playerid, conmode, fromid);
}
//第一局结算扣除房卡
imp.deduct_roomcard = function(o_room){
return youle_erqiwang.app.youle_room.export.deduct_roomcard(o_room);
}
//保存战绩
imp.save_grade = function(o_room, o_gameinfo1, o_gameinfo2, freeroomflag){
youle_erqiwang.app.youle_room.export.save_grade(o_room, o_gameinfo1, o_gameinfo2, freeroomflag);
}
return imp;
}
}
// 输入接口
youle_erqiwang.import = cls_youle_erqiwang_import.new();
+61
View File
@@ -0,0 +1,61 @@
///////////////////////////////////////////////////
////////// cls_youle_erqiwang_pai: 牌 //////////
///////////////////////////////////////////////////
var cls_youle_erqiwang_pai = cls_youle_erqiwang_pai || {
//创建单张牌对象
new: function(id, flower, number, score, dealowner){
/*
参数说明:
id 绝对id
flower 牌面花色
number 牌面数值
code 牌编码
扑克牌的统一编码规则:
1,牌面花色的定义 5:王 4:黑桃 3:红心 2:梅花 1:方块
2,牌面数值的定义 01:A 02:2 03:3 ... 09:9 10:10 11:J 12:Q 13:K 53:小王 54:大王
3,牌编码的定义
3.1 常主的牌编码定义:
未叫主之前:
2的牌编码定义 黑桃2:2402 红心2:2302 梅花2:2202 方块2:2102
7的牌编码定义 黑桃7:7407 红心2:7307 梅花7:7207 方块7:7107
王的牌编码定义 小王:9553 大王:9554
叫主之后:
主2牌编码定义 原有牌编码的基础上+1000,例如红心为主牌,则红心2的牌编码变为3302
主7牌编码定义 原有牌编码的基础上+1000,例如红心为主牌,则红心7的牌编码变为8307
3.2 普通牌的牌编码定义
A的定义:14
未叫主之前:
3位数字 第一位为牌面花色 第二三位为牌面数值
叫主之后:
主牌花色 4位数字,即在原有牌编码的基础上+1000,表示是主牌
非主牌花色 3位数字,即原有牌编码不变
3.3 综上所述
牌编码是3位的,即小于1000的都是非主牌
牌编码是4位的,即大于1000的都是主牌
可根据牌编码进行牌大小的比较和排序
4 牌型定义
>100: 单张, 101一张单张,102两张单张,103三张单张 ...
>200: 对子, 201一对,202两对,203三对 ...
>300: 拖拉机,302两连对拖拉机,303三连对拖拉机 ...
*/
var pai = {};
pai.id = id; //绝对id,即数组下标,从0开始计数
pai.flower = flower; //牌面花色
pai.number = number; //牌面数值
pai.score = score; //牌在游戏中的分值
pai.dealowner = dealowner; //发牌状态
//-1:规则去除的牌
// 0:未发的牌,即底牌
//>0:发牌发到谁手上,从1开始计数,即位置编号+1,位置编号是从0开始计数的
pai.playround = -1; //出牌状态
//-1:未出的牌
// 0:埋牌
//>0:牌是第几轮出出去的,从1开始计数
pai.playindex = -1; //本轮中的出牌顺序,从1开始计数
pai.playowner = -1; //出牌后被谁得到,与位置编号对应
return pai;
}
}
+863
View File
@@ -0,0 +1,863 @@
///////////////////////////////////////////////////
////////// cls_youle_erqiwang_paiju: 牌局 /////////
///////////////////////////////////////////////////
var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || {
new: function(o_desk, firstseat) {
var paiju = {};
o_desk.paiju_list.push(paiju);
// ================= 实例的私有属性 ================= //
//发牌过程中待发的位置列表
var tmpdeal = [];
//三家需要各发28张牌
for (var i = 0; i < 28; i++){
tmpdeal.push(0); //0表示第一个玩家的位置
tmpdeal.push(1); //1表示第二个玩家的位置
tmpdeal.push(2); //2表示第三个玩家的位置
}
//8张底牌
for (var i = 0; i < 8; i++){
tmpdeal.push(-1); //-1 表示是底牌
}
// ================= 实例的私有方法 ================= //
//初始化座位列表
var init_seatlist = function(){
//第一层是玩家列表,与位置序号对应
//第二层前4个数组是花色列表,与花色-1对应
//第三层的第一位是无该花色的牌标志
//第三层的第二位是该花色无对标志
//第二层第5个数组是报副情况
//第三层的第一位是剩余主牌数量
//第三层的第二位是剩余主牌对子
//0号位
paiju.seatlist.push([[0,0],[0,0],[0,0],[0,0],[-1,-1]]);
//1号位
paiju.seatlist.push([[0,0],[0,0],[0,0],[0,0],[-1,-1]]);
//2号位
paiju.seatlist.push([[0,0],[0,0],[0,0],[0,0],[-1,-1]]);
}
//初始化牌列表并发牌
var init_cards = function(){
//两副牌
for (var i = 1; i <= 2; i++){
//四种花色
for (var j = 1; j <= 4; j++){
for (var k = 1; k <= 13; k++){
//牌id
var id = (i - 1) * 54 + (j - 1) * 13 + k - 1;
//分值
var score = 0;
//发牌状态
var dealowner = 0;
//特殊处理
switch (k)
{
case 5:
score = 5;
break;
case 10:
score = 10;
break;
case 13:
score = 10;
break;
case 3:
dealowner = -1;
break;
case 4:
dealowner = -1;
break;
}
var pai = cls_youle_erqiwang_pai.new(id, j, k, score, dealowner);
paiju.cards.push(pai);
//发牌
if (k != 3 && k != 4){
do_dealpai(pai);
}
}
}
//小王
var pai = cls_youle_erqiwang_pai.new((i - 1) * 54 + 53 - 1, 5, 53, 0, 0);
paiju.cards.push(pai);
do_dealpai(pai);
//大王
var pai = cls_youle_erqiwang_pai.new((i - 1) * 54 + 54 - 1, 5, 54, 0, 0);
paiju.cards.push(pai);
do_dealpai(pai);
}
}
//将单张牌随机发到玩家手中或成为底牌
var do_dealpai = function(o_pai){
var idx = min_random(0, tmpdeal.length - 1);
if (tmpdeal[idx] == -1) { //底牌
o_pai.dealowner = 0;
} else {
o_pai.dealowner = tmpdeal[idx] + 1;
}
//将数组的最后一位至该位置
if (idx < tmpdeal.length - 1) {
tmpdeal[idx] = tmpdeal[tmpdeal.length - 1];
};
//数组长度减一
tmpdeal.length = tmpdeal.length - 1;
}
//进入叫分阶段
var start_call = function(){
paiju.step = 1;
var o_call = {};
o_call.seat = paiju.firstseat;
o_call.call = null; //null:未叫分 0:不叫分 >0:叫分分数
paiju.callproc.push(o_call);
}
// ================= 实例的公有属性 ================= //
paiju.o_desk = o_desk;
//牌局号
paiju.idx = o_desk.paiju_list.length;
//第一个叫分者的位置
paiju.firstseat = firstseat;
//开始时间
paiju.starttime = min_now();
//结束时间
paiju.endtime = null;
//座位列表
paiju.seatlist = [];
//牌列表
paiju.cards = [];
//当前阶段
paiju.step = -1; //1发完牌叫分 2选主 3埋牌 4投降 5出牌 6结算
//叫分过程
paiju.callproc = [];
//庄家位置
paiju.banker = -1;
//庄家叫分
paiju.call = -1;
//主牌花色
paiju.flower = -1;
//当前出牌情况
paiju.playproc = {};
//输赢结果 0:庄赢 1:闲赢 2:庄投降 3:中途解散
paiju.result = null;
//结算包,供断线重连时使用,新开一局时清除
paiju.tmp_jiesuan_aset = null;
//初始化座位列表
init_seatlist();
//初始化牌列表并发牌
init_cards();
//进入叫分阶段
start_call();
// ================= 实例的公有方法 ================= //
paiju.method = {};
//获取某个位置玩家手上的牌
paiju.method.get_seat_cards = function(seat){
return cls_youle_erqiwang_paiju.get_seat_cards(paiju, seat);
}
//获取当前的叫分位置
paiju.method.get_callgrade_seat = function(){
return paiju.callproc[paiju.callproc.length - 1].seat;
}
//获取当前的叫分分数
paiju.method.get_callgrade_value = function(){
return cls_youle_erqiwang_paiju.get_callgrade_value(paiju);
}
//获取某个玩家的当前叫分情况
paiju.method.get_seat_callgrade = function(seat){
return cls_youle_erqiwang_paiju.get_seat_callgrade(paiju, seat);
}
//叫分或不叫
paiju.method.do_callgrade = function(call){
cls_youle_erqiwang_paiju.do_callgrade(paiju, call);
}
//获取底牌
paiju.method.get_bottomcards = function(){
return cls_youle_erqiwang_paiju.get_bottomcards(paiju);
}
//选主
paiju.method.do_choiceflower = function(flower){
cls_youle_erqiwang_paiju.do_choiceflower(paiju, flower);
}
//判断牌是否在玩家手上
paiju.method.check_cards_inhand = function(cards, seat){
return cls_youle_erqiwang_paiju.check_cards_inhand(paiju, cards, seat);
}
//埋牌
paiju.method.do_burycard = function(cards){
cls_youle_erqiwang_paiju.do_burycard(paiju, cards);
}
//检查选中的牌是否可出,并出牌
paiju.method.do_playcard = function(cards){
return cls_youle_erqiwang_paiju.do_playcard(paiju, cards);
}
//是否有人报副
paiju.method.have_baofu = function(){
return (paiju.seatlist[0][4][0] == 0 || paiju.seatlist[1][4][0] == 0 || paiju.seatlist[2][4][0] == 0);
}
//在牌列表中获取分牌和分值
paiju.method.get_grade_incard = function(cards){
return cls_youle_erqiwang_paiju.get_grade_incard(paiju, cards);
}
//获取埋牌
paiju.method.get_burycard= function(){
return cls_youle_erqiwang_paiju.get_burycard(paiju);
}
//获取抠底包
paiju.method.get_bottom_account = function(msg){
return cls_youle_erqiwang_paiju.get_bottom_account(paiju, msg);
}
//小局结算,type 0正常结算 1投降结算 2解散结算
paiju.method.get_paiju_account = function(type, msg){
return cls_youle_erqiwang_paiju.get_paiju_account(paiju, type, msg);
}
//获取闲家当前的捡分分数和分牌
paiju.method.get_jian_grade = function(){
return cls_youle_erqiwang_paiju.get_jian_grade(paiju);
}
return paiju;
},
//下一个位置
get_nextseat: function(seat){
return (seat + 1) % 3;
},
//获取某个位置玩家手上的牌
get_seat_cards: function(o_paiju, seat){
var aryCardIDs = [];
for (var i = 0; i < o_paiju.cards.length; i++){
var o_card = o_paiju.cards[i];
if (o_card.playround == -1){ //未出的牌
if ((o_card.dealowner == seat + 1) || //发到手上
(o_card.dealowner == 0 && seat == o_paiju.banker)){ //庄家包括底牌
aryCardIDs.push(o_paiju.cards[i].id);
}
}
}
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
},
//获取某个位置玩家发到手上的牌
get_seat_cards_deal: function(o_paiju, seat){
var aryCardIDs = [];
for (var i = 0; i < o_paiju.cards.length; i++){
var o_card = o_paiju.cards[i];
if (o_card.dealowner == seat + 1){
aryCardIDs.push(o_paiju.cards[i].id);
}
}
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
},
//获取某个位置玩家发到手上的牌(庄家包含底牌)
get_seat_cards_owner: function(o_paiju, seat){
var aryCardIDs = [];
for (var i = 0; i < o_paiju.cards.length; i++){
var o_card = o_paiju.cards[i];
if ((o_card.dealowner == seat + 1) || //发到手上
(o_card.dealowner == 0 && seat == o_paiju.banker)){ //庄家包括底牌
aryCardIDs.push(o_paiju.cards[i].id);
}
}
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
},
//获取某个位置玩家手上的主牌
get_seat_zhucards: function(o_paiju, seat){
var aryCardIDs = [];
for (var i = 0; i < o_paiju.cards.length; i++){
var o_card = o_paiju.cards[i];
if (o_card.playround == -1){ //未出的牌
if ((o_card.dealowner == seat + 1) || //发到手上
(o_card.dealowner == 0 && seat == o_paiju.banker)){ //庄家包括底牌
if (o_card.flower == o_paiju.flower || o_card.flower == 5 || o_card.number == 2 || o_card.number == 7){
aryCardIDs.push(o_paiju.cards[i].id);
}
}
}
}
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
},
//获取当前的叫分分数
get_callgrade_value: function(o_paiju){
var value = 0;
for (var i = o_paiju.callproc.length - 2; i >= 0; i--) {
if (o_paiju.callproc[i].call) {
value = o_paiju.callproc[i].call;
break;
}
}
return value;
},
//获取某个位置的叫分 返回值:null未叫 0不叫 >0当前叫分
get_seat_callgrade: function(o_paiju, seat){
for (var i = o_paiju.callproc.length - 2; i >= 0; i--){
if (o_paiju.callproc[i].seat == seat){
return o_paiju.callproc[i].call;
}
}
return null;
},
//叫分
do_callgrade: function(o_paiju, call){
var o_call = o_paiju.callproc[o_paiju.callproc.length - 1];
o_call.call = call;
//等待下一个叫分者叫分
var do_wait_nextseat_call = function(seat){
var o_call = {};
o_call.seat = seat;
o_call.call = null;
o_paiju.callproc.push(o_call);
}
//上庄
var do_up_banker = function(seat, call){
//庄家位置
o_paiju.banker = seat;
//庄家叫分
o_paiju.call = call;
//进入选主阶段
o_paiju.step = 2;
}
//叫了5分直接上庄
if (call == 5){
do_up_banker(o_call.seat, call);
return;
}
//叫了分
if (call > 0){
//下一个位置
var nextseat = cls_youle_erqiwang_paiju.get_nextseat(o_call.seat);
//下一个位置的叫分
var nextcall = cls_youle_erqiwang_paiju.get_seat_callgrade(o_paiju, nextseat);
//下一个位置不叫分
if (nextcall == 0){
//再下一个位置
nextseat = cls_youle_erqiwang_paiju.get_nextseat(nextseat);
}
//等待下一个叫分者叫分
do_wait_nextseat_call(nextseat);
return;
}
//不叫
if (call == 0){
//下一个位置
var nextseat_1 = cls_youle_erqiwang_paiju.get_nextseat(o_call.seat);
//下一个位置的叫分
var nextcall_1 = cls_youle_erqiwang_paiju.get_seat_callgrade(o_paiju, nextseat_1);
//再下一个位置
var nextseat_2 = cls_youle_erqiwang_paiju.get_nextseat(nextseat_1);
//再下一个位置的叫分
var nextcall_2 = cls_youle_erqiwang_paiju.get_seat_callgrade(o_paiju, nextseat_2);
if (nextcall_1 != 0 && nextcall_2 != 0){
//另外两家都没有不叫分
do_wait_nextseat_call(nextseat_1);
return;
}
if (nextcall_1 != 0 && nextcall_2 == 0){
//nextseat_1上庄
do_up_banker(nextseat_1, nextcall_1);
return;
}
if (nextcall_1 == 0 && nextcall_2 != 0){
//nextseat_2上庄
do_up_banker(nextseat_2, nextcall_2);
return;
}
}
},
//获取底牌
get_bottomcards: function(o_paiju){
var aryCardIDs = [];
for (var i = 0; i < o_paiju.cards.length; i++){
if (o_paiju.cards[i].dealowner == 0){
aryCardIDs.push(o_paiju.cards[i].id);
}
}
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
},
//选主
do_choiceflower: function(o_paiju, flower){
o_paiju.flower = flower;
//修改主牌的牌编码
for (var i = 1; i <= 2; i++){ //两副牌
for (var j = 0; j < 13; j++){
var o_card = o_paiju.cards[(i - 1) * 54 + (flower - 1) * 13 + j];
}
}
//进入埋牌阶段
o_paiju.step = 3;
},
//判断牌是否在玩家手上
check_cards_inhand: function(o_paiju, cards, seat){
for (var i = 0; i < cards.length; i++) {
var o_card = o_paiju.cards[cards[i]];
if (o_card.playround != -1){
return;
}
if (seat == o_paiju.banker){
//是庄家
if (o_card.dealowner != seat + 1 && o_card.dealowner != 0){
return false;
}
} else {
//是闲家
if (o_card.dealowner != seat + 1){
return false;
}
}
}
return true;
},
//埋牌
do_burycard: function(o_paiju, cards){
for (var i = 0; i < cards.length; i++) {
o_paiju.cards[cards[i]].playround = 0;
};
//进入投降阶段
o_paiju.step = 4;
cls_youle_erqiwang_paiju.new_playround(o_paiju, 1, o_paiju.banker);
},
//新一轮出牌
new_playround: function(o_paiju, round, startseat){
o_paiju.playproc.round = round; //第几轮
o_paiju.playproc.start = startseat; //本轮的起始出牌位置
o_paiju.playproc.currseat = startseat; //本轮的当前的出牌位置
o_paiju.playproc.startcount = -1; //本轮第一个出牌的张数
o_paiju.playproc.startflower = -1; //本轮第一个出牌的花色
o_paiju.playproc.starttype = -1; //本轮第一个出牌的牌型
o_paiju.playproc.maxseat = -1; //谁的牌最大,位置
o_paiju.playproc.maxcard = -1; //满足第一个玩家出牌的牌型最大牌
o_paiju.playproc.cards = []; //本轮各位置出的牌,数组与位置序号对应
o_paiju.playproc.cards.length= 3;
},
//在牌列表中获取分牌和分值
get_grade_incard: function(o_paiju, cards){
var re = {};
re.grade = 0;
re.cards = [];
for (var i = 0; i < cards.length; i++) {
if (o_paiju.cards[cards[i]].score > 0){
re.grade = re.grade + o_paiju.cards[cards[i]].score;
re.cards.push(cards[i]);
}
}
return re;
},
//检查选中的牌是否可出,并出牌
do_playcard: function(o_paiju, cards){
//出牌
var doplay = function(playindex){
for (var i = 0; i < cards.length; i++) {
o_paiju.cards[cards[i]].playround = o_paiju.playproc.round;
o_paiju.cards[cards[i]].playindex = playindex;
};
o_paiju.playproc.cards[o_paiju.playproc.currseat] = cards;
//进入出牌阶段
o_paiju.step = 5;
//是否报副处理
if (o_paiju.seatlist[o_paiju.playproc.currseat][4][0] != 0){
var _zhu = cls_youle_erqiwang_paiju.get_seat_zhucards(o_paiju, o_paiju.playproc.currseat);
if (_zhu.length == 0){
o_paiju.seatlist[o_paiju.playproc.currseat][4][0] = 0;
o_paiju.seatlist[o_paiju.playproc.currseat][4][1] = 0;
}
if (o_paiju.method.have_baofu()){
var _parilist = cls_youle_erqiwang_arith.get_pairlist(o_paiju.flower, _zhu);
o_paiju.seatlist[o_paiju.playproc.currseat][4][0] = _zhu.length;
o_paiju.seatlist[o_paiju.playproc.currseat][4][1] = _parilist.length;
if (o_paiju.seatlist[o_paiju.playproc.currseat][4][0] == 0){
o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.flower - 1][0] = 1;
o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.flower - 1][1] = 1;
} else if (o_paiju.seatlist[o_paiju.playproc.currseat][4][1] == 0){
o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.flower - 1][1] = 1;
}
}
}
}
//本轮中的第二个出牌位置
var nextseat_1 = cls_youle_erqiwang_paiju.get_nextseat(o_paiju.playproc.start);
//本轮中的第三个出牌位置
var nextseat_2 = cls_youle_erqiwang_paiju.get_nextseat(nextseat_1);
if (o_paiju.playproc.currseat == o_paiju.playproc.start){ //是本轮的第一个出牌
var can = cls_youle_erqiwang_arith.can_playcard(o_paiju.flower, cards, o_paiju.playproc.currseat, o_paiju.seatlist);
if (!can.result){
var re = {};
re.result = false;
return re;
}
//出牌
doplay(1);
//记录下牌型、花色、牌型最大牌
o_paiju.playproc.startcount = cards.length;
o_paiju.playproc.startflower = can.flower;
o_paiju.playproc.starttype = can.cardtype;
o_paiju.playproc.maxseat = o_paiju.playproc.currseat;
o_paiju.playproc.maxcard = can.cardvalue;
o_paiju.playproc.currseat = nextseat_1;
//返回结果
var re1 = {};
re1.result = true;
re1.idx = 1;
re1.count = cards.length;
re1.flower = can.flower;
re1.cardtype = can.cardtype;
return re1;
}
//不是本轮中的第一个出牌,是跟牌
var cardsinhand = cls_youle_erqiwang_paiju.get_seat_cards(o_paiju, o_paiju.playproc.currseat);
var can = cls_youle_erqiwang_arith.can_followcard(o_paiju.flower, cardsinhand, cards, o_paiju.playproc.startcount, o_paiju.playproc.startflower, o_paiju.playproc.starttype);
if (!can.result){
var re = {};
re.result = false;
return re;
}
//与之前的出牌比较大小
if (can.cardvalue > o_paiju.playproc.maxcard){
o_paiju.playproc.maxseat = o_paiju.playproc.currseat;
o_paiju.playproc.maxcard = can.cardvalue;
}
//无花色、无对子处理
if (can.nopair){
o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.playproc.startflower - 1][1] = 1;
}
if (can.noflower){
o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.playproc.startflower - 1][0] = 1;
o_paiju.seatlist[o_paiju.playproc.currseat][o_paiju.playproc.startflower - 1][1] = 1;
}
//是本轮中的第二个出牌
if (o_paiju.playproc.currseat == nextseat_1){
//出牌
doplay(2);
//下一个出牌者
o_paiju.playproc.currseat = nextseat_2;
//返回结果
var re2 = {};
re2.idx = 2;
re2.result = true;
return re2;
}
//是本轮中的第三个出牌
//出牌
doplay(3);
//本轮出牌结束
for (var i = 0; i < o_paiju.playproc.cards.length; i++) {
for (var j = 0; j < o_paiju.playproc.cards[i].length; j++) {
o_paiju.cards[o_paiju.playproc.cards[i][j]].playowner = o_paiju.playproc.maxseat;
}
}
//返回结果
var re3 = {};
re3.result = true;
re3.idx = 3;
re3.maxseat = o_paiju.playproc.maxseat;
if (o_paiju.playproc.maxseat != o_paiju.banker){
re3.grade = 0;
// re3.gradecards = [];
for (var i = 0; i < o_paiju.playproc.cards.length; i++) {
var grade_incards = cls_youle_erqiwang_paiju.get_grade_incard(o_paiju, o_paiju.playproc.cards[i]);
re3.grade = re3.grade + grade_incards.grade;
// re3.gradecards = re3.gradecards.concat(grade_incards.cards);
}
}
//判断是否还有牌在玩家手上未出
var havecard_inhand = function(){
for (var i = 0; i < o_paiju.cards.length; i++) {
if (o_paiju.cards[i].playround == -1 && o_paiju.cards[i].number != 3 && o_paiju.cards[i].number != 4){
return true;
}
}
return false;
}
// if (!havecard_inhand() || o_paiju.playproc.round == 2){ //测试用
if (!havecard_inhand()){
//进入结算阶段
o_paiju.step = 6;
} else {
//新一轮出牌
cls_youle_erqiwang_paiju.new_playround(o_paiju, o_paiju.playproc.round + 1, o_paiju.playproc.maxseat);
}
return re3;
},
//获取埋牌
get_burycard: function(o_paiju){
var aryCardIDs = [];
for (var i = 0; i < o_paiju.cards.length; i++){
if (o_paiju.cards[i].playround == 0){
aryCardIDs.push(o_paiju.cards[i].id);
}
}
return cls_youle_erqiwang_arith.order_cards(o_paiju.flower, aryCardIDs);
},
//获取抠底包
get_bottom_account: function(o_paiju, msg){
msg.data.bottom = {};
msg.data.bottom.cards = o_paiju.method.get_burycard();
if (o_paiju.playproc.maxseat != o_paiju.banker){ //闲家抠底
var multiple = cls_youle_erqiwang_arith.get_bottom_multiple(o_paiju.flower, o_paiju.playproc.cards[o_paiju.playproc.maxseat]);
if (multiple > 0){
msg.data.bottom.multiple = multiple;
var get = o_paiju.method.get_grade_incard(msg.data.bottom.cards);
msg.data.bottom.grade1 = get.grade;
msg.data.bottom.grade2 = multiple * msg.data.bottom.grade1;
}
}
return msg;
},
//获取闲家捡分(不包含抠底)
get_jian_grade: function(o_paiju){
var re = {};
re.grade = 0;
re.cards = [];
for (var i = 0; i < o_paiju.cards.length; i++){
if (o_paiju.cards[i].playowner > -1 && o_paiju.cards[i].playowner != o_paiju.banker){
if (o_paiju.cards[i].score > 0){
re.grade = re.grade + o_paiju.cards[i].score;
re.cards.push(i);
}
}
}
return re;
},
//获取小局结算包,type 0正常结算 1投降结算 2解散结算
get_paiju_account: function(o_paiju, type, msg){
o_paiju.endtime = min_now();
//第一局结算扣除房卡
if (o_paiju.idx == 1 && type != 2){
var do_deduct_roomcard = function(){
youle_erqiwang.import.deduct_roomcard(o_paiju.o_desk.o_room);
}
min_ontimeout(do_deduct_roomcard, 1000);
}
if (msg.rpc != "jiesuan"){
msg.rpc = "jiesuan";
}
if (!msg.data){
msg.data = {};
}
msg.data.aset = {};
msg.data.aset.banker = o_paiju.banker;
msg.data.aset.call = o_paiju.call;
msg.data.aset.multiple = cls_youle_erqiwang_arith.get_multiple_bycall(o_paiju.call);
msg.data.aset.flower = o_paiju.flower;
var jian_grade = cls_youle_erqiwang_paiju.get_jian_grade(o_paiju);
msg.data.aset.grade = jian_grade.grade;
if (msg.data.bottom && msg.data.bottom.grade2){
msg.data.aset.grade = msg.data.aset.grade + msg.data.bottom.grade2;
}
switch (type){
case 0:
msg.data.aset.upgrade = cls_youle_erqiwang_arith.get_upgrade(o_paiju.call, msg.data.aset.grade);
if (msg.data.aset.upgrade > 0){
o_paiju.result = 0;
} else {
o_paiju.result = 1;
}
break;
case 1:
msg.data.aset.upgrade = -99;
o_paiju.result = 2;
break;
case 2:
msg.data.aset.upgrade = 0;
o_paiju.result = 3;
break;
}
msg.data.aset.seatlist = [];
var _player0 = {};
var _player1 = {};
var _player2 = {};
//获取玩家的牌
var _card0 = cls_youle_erqiwang_paiju.get_seat_cards_owner(o_paiju, 0);
var _card1 = cls_youle_erqiwang_paiju.get_seat_cards_owner(o_paiju, 1);
var _card2 = cls_youle_erqiwang_paiju.get_seat_cards_owner(o_paiju, 2);
//计算冲关或王牌
var _flower = o_paiju.flower;
if (type != 2){
_flower = -1;
}
var _chongguan0 = cls_youle_erqiwang_arith.get_chongguan(_flower, _card0);
var _chongguan1 = cls_youle_erqiwang_arith.get_chongguan(_flower, _card1);
var _chongguan2 = cls_youle_erqiwang_arith.get_chongguan(_flower, _card2);
//冲关的牌或王牌
_player0.cards = _chongguan0.cards;
_player1.cards = _chongguan1.cards;
_player2.cards = _chongguan2.cards;
//关数
_player0.chongguan = _chongguan0.count;
_player1.chongguan = _chongguan1.count;
_player2.chongguan = _chongguan2.count;
//是否作废
_player0.obsolete = 0;
_player1.obsolete = 0;
_player2.obsolete = 0;
//根据叫分判断冲关是否作废
var do_obsolete_with_call = function(player, seat){
var _dealcards = cls_youle_erqiwang_paiju.get_seat_cards_deal(o_paiju, seat);
var _dealchongguan = cls_youle_erqiwang_arith.get_chongguan(-1, _dealcards);
if (_dealchongguan.count == 0){
return;
}
var _call = null;
for (var i = 0; i < o_paiju.callproc.length; i++){
if (o_paiju.callproc[i].seat == seat){
_call = o_paiju.callproc[i].call;
break;
}
}
if (!_call){ //冲关必叫
player.obsolete = 1;
} else if (_dealchongguan.wang == 4 && _call > 60){ //四王必踢
player.obsolete = 1;
}
}
do_obsolete_with_call(_player0, 0);
do_obsolete_with_call(_player1, 1);
do_obsolete_with_call(_player2, 2);
//计算冲关得分
var _cg0 = _player0.chongguan;
var _cg1 = _player1.chongguan;
var _cg2 = _player2.chongguan;
if (_player0.obsolete){
_cg0 = 0;
}
if (_player1.obsolete){
_cg1 = 0;
}
if (_player2.obsolete){
_cg2 = 0;
}
_player0.grade_cg = _cg0 * 2 - _cg1 - _cg2;
_player1.grade_cg = _cg1 * 2 - _cg2 - _cg0;
_player2.grade_cg = _cg2 * 2 - _cg0 - _cg1;
//计算傍王得分
var _w0 = _chongguan0.wang;
var _w1 = _chongguan1.wang;
var _w2 = _chongguan2.wang;
if (_player0.obsolete){
_w0 = 0;
}
if (_player1.obsolete){
_w1 = 0;
}
if (_player2.obsolete){
_w2 = 0;
}
_player0.grade_w = _w0 * 2 - _w1 - _w2;
_player1.grade_w = _w1 * 2 - _w2 - _w0;
_player2.grade_w = _w2 * 2 - _w0 - _w1;
//计算捡分得分
var _upgrade = msg.data.aset.upgrade;
if (_upgrade == -99){
_upgrade = -2;
}
switch (o_paiju.banker){
case -1:
_player0.grade_jf = 0;
_player1.grade_jf = 0;
_player2.grade_jf = 0;
break;
case 0:
_player0.grade_jf = _upgrade * msg.data.aset.multiple * 2;
_player1.grade_jf = _upgrade * msg.data.aset.multiple * -1;
_player2.grade_jf = _player1.grade_jf;
break;
case 1:
_player0.grade_jf = _upgrade * msg.data.aset.multiple * -1;
_player1.grade_jf = _upgrade * msg.data.aset.multiple * 2;
_player2.grade_jf = _player0.grade_jf;
break;
case 2:
_player0.grade_jf = _upgrade * msg.data.aset.multiple * -1;
_player1.grade_jf = _player0.grade_jf;
_player2.grade_jf = _upgrade * msg.data.aset.multiple * 2;
break;
}
//计算本局总得分
_player0.grade = _player0.grade_cg + _player0.grade_w + _player0.grade_jf;
_player1.grade = _player1.grade_cg + _player1.grade_w + _player1.grade_jf;
_player2.grade = _player2.grade_cg + _player2.grade_w + _player2.grade_jf;
//大局累积得分
o_paiju.o_desk.seatlist[0][0] = o_paiju.o_desk.seatlist[0][0] + _player0.grade;
o_paiju.o_desk.seatlist[1][0] = o_paiju.o_desk.seatlist[1][0] + _player1.grade;
o_paiju.o_desk.seatlist[2][0] = o_paiju.o_desk.seatlist[2][0] + _player2.grade;
_player0.score = o_paiju.o_desk.seatlist[0][0];
_player1.score = o_paiju.o_desk.seatlist[1][0];
_player2.score = o_paiju.o_desk.seatlist[2][0];
//保存每局得分
o_paiju.o_desk.seatlist[0][1].push(_player0.grade);
o_paiju.o_desk.seatlist[1][1].push(_player1.grade);
o_paiju.o_desk.seatlist[2][1].push(_player2.grade);
msg.data.aset.seatlist.push(_player0);
msg.data.aset.seatlist.push(_player1);
msg.data.aset.seatlist.push(_player2);
if (o_paiju.idx >= o_paiju.o_desk.o_room.asetcount || type == 2){
msg = o_paiju.o_desk.method.get_desk_account(msg);
}
//保存结算包,供断线重连时使用
o_paiju.tmp_jiesuan_aset = msg.data.aset;
return msg;
}
}
+633
View File
@@ -0,0 +1,633 @@
# 二七王 玩法设计文档
> 本文档是二七王玩法的唯一权威规则说明。内容随讨论持续与规则设计者逐项确认、修订;文中标注"待确认"的地方是当前仍未有定论的开放问题,其余内容均已确认生效。
## 目录
1. [术语约定](#1-术语约定)
2. [牌局构成](#2-牌局构成)
3. [主牌顺序](#3-主牌顺序)
4. [开局与坐庄流程](#4-开局与坐庄流程)
5. [出牌规则](#5-出牌规则)
- [5.1 跟牌基本原则](#51-跟牌基本原则)
- [5.2 按牌型跟牌](#52-按牌型跟牌)
- [5.3 拖拉机定义与相邻关系](#53-拖拉机定义与相邻关系)
- [5.4 甩牌详细规则](#54-甩牌详细规则)
6. [捡分与扣底](#6-捡分与扣底)
- [6.1 分牌](#61-分牌)
- [6.2 捡分规则](#62-捡分规则)
- [6.3 扣底](#63-扣底)
7. [结算:子数与升级](#7-结算子数与升级)
- [7.0 四层关系总览 / 查表索引](#70-四层关系总览)
- [7.1 常规算子:叫分 → 基础子数](#71-常规算子叫分-→-基础子数未勾选爬坡时的默认规则)
- [7.2 常规算子的判定表](#72-捡分-vs-叫分庄家的过庄-小光-大光闲家的升级常规算子)
- [7.3 爬坡](#73-爬坡叫分-→-基础子数与保小光分段可选规则开房时勾选爬坡生效)
8. [算奖规则](#8-算奖规则)
- [8.1 常规算奖](#81-常规算奖)
- [8.2 亮牌规则](#82-亮牌规则)
- [8.3 傍王(可选规则)](#83-傍王可选规则)
- [8.4 算奖如何影响最终结算](#84-算奖如何影响最终结算)
9. [牌局查看模式](#9-牌局查看模式)
10. [房间设置选项](#10-房间设置选项)
11. [牌局交互提示](#11-牌局交互提示)
---
## 1. 术语约定
| 术语 | 含义 |
| --- | --- |
| 主牌 / 副牌 | 庄家选定的花色为"主牌",其余三个花色为"副牌";主牌大于副牌 |
| 固定主牌 | 不论选择哪个花色为主,大王、小王、以及所有花色的"2""7"恒为主牌 |
| 正2 / 正7 | 选定花色的"2""7",大于其余花色的"2""7" |
| 副2 / 副7 | 非选定花色的"2""7" |
| 暗牌 | 发牌时留在桌面的 8 张牌;庄家确认后可摸起查看,摸起后就不再是"暗牌",而是并入庄家手牌 |
| 底牌 | 庄家"埋牌"时从手牌中选出重新扣下的 8 张牌;"暗牌"与"底牌"是两批不同的牌,不通用、不互换 |
| 埋牌 | 庄家看到暗牌、摸起查看后,从手牌中选出 8 张重新扣下,作为"底牌" |
| 捡分 | 出牌中赢下的分牌(5、10、K)计入闲家总得分的过程 |
| 扣底 | 出完所有手牌后的最后一轮,若被闲家用主牌牌型压过庄家,则埋下的 8 张底牌也计入闲家捡分,并按牌型翻倍 |
| 拖拉机 | 同一花色(含主牌)里点数相邻的连续对子,如 K K Q Q 为"两托"(两连对) |
| 甩牌 | 首家出牌时,一次性打出多组主牌组合(单张 / 对子 / 拖拉机自由搭配);仅主牌可以甩牌,副牌禁止甩牌,只能分单张 / 对子 / 拖拉机依次打出(见第 5.4 节) |
| 毙牌 | 跟牌时手中没有本轮花色(缺门),改用主牌打出、且牌面大过本轮目前所有人的牌(见第 5.1 / 5.2 节) |
| 垫牌 | 跟牌时手中没有本轮花色(缺门)、且不用主牌毙牌,随意出一张其他副牌,不参与、不争夺本轮的出牌权(见第 5.1 节) |
| 子 | 结算时用于计算输赢筹码的单位,与"倍数"是两个独立概念(见第 7 节) |
| 常规算奖 | 局末对固定组合(三王/四王、六 2/七 2/八 2、六 7/七 7/八 7、连续对子链等)的额外奖励结算,只计入庄家的手牌结构(见第 8.1 节) |
| 傍王 | 可选房间规则:勾选后每张王额外算一奖,庄家、闲家都要算;这是在 8.1 常规算奖之外**额外叠加**的奖励,与常规算奖互不冲突(见第 8.3 节) |
| 爬坡 | 可选房间规则:改变叫分对应的基础子数与保小光分数线,未勾选时按"常规算子"结算(见第 7 节) |
---
## 2. 牌局构成
两副扑克牌共 108 张,去掉两副牌中的"3"和"4"(2 种点数 × 4 花色 × 2 副 = 16 张),剩余 **92 张**:
- 点数 5、6、7、8、9、10、J、Q、K、A、2:共 11 种点数,每种 8 张(2 副 × 4 花色)
- 大王、小王:各 2 张(每副各 1 对)
花色为黑桃 ♠、红桃 ♥、梅花 ♣、方块 ♦。
---
## 3. 主牌顺序
每局开局前庄家选择一个花色作为本局主牌,其余三个花色为副牌。
固定主牌(大王、小王、所有花色的 2 和 7)不论选择哪个花色为主,恒定属于主牌;选定花色里的其余普通牌也算主牌。
主牌从大到小排列:
| 顺序 | 牌 |
| --- | --- |
| 1 | 大王 |
| 2 | 小王 |
| 3 | 正 7(选定花色的 7) |
| 4 | 副 7(其余花色的 7) |
| 5 | 正 2(选定花色的 2) |
| 6 | 副 2(其余花色的 2) |
| 7 及以下 | 选定花色的其余普通牌:A、K、Q、J、10、9、8、6、5 |
副牌(非选定花色,且非 2、7、王)从大到小:A、K、Q、J、10、9、8、6、5。
---
## 4. 开局与坐庄流程
1. 每人摸 28 张牌,剩余 8 张扣在桌面作为"暗牌"。
2. 由暂定庄家开始,按逆时针方向依次叫分:
- 叫分范围 5 ~ 70 分,步进 5 分。
- 叫分数值代表"庄家承诺闲家捡分总数将严格低于该数值"(即闲家捡分只要达到或超过这个数值,庄家就算没达成承诺,见第 7 节);数值越低代表叫分者越有信心,因此后叫的人必须叫出比当前更低的分数才能压过前者,不能叫相同或更高的分数。
- **暂定庄家必须叫分,不能"不叫"**:叫分从暂定庄家开始,他必须报出一个 5 ~ 70 之间的具体分数,不存在"暂定庄家弃权、三家都不叫"这种情况;起始叫分确定之后,后面的玩家才可以选择"叫出更低的分数"或"不叫"。
- 若某玩家直接叫出 5 分,则无人能再压过,立即确定为庄家。
- 若一家叫分后,另外两家都选择"不叫",则该叫分玩家成为本局庄家。
3. 庄家确定后可以查看暗牌(只有庄家可见),将 8 张暗牌摸到手上,同时从手牌中选出 8 张牌重新扣下("埋牌"),作为"底牌",供最后判断"扣底"使用。
4. **若叫分为 70 分**:埋牌完成后,庄家必须立即在"投降"与"打牌"之间二选一(埋牌步骤本身不受这个选择影响,两种情况都要先正常埋牌):
- **选择投降**:不再进行下面的选主牌步骤,本局直接结束,按 7.1 节"70 分坐庄 · 投降"的固定结果结算(基础子数 1 个,庄家直接输)。
- **选择打牌**:进入下一步,正常选主牌花色、开始出牌,按 7.1 / 7.2 节"70 分坐庄 · 打牌"的完整判定流程结算。
- 叫分低于 70 分时没有这个分支,直接进入下一步。
5. 庄家选定一个花色作为本局主牌(见第 3 节)。
6. 坐庄轮换规则(决定下一局的"暂定庄家"):
- 第一局的暂定庄家为最0号座位的玩家。
- 若庄家本局获胜(庄赢),下一局暂定庄家仍为该玩家(连庄)。
- 若庄家本局失败(闲家捡分达标 / 庄家投降),下一局暂定庄家变为该玩家的下家(按逆时针顺延)。
---
## 5. 出牌规则
### 5.1 跟牌基本原则
- 每一轮由上一轮牌面最大的一方先出牌;**每一局(每一手牌)的第一轮固定由庄家先出**——此时还没有"上一轮"可以比较,只能由庄家开局。
- 首家出什么花色,其余两家必须跟出同花色的牌;若手中没有该花色的牌或数量不够(缺门),可以任选以下一种方式补齐:
- **毙牌**:改用主牌打出,且牌面要大过本轮目前所有人的牌,即可抢下本轮的出牌权;
- **垫牌**:随意出一张其他副牌,不参与、不争夺本轮出牌权。
- 首家若出主牌,其余两家必须跟出主牌,规则同上(此时缺门只能垫副牌,不存在再用主牌毙牌的情况)。
### 5.2 按牌型跟牌
| 首家牌型 | 跟牌要求 |
| --- | --- |
| 单张 | 跟同花色任意单张;不够则任意牌补齐 |
| 对子 | 必须跟同花色对子;没有对子则用两张同花色单牌;同花色不足两张则任意牌补齐 |
| 拖拉机(连续对子) | 有同花色拖拉机必须跟出拖拉机;没有拖拉机但有两对不连续的对子,也必须出这两对;都没有则优先同花色任意牌补齐 |
毙牌(缺门时改用主牌顶替首家副牌)的特殊情况:
- 若首家出副牌单张,其余玩家没有该副牌花色的,可以任意出牌(含主牌);但要压过首家,出的主牌也必须是单张(数量对应)。
- 若首家出副牌对子,其余玩家没有该副牌花色的,要压过首家,出的主牌必须也是对子(不能用任意两张主牌顶替)。
- 若首家出副牌拖拉机(N 连对),其余玩家没有该副牌花色的同长度拖拉机,要压过首家,出的主牌必须也是同样连对组数(N 连对)的主拖拉机,总张数完全对应;单张主牌或零散的主对子都不能顶替拖拉机。
### 5.3 拖拉机定义与相邻关系
同一花色内点数相邻的两个(或以上)对子构成"拖拉机",如 K K Q Q 为两连对(两托),Q Q J J K K 为三连对(三托)。7 不可以和 6、8 组成拖拉机;6 和 8 可以组成拖拉机。
拖拉机的相邻顺序(从大到小):
```
大王 - 小王 - 正7 - 副7 - 正2 - 副2 - A - K - Q - J - 10 - 9 - 8 - 6 - 5
```
### 5.4 甩牌详细规则
#### 5.4.1 基础定义
**甩牌**:当你是本轮首家出牌人时,一次性打出多组主牌混合牌型(单张、对子、拖拉机自由组合),一轮打完多张主牌,无需分多轮依次打出。
两条硬性基础禁令:
1. **仅主牌允许甩牌,所有副牌完全禁止甩牌**:无论外面是否剩余该花色副牌,副牌只能分开单出、出对子、出拖拉机,不能一次性打包甩出。
2. **只有本轮先手才能执行甩牌**:若他人先出牌,你只能跟牌、垫牌、用主牌毙牌,不具备甩牌资格。
#### 5.4.2 生效条件(最大性原则)与报无主
想要成功甩牌,必须同时满足:**剩余两名对手手中,不存在任何一张、一对、一组拖拉机能压制你甩出的整套主牌组合**。简单说,全场所有更大的主牌、更大的主对子、更长的主拖拉机必须全部在你手中,两家闲家没有任何牌型能盖过你的甩牌组合。
**合法性由服务端在甩牌提交时自动判定**:服务端掌握全部玩家的手牌,甩牌一旦提交,立即检查另外两名玩家手中是否"持有"(不要求对方真正打出)能压过这套组合任意一部分的主牌;只要有人持有,就直接判定为甩错(见 5.4.5),不需要、也不会等到对方实际出牌反制才判定。
"报无主"是给玩家参考的辅助信息,**不是合法性判定的依据**(合法性判定始终由服务端按全部手牌精确计算):对局中非最后一轮,任意玩家手里没有主牌了,必须口头报无主,方便其他玩家在决定要不要甩牌前,大致判断风险:
- **另外两家都报了无主**:全场只剩你自己持有主牌,甩牌必然合法,无需记牌。
- **另外两家只有一家报了无主、还有一家没报**:那一家仍持有主牌,你若甩牌需要自行记牌估算风险;但即使判断失误,最终是否算甩错仍由服务端精确判定,不取决于你的记牌是否正确。
#### 5.4.3 甩牌可包含的牌型组合
甩牌仅限本局主牌(固定主牌 + 选定主花色的普通牌),内部可自由混搭任意数量、任意种类牌型:
- **纯单张**:大王、小王、正 7、副 7、正 2、副 2、主花色单牌混合;
- **纯对子**:大王对 / 小王对(各自成对,大王与小王点数不同,不能混成一对)、正七对、副七对、正二对、主花色数字对子;
- **主拖拉机**:大小王拖拉机、七七连对、二二连对、主花色数字连对;
- **混合搭配**:单张 + 对子 + 多连拖拉机一同甩出,无组合数量限制。
#### 5.4.4 其余两家应对甩牌的强制跟牌规则
甩牌一旦被判定合法(见 5.4.2:服务端已确认两名闲家都不持有能盖压这套组合任何一部分的主牌),闲家就不可能再用主牌把它压下去——"能不能盖压"在甩牌提交的那一刻就已经由服务端裁定完毕,不会走到"闲家出牌盖压"这一步。
甩牌本身可能是单张、对子、多个不同长度拖拉机自由混搭的复杂组合(见 5.4.3)。闲家跟牌时,**要把这套甩牌拆解成一个个独立的分量,逐个分量分别匹配**,而不是把整套甩牌当成一个笼统的整体来对待:
1. **每一组拖拉机分量**:闲家手中若有同等连对数的主拖拉机,必须优先拆出来跟这一组;没有同等连对数的拖拉机,退而求其次用同等张数的主对子顶替;主对子也不够,才能拆主单张顶替。甩牌里若有多组不同长度的拖拉机,逐组分别按此规则处理。
2. **每一组单独的对子分量**:闲家手中若有主对子,必须优先拆出来跟这一组;没有主对子,才能拆主单张顶替。
3. **每一组单独的单张分量**:用主单张顶替即可。
4. **闲家手中的主牌拆解完仍不够覆盖甩牌剩余的分量**:不够的那部分只能随意垫副牌。
5. **闲家完全没有主牌**:只能随意垫任意副牌,本轮无法压制你的甩牌,本轮出牌权归你。
无论怎么拆解跟牌,都不影响这套甩牌本身的合法性——合法性在提交时已经由服务端按 5.4.2 判定完毕,闲家的跟牌动作只是"凑够同等张数、同等结构"的手牌垫上去,不可能真正反超压制。
#### 5.4.5 甩错判定与惩罚规则
**甩错判定**:甩牌提交时,服务端按全部玩家的真实手牌自动检查——只要两名对手任意一人**持有**(不要求已经打出)能盖压你甩牌内任意一组牌型的主牌,本次甩牌立即判定为甩错,对方是否愿意打出来压制不影响判定结果。
**统一惩罚标准**:
- 本次甩牌全部收回手牌;
- 本轮仅强制打出你甩出组合里最小的那一张牌;
- 本轮失去再次甩牌、一次性多出牌的资格,剩余手牌只能分轮正常打出;
- 线下娱乐、线上棋牌平台均统一执行该惩罚,不额外扣分。
甩错判罚后,本轮实际只算打出了那一张最小的单张主牌,其余两家按 5.2 节的"单张"跟牌规则正常应对即可,不再涉及甩牌相关规则。
---
## 6. 捡分与扣底
### 6.1 分牌
牌面 5 记 5 分,牌面 10 记 10 分,牌面 K 记 10 分,其余牌不计分。
### 6.2 捡分规则
每一轮出完牌后比较牌面大小:
- 若闲家最大,则本轮牌面上的分牌全部计入闲家捡分(两个闲家谁大不影响,都算闲家捡到)。
- 若庄家最大,则本轮牌面上的分牌作废——不计入闲家捡分总数,也不属于庄家的个人分数(庄家本身没有"捡分"这个概念,庄家的输赢只看闲家捡分总数是否达到叫分,见第 7 节)。
### 6.3 扣底
所有手牌出完后,若最后一轮闲家**用主牌**(不论单张、对子、拖拉机)**牌面压过庄家**、赢下最后一轮,才视为"扣底"(若闲家赢了最后一轮但用的不是主牌,则不算扣底)。触发后,庄家埋下的 8 张底牌翻开,若有分则计入闲家捡分。
底牌的分要不要翻倍、翻几倍,取决于闲家扣底所用的那手牌(赢下最后一轮的牌)本身是什么牌型:
| 闲家扣底所用牌型 | 倍数 |
| --- | --- |
| 单张主牌 | 不翻倍,按原始分值计入 |
| 主对子(一对主牌) | 2 倍 |
| 两连对拖拉机 | 4 倍 |
| 三连对拖拉机 | 6 倍 |
| N 连对拖拉机 | 2 × N 倍,以此类推递增 |
若最后一轮闲家是用第 5.4 节的**甩牌**(单张 + 对子 + 拖拉机混合的组合)扣底,取这套组合里**最高规格牌型**对应的倍数计算,其余较低规格的部分不额外叠加。例如甩出"单张 + 一对 + 两连对拖拉机"并以此扣底,最高规格是两连对拖拉机,整手按 4 倍计算。
> **重要区分**:扣底翻的是**闲家的捡分得分(grade)**本身,属于"算分"阶段;这与第 7 节里"大光 ×3""小光 ×2""升 N 级 ×N"这些子数倍率是完全不同的两件事——那些乘的是**子数**,属于"算子"阶段。两者的先后顺序是:扣底先把底牌分(可能翻倍)计入闲家的捡分总数,这个最终捡分总数再拿去跟庄家叫分比较,判定过庄 / 小光 / 大光 / 闲家赢(升级),最后才由判定结果决定子数乘几倍。
---
## 7. 结算:子数与升级
叫分与结算涉及两套并存的数值:**倍数**(用于计算"捡分得分" grade)与**子数**(用于计算最终筹码输赢),两者是独立概念,不能混用。
> **重要区分**:本节里提到的倍率(大光 ×3、小光 ×2、过庄 ×1、升 N 级 ×N),乘的都是**子数**(最终结算给玩家的筹码单位),不是对局过程中闲家捡到的分数(捡分得分)本身。捡分得分只用来判定庄家的过庄 / 小光 / 大光,或闲家的升级级数,一旦等级判定完成,实际相乘的对象是这一局最终要结算的子数。
> **算子模式的选择**:7.1 + 7.2 描述的"常规算子"是默认结算方式;开房时勾选"爬坡"后,才改用 7.3 的子数梯度和保小光分段;未勾选"爬坡"时,一律按常规算子(7.1 + 7.2)结算,两套规则不会同时生效。
### 7.0 四层关系总览
结算分四层,前三层是"算子"(本节),第四层是"算奖"(第 8 节),一层的输出是下一层的输入:
1. **叫分 → 基础子数**:叫分档位本身决定这一局的基础子数(见 7.1 / 7.3 的对照表),叫分越低(庄家承诺越苛刻),基础子数越高。
2. **捡分 vs 叫分 → 判定结果**:局末闲家的捡分总数(含扣底,见 6.3 节)与庄家叫分作比较,只会落入互斥的两个分支之一:
- **闲家没达到叫分**(庄家赢)→ 结果记在**庄家**名下,分三级:**过庄 / 小光 / 大光**。
- **闲家达到或超过叫分**(闲家赢,庄家倒庄)→ 结果记在**闲家**名下,只有一个概念:**升级**(第几级)。
- **大光 / 小光和升级是两套完全独立的命名体系,分别只属于庄家一侧和闲家一侧,不能混用或类比**:庄家赢的时候没有"升级",闲家赢的时候也没有"大光 / 小光"。
3. **判定结果 → 子数倍率**:过庄 / 小光 / 大光 / 升级第几级,各自对应一个子数倍率,乘上第 1 层的基础子数,得到这一局的**子数结算结果 `X`**(前三层,即"算子",到此为止)。`X` 是庄家与**每一个**闲家之间"一对一"结算的基础金额:庄家赢(过庄 / 小光 / 大光)时,两个闲家各输给庄家 `X` 子;闲家赢(升级)时,庄家各输给两个闲家 `X` 子。
4. **算奖 → 结算倍率**:第 8 节算出的"奖数"(8.1 常规算奖 + 8.2 傍王,各玩家分别求和)**是"持有奖数的这个人"从其余两名玩家那里各自多收一笔钱的独立结算线,作用对象是这个人本身,不是整场结算的统一系数**——某玩家 P 这一局总共有 `N_P` 奖,则**另外两名玩家(不分庄闲)都要各自额外付给 P:`X × N_P` 子**。三名玩家可能同时都持有各自的奖数,会产生最多 3 条互相独立的额外支付线,与第 1~3 层的基础输赢结算加总,才是每个人这一局的最终盈亏。算奖的计算过程本身不受叫分、过庄 / 小光 / 大光、升级影响(只看手牌结构),详见 8.3 节的完整举例。
> **例外**:70 分坐庄选择投降时不走第 1~3 层模型——直接固定基础子数 1 个、庄家输掉,不比较捡分、不判定过庄 / 小光 / 大光,见 7.1 末尾说明;但第 4 层算奖仍然照常叠加(庄家埋牌后的手牌结构依然存在,见 8.4 节)。
**查表索引**:本节按叫分档位给出了完整判定表,位置如下:
| 叫分 | 常规算子(未勾选"爬坡") | 爬坡(勾选"爬坡") |
| --- | --- | --- |
| 70 分 | 7.2.1 末尾"70 分坐庄" | 与常规算子一致,见 7.3.3 末尾说明 |
| 65 / 60 / 55 / 50 / 45 分 | 7.2.1 对应表格 | 与常规算子数值一致(45 分子数不同),见 7.3.3 末尾说明 |
| 40 / 35 / 30 / 25 / 20 / 15 / 10 分 | 7.2.1"40 分及以下坐庄"通用规则 | 7.3.3 对应表格(子数、`Q` 值均与常规算子不同) |
| 5 分 | 7.2.1"40 分及以下坐庄"内的"5 分坐庄没有小光"说明 | 7.3.3"5 分坐庄"(没有小光) |
### 7.1 常规算子:叫分 → 基础子数(未勾选"爬坡"时的默认规则)
叫分范围是 5 ~ 70 分(见第 4 节),常规算子下完整的"叫分 → 基础子数"对照:
| 叫分 | 65 分 | 60 分 | 55 分 | 50 分及以下(50 / 45 / 40 / 35 / 30 / 25 / 20 / 15 / 10 / 5) |
| --- | --- | --- | --- | --- |
| 基础子数 | 2 个子 | 3 个子 | 4 个子 | 统一 6 个子,不再随叫分继续增加 |
即叫分从 65 分往下每降 5 分子数依次是 2 → 3 → 4,但**到 50 分封顶**:50 分及以下所有档位(50、45、40、35、30、25、20、15、10、5)基础子数都固定是 6 个子,不会像爬坡那样继续往上加(爬坡的区别见 7.3)。
70 分叫庄是特例,基础子数不接着上面的阶梯往下走,而是在"投降"和"打牌"两条路径下分别取值:
- **选择投降**:基础子数按 **1 个**算,庄家直接输 1 个子(此时局面还没有真正出牌捡分,不经过 7.2 的大光 / 小光 / 升级判定,是一次性的固定结算)。
- **选择打牌**:基础子数按 **2 个**算(与 65 分相同),之后**照样按 7.2 节的方法,用捡分总数判定大光 / 小光 / 过庄 / 升级**,不是不细分——完整判定表见 7.2.1。
70 分叫庄时,发牌时桌面留下的那 8 张暗牌(见第 4 节;不是埋牌埋下的底牌)需要向两个闲家亮出来 3 秒,投降和打牌两种情况都适用。
### 7.2 捡分 vs 叫分:庄家的过庄 / 小光 / 大光,闲家的升级(常规算子)
#### 7.2.0 判定方法(对任意叫分档位通用)
**大光的判定条件是固定的、不随叫分或算子模式变化**:闲家捡分 `grade == 0`(一分未捡到)就是大光,常规算子和爬坡完全一样。会随叫分变化的,是**小光 / 过庄**之间的分界线——常规算子下这条分界线**固定为 40 分**,不随叫分变化(这是与爬坡最核心的区别,爬坡的分界线 `Q` 会随叫分变化,见 7.3.2)。设叫分为 `call`,闲家捡分为 `grade`,按以下顺序依次判断(**必须先判断第 1 步,即是否达标,再判断大光 / 小光 / 过庄**,顺序不能颠倒):
1. **先判断是否达标**:若 `grade ≥ call`,闲家赢,判为**升级**——先升 1 级,此后每再多捡 40 分再升 1 级;子数倍率就是当前的级数本身:升 1 级 ×1、升 2 级 ×2、升 3 级 ×3……逐级 **+1**,不是逐级翻倍。
2. **未达标(`grade < call`)时,再看捡分具体落在哪个区间**:
- `grade == 0`(一分未捡到)→ **庄家:大光**,子数倍率 ×3。
- `0 < grade < 40`(捡了一点,但没满 40 分)→ **庄家:小光**,子数倍率 ×2。
- `40 ≤ grade < call`(捡满 40 分,但仍没达标)→ **庄家:过庄**,子数倍率 ×1(即基础子数,不翻倍)。
之所以要先判断"是否达标":当叫分本身 ≤ 40 时(比如叫 35 分坐庄),闲家只要捡到 35 分就已经达标升级了,根本不可能出现"捡到 40 分却还没达标"这种情况——"过庄"这一档这时候是空的,直接从"小光"跳到"升级"(完整例子见 7.2.1"40 分及以下坐庄")。
**大光 / 小光只描述庄家(闲家没达标时,庄家赢得有多彻底);升级只描述闲家(闲家达标之后,赢得有多彻底),两套命名不通用、不能互换。**
#### 7.2.1 常规算子完整判定表
按上面的方法,叫分 > 40 分的档位(65 / 60 / 55 / 50 / 45)"过庄"区间是存在的;叫分 ≤ 40 分的档位(40 / 35 / 30 / 25 / 20 / 15 / 10 / 5)"过庄"区间不存在,闲家捡分只要达到叫分就直接升级。
**65 分坐庄(基础 2 个子)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 6 个子 |
| 1 ~ 39 分 | 庄家:小光 | ×2 | 4 个子 |
| 40 ~ 64 分 | 庄家:过庄 | ×1 | 2 个子 |
| 65 ~ 104 分 | 闲家:升 1 级 | ×1 | 2 个子 |
| 105 ~ 144 分 | 闲家:升 2 级 | ×2 | 4 个子 |
| 145 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +2 个子 |
**60 分坐庄(基础 3 个子)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 9 个子 |
| 1 ~ 39 分 | 庄家:小光 | ×2 | 6 个子 |
| 40 ~ 59 分 | 庄家:过庄 | ×1 | 3 个子 |
| 60 ~ 99 分 | 闲家:升 1 级 | ×1 | 3 个子 |
| 100 ~ 139 分 | 闲家:升 2 级 | ×2 | 6 个子 |
| 140 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +3 个子 |
**55 分坐庄(基础 4 个子)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 12 个子 |
| 1 ~ 39 分 | 庄家:小光 | ×2 | 8 个子 |
| 40 ~ 54 分 | 庄家:过庄 | ×1 | 4 个子 |
| 55 ~ 94 分 | 闲家:升 1 级 | ×1 | 4 个子 |
| 95 ~ 134 分 | 闲家:升 2 级 | ×2 | 8 个子 |
| 135 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +4 个子 |
**50 分坐庄(基础 6 个子)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 18 个子 |
| 1 ~ 39 分 | 庄家:小光 | ×2 | 12 个子 |
| 40 ~ 49 分 | 庄家:过庄 | ×1 | 6 个子 |
| 50 ~ 89 分 | 闲家:升 1 级 | ×1 | 6 个子 |
| 90 ~ 129 分 | 闲家:升 2 级 | ×2 | 12 个子 |
| 130 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 |
**45 分坐庄(基础 6 个子——从这一档开始进入"50 分及以下统一 6 个子"的范围)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 18 个子 |
| 1 ~ 39 分 | 庄家:小光 | ×2 | 12 个子 |
| 40 ~ 44 分 | 庄家:过庄 | ×1 | 6 个子 |
| 45 ~ 84 分 | 闲家:升 1 级 | ×1 | 6 个子 |
| 85 ~ 124 分 | 闲家:升 2 级 | ×2 | 12 个子 |
| 125 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 |
**40 分及以下坐庄(40 / 35 / 30 / 25 / 20 / 15 / 10 / 5,基础都是 6 个子;此时 `call ≤ 40`,没有"过庄"这一档)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 18 个子 |
| 1 分 ~(叫分 − 1)分 | 庄家:小光 | ×2 | 12 个子 |
| 达到叫分 | 闲家:升 1 级 | ×1 | 6 个子 |
| 达到叫分 + 40 分 | 闲家:升 2 级 | ×2 | 12 个子 |
| 此后每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +6 个子 |
例如叫 10 分坐庄(基础 6 个子):0 分为大光(18 子);1 ~ 9 分为小光(12 子);捡到 10 分起升 1 级(6 子);捡到 50 分起升 2 级(12 子);之后每再多 40 分再升 1 级(每级再 +6 子)。
> **5 分坐庄没有"小光"**:分牌只有 5 分、10 分两种面值(见 6.1 节),闲家捡分永远是 5 的倍数。上面"1 分 ~(叫分 − 1)分为小光"这一行,在叫分为 5 分时对应区间是 `1 ~ 4 分`,里面不存在任何 5 的倍数,是空区间。所以叫 5 分坐庄时,闲家捡分只有 0(大光)和达到 5 分及以上(直接升 1 级,之后每再多 40 分再升 1 级)两种结果,没有小光这一档;这是"叫分 ≤ 40 分"这组档位里唯一的特例,其余档位(40/35/30/25/20/15/10)小光区间都至少包含一个 5 的倍数,正常存在。
**70 分坐庄**
- **投降**:基础子数按 1 个算,庄家直接输 1 个子,不经过大光 / 小光 / 过庄 / 升级判定(还没出牌捡分,直接放弃)。
- **打牌**:基础子数按 2 个算,之后按 7.2.0 的方法正常判定,`Q` 同样固定为 40 分——判定表和 65 分坐庄完全一致,只是把"基础 2 个子"原样代入(数值恰好和 65 分相同):
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 6 个子 |
| 1 ~ 39 分 | 庄家:小光 | ×2 | 4 个子 |
| 40 ~ 69 分 | 庄家:过庄 | ×1 | 2 个子 |
| 70 ~ 109 分 | 闲家:升 1 级 | ×1 | 2 个子 |
| 110 ~ 149 分 | 闲家:升 2 级 | ×2 | 4 个子 |
| 150 分起,每再多 40 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +2 个子 |
### 7.3 爬坡:叫分 → 基础子数与保小光分段(可选规则,开房时勾选"爬坡"生效)
#### 7.3.1 基础子数:叫分 → 子数的完整对照
叫分从 65 分(2 个子)开始,每叫低 5 分加 1 个子,直到 50 分(6 个子)——这一段与常规算子的四档数值完全相同;50 分以下,继续按每叫低 5 分加 1 个子延伸,一直到叫分范围的下限 5 分:
| 叫分 | 65 | 60 | 55 | 50 | 45 | 40 | 35 | 30 | 25 | 20 | 15 | 10 | 5 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 基础子数 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
#### 7.3.2 保小光分段:叫分 → 小光 / 过庄分界线(Q 值)
判定方法与 7.2.0 完全相同,只是把固定的 `Q = 40` 换成按叫分区间取值的 `Q`;**大光的判定条件不受 `Q` 影响,永远是 `grade == 0`**,常规算子和爬坡完全一样——`Q` 只决定"小光"和"过庄"之间的分界线:
| 叫分区间 | 小光 / 过庄的分界线(Q) |
| --- | --- |
| ≥ 45 分(含 65 / 60 / 55 / 50 / 45) | 40 分 |
| 40 / 35 分 | 20 分 |
| 30 / 25 分 | 15 分 |
| 20 / 15 分 | 10 分 |
| 10 分 | 5 分 |
| 5 分 | 5 分(升级级距仍是 5,只是"小光"这一档因为区间为空而不存在,见下方说明) |
`≥ 45 分`这一档的 `Q = 40`,与 7.2 节常规算子沿用的分界线数值相同,只是子数改按 7.3.1 的公式取值(65/60/55/50 与常规算子数值一致,45 分是爬坡独有的新增档位)。
**5 分档没有"小光"**:分牌只有牌面 5(5 分)和牌面 10 / K(10 分)两种面值(见 6.1 节),闲家的捡分总数(`grade`)因此永远是 5 的倍数(0、5、10、15…),不可能出现 1 ~ 4 这样的中间值。当叫分是 5 分时,"小光"原本对应的区间是 `0 < grade < 5`,这个开区间里不存在任何 5 的倍数,也就是说这个区间**必然是空的**——闲家捡分要么是 0(大光),要么一旦捡到任何分牌(最少 5 分)就已经达到叫分、直接升级,中间不存在"捡到了一点、但没达标"的"小光"状态。所以 5 分档只有**大光**和**升级**两种结果,没有"小光"这一档;但 `Q` 值本身仍然是 5(与 10 分档一样),只是在"小光 / 过庄分界线"这个用途上区间恰好收窄为空,`Q = 5` 仍然继续作为"升级级距"生效(见 7.3.3 的 5 分坐庄判定表)。
**升级级距的通用规则**:不管哪个叫分档位,闲家赢了之后,"超过叫分多少分算升一级",用的分数差**就是该档位的 `Q` 值**——即上表里每一档的 `Q` 值,既是"小光 / 过庄"的分界线,也是"升级"的级距单位,两者共用同一个数字。例如叫分 40 分时 `Q = 20`:闲家捡分达到 40 分先升 1 级,之后每再多捡 20 分(60、80、100…)就再升一级。
#### 7.3.3 各分段的完整判定表(按 7.2.0 的方法代入对应 `call`、`Q`、基础子数)
**40 分坐庄(基础 8 个子,Q = 20)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 24 个子 |
| 1 ~ 19 分 | 庄家:小光 | ×2 | 16 个子 |
| 20 ~ 39 分 | 庄家:过庄 | ×1 | 8 个子 |
| 40 ~ 59 分 | 闲家:升 1 级 | ×1 | 8 个子 |
| 60 ~ 79 分 | 闲家:升 2 级 | ×2 | 16 个子 |
| 80 分起,每再多 20 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +8 个子 |
**35 分坐庄(基础 9 个子,Q = 20)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 27 个子 |
| 1 ~ 19 分 | 庄家:小光 | ×2 | 18 个子 |
| 20 ~ 34 分 | 庄家:过庄 | ×1 | 9 个子 |
| 35 ~ 54 分 | 闲家:升 1 级 | ×1 | 9 个子 |
| 55 ~ 74 分 | 闲家:升 2 级 | ×2 | 18 个子 |
| 75 分起,每再多 20 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +9 个子 |
**30 分坐庄(基础 10 个子,Q = 15)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 30 个子 |
| 1 ~ 14 分 | 庄家:小光 | ×2 | 20 个子 |
| 15 ~ 29 分 | 庄家:过庄 | ×1 | 10 个子 |
| 30 ~ 44 分 | 闲家:升 1 级 | ×1 | 10 个子 |
| 45 ~ 59 分 | 闲家:升 2 级 | ×2 | 20 个子 |
| 60 分起,每再多 15 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +10 个子 |
**25 分坐庄(基础 11 个子,Q = 15)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 33 个子 |
| 1 ~ 14 分 | 庄家:小光 | ×2 | 22 个子 |
| 15 ~ 24 分 | 庄家:过庄 | ×1 | 11 个子 |
| 25 ~ 39 分 | 闲家:升 1 级 | ×1 | 11 个子 |
| 40 ~ 54 分 | 闲家:升 2 级 | ×2 | 22 个子 |
| 55 分起,每再多 15 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +11 个子 |
**20 分坐庄(基础 12 个子,Q = 10)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 36 个子 |
| 1 ~ 9 分 | 庄家:小光 | ×2 | 24 个子 |
| 10 ~ 19 分 | 庄家:过庄 | ×1 | 12 个子 |
| 20 ~ 29 分 | 闲家:升 1 级 | ×1 | 12 个子 |
| 30 ~ 39 分 | 闲家:升 2 级 | ×2 | 24 个子 |
| 40 分起,每再多 10 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +12 个子 |
**15 分坐庄(基础 13 个子,Q = 10)**
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 39 个子 |
| 1 ~ 9 分 | 庄家:小光 | ×2 | 26 个子 |
| 10 ~ 14 分 | 庄家:过庄 | ×1 | 13 个子 |
| 15 ~ 24 分 | 闲家:升 1 级 | ×1 | 13 个子 |
| 25 ~ 34 分 | 闲家:升 2 级 | ×2 | 26 个子 |
| 35 分起,每再多 10 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +13 个子 |
**10 分坐庄(基础 14 个子,Q = 5——这一档同样没有"小光")**
分牌只有 5 分、10 分两种面值,捡分永远是 5 的倍数。`Q = 5` 时,"小光"对应的区间 `0 < grade < 5` 里不存在任何 5 的倍数,同样是空区间:闲家捡分不可能停在 1 ~ 4 分,只要捡到分就已经是 5 分(直接落入"过庄"区间)。
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 42 个子 |
| 5 ~ 9 分 | 庄家:过庄(没有小光这一档) | ×1 | 14 个子 |
| 10 ~ 14 分 | 闲家:升 1 级 | ×1 | 14 个子 |
| 15 ~ 19 分 | 闲家:升 2 级 | ×2 | 28 个子 |
| 20 分起,每再多 5 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +14 个子 |
**5 分坐庄(基础 15 个子,没有"小光",见 7.3.2 的说明)**
5 分是叫分范围的下限,捡分只能是 5 的倍数:一旦捡到任何分牌就已经是 5 分,等于直接达标(`grade ≥ call = 5`)。所以"小光"和"过庄"这两个区间都不存在,闲家捡分只有 0(大光)和达到 5 分及以上(直接升 1 级)两种结果。
| 闲家捡分 | 判定 | 倍率 | 最终子数 |
| --- | --- | --- | --- |
| 0 分 | 庄家:大光 | ×3 | 45 个子 |
| 5 ~ 9 分 | 闲家:升 1 级(没有小光、过庄这两档) | ×1 | 15 个子 |
| 10 ~ 14 分 | 闲家:升 2 级 | ×2 | 30 个子 |
| 15 分起,每再多 5 分 | 闲家:再升 1 级 | 再 +1 | 逐级 +15 个子 |
`≥ 45` 分档(65 / 60 / 55 / 50 / 45)判定逻辑与 7.2.1 完全相同(Q = 40),只是 45 分档基础子数按 7.3.1 取 7 个子,其余数值套用 7.2.1 对应叫分的表即可,此处不重复列出。
70 分叫庄时与常规算子规则一致(见 7.2.1 末尾"70 分坐庄":投降固定基础子数 1 个,打牌基础子数 2 个并正常套用大光 / 小光 / 过庄 / 升级判定),不适用本节的保小光分段。
---
## 8. 算奖规则
算奖在**每个小局结算时**计算(与第 7 节的子数结算属于同一次小局结算,不是等大局所有局数打完才统一算,也不是在出牌过程中途算)。8.1 常规算奖只计入庄家;8.3 傍王是额外叠加的独立奖励,庄家、闲家都要算——两者的计入范围不同,注意区分。
**8.1 常规算奖只认庄家的身份,不看闲家的手牌是否达标**:即使某位闲家的手牌结构凑巧也满足 8.1 表里的条件(比如手里也有 3 张王),这份常规算奖也**不会**计入这位闲家——常规算奖从规则上就只针对庄家一人。闲家想要获得算奖,只能靠 8.3 傍王(且必须房间勾选了这条可选规则)。
**算奖依据的手牌快照时间点**:不管是 8.1(只看庄家)还是 8.3 傍王(庄闲都算),都按**静态初始手牌**计算——庄家是埋牌完成后的那一刻手牌,闲家是发牌完成后的那 28 张手牌;这个快照全程固定,不随之后的出牌、被吃、被打出而改变。
**算奖(8.1 / 8.3)与亮牌(8.2)是两个互不干涉的独立概念**:算奖是结算阶段"该给多少额外结算分"的规则;亮牌只是出牌开始前"要不要向对手公开一部分手牌统计信息"的展示规则。两者各自有自己的触发条件,只是恰好都以庄家埋牌后的手牌结构为依据,因此部分门槛数值相同——但达成亮牌的条件不代表一定触发算奖,反之亦然,具体对照见 8.2 末尾的说明。
### 8.1 常规算奖
| 组合 | 奖数 |
| --- | --- |
| 三个王 | 1 奖 |
| 四个王 | 3 奖 |
| 六个 7 | 1 奖 |
| 七个 7 | 2 奖 |
| 八个 7 | 3 奖 |
| 六个 2 | 1 奖 |
| 七个 2 | 2 奖 |
| 八个 2 | 3 奖 |
三王 / 四王在拥有"正 7"及以后连续对子的情况下,每多一对连续对子再加 1 奖,连续顺序为:
```
正7 - 副7 - 正2 - 副2 - A - K - Q - J - 10 - 9 - 8 - 6 - 5
```
其中从 A 到 5(即 A、K、Q、J、10、9、8、6、5 这一段普通点数)都必须是选定花色的主牌,才能计入连续对子——非主花色的普通对子不算数,链条到这里就断了。
### 8.2 亮牌规则
**亮牌与算奖(8.1 / 8.3)是两个独立概念,互不干涉**:亮牌只是"要不要向对手公开一部分手牌统计信息"的展示规则,本身不产生任何奖数、不影响结算;算奖该怎么算、算多少,完全按 8.1 / 8.3 的规则来,不因为触发了亮牌而增加或减少。
**亮牌要求**(仅针对庄家):庄家埋牌后,若剩余手牌满足以下任一条件,出牌开始需要向两个闲家亮出对应的统计信息——**只亮数量 / 结构,不亮具体是哪几张牌**:
| 触发条件 | 亮出的信息 |
| --- | --- |
| 固定主牌(王 + 2 + 7)总数 ≥ 10 张 | 主牌总数、其中的对子数、组成的拖拉机数(如"x 张主,x 对主带 x 拖") |
| 王 ≥ 3 张 | 王的数量(如"x 张王") |
| 7 ≥ 6 张 | 7 的数量(如"x 张 7") |
| 2 ≥ 6 张 | 2 的数量(如"x 张 2") |
同时满足多个条件时,对应的信息都要组合展示。若房间勾选了"不查牌"模式,则闲家看不到这些信息。
**亮牌门槛与 8.1 算奖起点的数值巧合,不代表两者是同一条规则**:王 ≥ 3 张 / 7 ≥ 6 张 / 2 ≥ 6 张这三个门槛,数值上恰好分别与 8.1 表里"三个王""六个 7""六个 2"的算奖起点相同,达到时会同时触发"亮牌"和"算奖"这两件独立的事——但它们是各自独立判定后凑巧同时发生,不是"亮牌导致算奖"或者"算奖导致亮牌"。这一点在"固定主牌 ≥ 10 张"这一档表现得最清楚:它只在亮牌这条规则里有定义、会触发亮牌,但 8.1 的算奖表里根本没有为这一档定义奖数,所以**不触发任何算奖**——如果亮牌和算奖是同一回事,这里就该矛盾;现在不矛盾,正说明两者互不干涉。
### 8.3 傍王(可选规则)
开房时勾选"傍王"后,每张王额外算一奖:这个奖是**庄家、闲家都要各自计算**(每个人按自己手里的王算,不限庄家)。
**傍王奖是在 8.1 常规算奖之外额外叠加的一份奖励,与常规算奖互不冲突、不互相替代**:庄家照常按 8.1 的规则算常规算奖(只计庄家),同时如果勾选了傍王,庄家、闲家还要**各自另外**再按自己手里的王数算一份傍王奖;两份奖励分开计算,最后相加。
### 8.4 算奖如何影响最终结算
**算奖是"持有奖数的那个人"从其余两名玩家那里各自多收一笔钱的独立结算线,不是把第 7 节算出的输赢结果整体重新乘一遍**:设第 7 节前三层算出的结算子数为 `X`(庄家与每个闲家之间"一对一"结算的基础金额,见 7.0 第 3 层),玩家 P 这一局总共积累了 `N_P` 奖(8.1 常规算奖 + 8.3 傍王,各自求和后相加;只有庄家可能有 8.1 的分量,8.3 傍王则庄闲都可能有):
- 若 `N_P > 0`,**另外两名玩家(不分是庄家还是闲家)都要各自额外付给 P:`X × N_P` 子**;
- 若 `N_P = 0`,P 没有额外收入。
三名玩家可能同时都持有各自的奖数,这时会产生最多 3 条互相独立、方向不同的额外支付线,互不冲突、按各自的 `N` 值同时生效,最后与第 1~3 层的基础输赢结算加总,才是每个人这一局的最终盈亏。
**举例**(`X = 6` 个子,庄家为 A,闲家为 B、C):
- 若庄家 A 本局有 1 奖(`N_A = 1`):B、C 各自额外付给 A `6 × 1 = 6` 子。
- 若闲家 B 同时勾选了"傍王"、本局有 2 奖(`N_B = 2`):A、C 各自额外付给 B `6 × 2 = 12` 子。
- 这两条支付线同时成立、互不影响:A 从 B、C 各多收 6 子,B 从 A、C 各多收 12 子——包括"闲家 C 要付给闲家 B"这样的支付,即使 B、C 之间在第 1~3 层的基础输赢结算里并不直接结算(基础结算只发生在"庄家 vs 单个闲家"之间),算奖这一层是单独叠加在所有玩家两两之间的。
以下三点已确认:
- **庄家倒庄(闲家达标升级)时,8.1 常规算奖仍然生效**:算奖只看庄家埋牌后的手牌结构,与本局谁输谁赢无关。
- **70 分投降时,仍叠加算奖倍率**:投降只是跳过了第 1~3 层的叫分 / 捡分判定,庄家埋牌后的手牌结构依然存在,8.1(以及勾选了傍王时的 8.3)照常计算并叠加。
- **算奖对象的身份限制会带进 `N` 的计算**:8.1 只算庄家、8.3 傍王勾选后才不分庄闲(见第 8 节开头说明),把这名玩家实际适用的分量求和,才是他自己的 `N`;"其余两人向他支付 `X × N`"这条支付规则本身不因身份而改变,但闲家的 `N` 从一开始就不可能包含 8.1 的分量。
---
## 9. 牌局查看模式
- **可查牌模式**:
- 当前牌局中,所有玩家已经打出的牌,任何时候都可以查看。
- 一旦有玩家的主牌全部打空(即 5.4.2 节所说的"报无主"状态;"主牌"指大王、小王、所有花色的 2、所有花色的 7,以及主花色的其余普通牌,见第 3 节),系统就为**全体三人**(含刚刚报无主的这位玩家自己)显示另外两家各自的主牌数量、以及这些主牌里有多少对子。
- 同时界面上会出现一个"明牌"按钮:点击后可以查看另外两家手中全部主牌的**具体牌面**(不只是数量 / 对子结构),再点一次取消查看、恢复隐藏。
- 庄家若触发了 8.2 节的"亮牌要求",两个闲家可以看到对应的统计信息(只亮数量 / 结构,不亮具体牌面,见 8.2 节)。
- **不可查牌模式**:不提供以上任何查看 / 明牌功能——即使有玩家报无主,也不会显示任何人的主牌数量、对子结构,也没有"明牌"按钮;庄家即使触发了 8.2 节的亮牌条件,闲家也看不到相应的统计信息(见 8.2 节)。
---
## 10. 房间设置选项
### 10.1 牌局扣卡方式
| 扣卡方式 | 6 局 | 12 局 |
| --- | --- | --- |
| AA 制(每人扣卡) | 每人 1 张 | 每人 2 张 |
| 房主扣卡 | 2 张 | 4 张 |
### 10.2 查牌模式
可查牌 / 不查牌(见第 9 节)。
### 10.3 附加规则
傍王、爬坡:两者可以同时勾选(互不冲突)。
---
## 11. 牌局交互提示
- 首家出主牌时可以甩牌,甩牌的生效条件、跟牌规则、甩错惩罚详见 5.4 节。
- 牌局结束需要亮出底牌。
- 闲家在出牌时需要有 3 个提示选项:
1. **踩**:提示对家"我能大过庄家"。
2. **没分**:提示对家"我手上没分了"。
3. **有分**:提示对家"我手上有分"。
@@ -0,0 +1,431 @@
# 二七王协议包列表
说明:`data` 均为发送方与接收方之间约定的 JSON 内容;成败判定只看推送包里的 `data.success`。
---
## 1. 发牌(fapai)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:fapai
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| asetidx | 整数 | 当前局数 |
| asetcount | 整数 | 总局数 |
| cards | 数组 | 自己得到的牌id列表 |
| seat | 整数 | 当前等待叫分者的位置 |
| countdown | 整数 | 叫分倒计时 |
---
## 2. 叫分或不叫(jiaofen,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:jiaofen
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 叫分者的位置序号 |
| call | 整数 | 分数,0表示不叫 |
---
## 3. 叫分或不叫(jiaofen,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiaofen
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 叫分者的位置序号 |
| call | 整数 | 分数,0表示不叫 |
| currcall | 整数 | 当前叫到的分数 |
| multiple | 整数 | 倍数 |
| nextseat | 整数 | 下一个叫分者的位置序号 |
| countdown | 整数 | 叫分倒计时 |
---
## 4. 上庄(shangzhuang)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:shangzhuang
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 叫分者的位置序号 |
| call | 整数 | 分数,0表示不叫 |
| banker | 整数 | 庄家的位置序号 |
| grade | 整数 | 庄家的叫分 |
| multiple | 整数 | 倍数 |
| bottomcards | 数组 | 底牌 |
| cards | 数组 | 拿了底牌后手上的牌,包括底牌,庄家才有此属性,闲家没有该属性 |
| countdown | 整数 | 选主倒计时 |
| touxiang | 整数 | 是否允许投降 0:不允许 1:允许 |
---
## 5. 投降(touxiang,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:touxiang
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 庄家的位置序号(`mod.js` 中校验 `o_paiju.banker != seat`,只有庄家能投降;选主/埋牌/投降阶段均可发起) |
---
## 6. 选主(xuanzhu,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:xuanzhu
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 选主者的位置序号 |
| flower | 整数 | 花色 1方块 2梅花 3红心 4黑桃 |
---
## 7. 选主(xuanzhu,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:xuanzhu
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| banker | 整数 | 庄家的位置序号 |
| flower | 整数 | 花色 1方块 2梅花 3红心 4黑桃 |
| countdown | 整数 | 埋牌倒计时 |
| cards | 数组 | 选主后自己手上的牌id列表 |
---
## 8. 埋牌(maipai,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:maipai
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 埋牌者的位置序号 |
| cards | 数组 | 埋牌的id列表 |
---
## 9. 埋牌(maipai,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:maipai
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| cards | 数组 | 埋牌后手上的牌,去掉了埋牌,庄家才有此属性,闲家没有该属性 |
| bottomcards | 数组 | 埋的牌,庄家才有此属性,闲家没有该属性 |
| seat | 整数 | 出牌者的位置序号 |
| countdown | 整数 | 出牌倒计时 |
---
## 10. 出牌(chupai,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:chupai
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 出牌者的位置序号 |
| cards | 数组 | 出牌的id列表 |
---
## 11. 第一个玩家出牌(chupai1)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai1
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 出牌者的位置序号 |
| cards | 数组 | 出的牌id列表 |
| info | 数组 | 出牌者的座位牌况 `o_paiju.seatlist[seat]`,5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,报无主后为实际数量) |
| count | 整数 | 出牌数量 |
| flower | 整数 | 出牌花色 |
| cardtype | 整数 | 出牌牌型 |
| nextseat | 整数 | 下一个出牌者的位置序号 |
| countdown | 整数 | 下一个出牌者的出牌倒计时 |
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0` 即为 1),供客户端在可查牌模式下展示他家主牌数量/对子结构 |
| cardsinhand | 数组 | 出牌者出牌后手上剩下的牌id列表,只有出牌者才有此属性 |
---
## 12. 第二个玩家出牌(chupai2)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai2
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 出牌者的位置序号 |
| cards | 数组 | 出的牌id列表 |
| info | 数组 | 出牌者的座位牌况 `o_paiju.seatlist[seat]`,5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,报无主后为实际数量) |
| nextseat | 整数 | 下一个出牌者的位置序号 |
| countdown | 整数 | 出牌倒计时 |
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0` 即为 1),供客户端在可查牌模式下展示他家主牌数量/对子结构 |
| cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 |
---
## 13. 第三个玩家出牌(chupai3)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:chupai3
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 出牌者的位置序号 |
| cards | 数组 | 出的牌id列表 |
| info | 数组 | 出牌者的座位牌况 `o_paiju.seatlist[seat]`,5 元素数组:前 4 个对应花色1~4 的 `[无该花色标志, 该花色无对标志]`(0/1),第 5 个为报副情况 `[剩余主牌数, 剩余主对数]`(初始 `[-1,-1]`,报无主后为实际数量) |
| nextseat | 整数 | 下一个出牌者的位置序号 |
| countdown | 整数 | 出牌倒计时 |
| baozhu | 整数 | 是否已有玩家报无主(主牌出空)0否 1是;对应服务端 `have_baofu()`(任一玩家 `seatlist[seat][4][0]==0` 即为 1),供客户端在可查牌模式下展示他家主牌数量/对子结构 |
| cardsinhand | 数组 | 出牌后出牌者手上剩下的牌id列表,只有出牌者才有此属性 |
| maxseat | 整数 | 本轮出牌谁最大,只有本轮最后一个玩家出牌后才有此属性 |
| grade | 整数 | 本轮闲家得分,只有本轮最后一个玩家出牌后且闲家有得分才有此属性 |
---
## 14. 结算(jiesuan)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:jiesuan
结算包由以下几个子结构拼装而成。三种结算来源的组成不同:
- **正常出牌结算**(`mod.chupai` 中最后一张牌出完,`get_paiju_account(0, ...)`):含 `chupai` + `bottom` + `aset`(末局再加 `account`)。
- **投降结算**(`mod.touxiang`,`get_paiju_account(1, ...)`):只含 `aset`(末局再加 `account`),无 `chupai`、无 `bottom`。
- **解散结算**(`export.get_disbandRoom`,`get_paiju_account(2, ...)`):只含 `aset` + `account`,无 `chupai`、无 `bottom`。
**chupai(出牌包,仅正常出牌结算存在)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 出牌者的位置序号,投降和解散无此属性 |
| cards | 数组 | 出的牌id列表,投降和解散无此属性 |
| maxseat | 整数 | 本轮出牌谁最大,投降和解散无此属性 |
| grade | 整数 | 本轮闲家得分,投降和解散无此属性 |
| gradecards | 数组 | 本轮闲家得分分牌列表,投降和解散无此属性。**注意:当前源码中赋值语句被注释(`class.paiju.js`/`mod.js` 中相关行均被注释掉),该字段实际永远不会出现在下发的包里,属于失效字段** |
**bottom(抠底包,仅正常出牌结算存在;`cards` 恒有,其余仅闲家抠底时有)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| cards | 数组 | 底牌,投降和解散无此属性 |
| multiple | 整数 | 闲家抠底倍数,闲家没抠底无此属性,投降和解散无此属性 |
| grade1 | 整数 | 底牌分数,闲家没抠底无此属性,投降和解散无此属性 |
| grade2 | 整数 | 闲家抠底得分,闲家没抠底无此属性,投降和解散无此属性 |
**aset(单局结算包)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| banker | 整数 | 庄家的位置序号,-1:无庄 |
| call | 整数 | 庄家叫分,-1:无叫分 |
| multiple | 整数 | 叫分倍数 `get_multiple_bycall(call)`:call>60→1,call>40→2,call>0→4;无叫分(call≤0)→0 |
| flower | 整数 | 主牌花色,-1:无主牌花色 |
| grade | 整数 | 闲家得分 |
| upgrade | 整数 | 升级级数:0中途解散 1过庄 2小光 4大光 -1倒庄 <-1升几级 -99投降 |
| seatlist | 数组 | 玩家列表,元素结构见下 |
`seatlist` 数组元素结构:
```json
{
"cards": [], // 王牌或冲关牌列表
"obsolete": 0, // 冲关作废标志 0不作废 1作废
"chongguan": 0, // 冲关数
"grade_cg": 0, // 冲关得分
"grade_w": 0, // 傍王得分
"grade_jf": 0, // 捡分得分
"grade": 0, // 本局总分
"score": 0 // 累计得分
}
```
**account(大局结算包)**
> 仅当打到最后一局(`o_paiju.idx >= o_room.asetcount`)或房间中途解散(解散结算)时,`jiesuan` 包才会带上此分组;普通的中间局结算包没有 `account` 字段(`class.paiju.js` `get_paiju_account`)。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| account | 数组 | 玩家列表,第一位为累积得分,第二位为每局得分,结构见下 |
```json
[
[0, []],
[0, []],
[0, []]
]
```
---
## 15. 准备(zhunbei,客户端→服务器)
发包者:游戏 收包者:服务器 app:youle route:erqiwang rpc:zhunbei
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| agentid | 字符 | 代理id |
| playerid | 整数 | 玩家id |
| gameid | 字符 | 游戏id |
| roomcode | 整数 | 房间号 |
| seat | 整数 | 位置序号 |
---
## 16. 准备(zhunbei,服务器→客户端)
发包者:服务器 收包者:游戏 app:youle route:erqiwang rpc:zhunbei
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 位置序号 |
---
## 断线重连(deskinfo)
> 由平台在玩家进入房间/断线重连时回调 `youle_erqiwang.export.get_deskinfo(o_room, seat)` 生成并下发(服务器→客户端);下发的 app/route/rpc 由平台的进房/重连流程决定,不在本子游戏代码内固定。下列字段即该函数返回的 `deskinfo` 对象结构,按当前 `paiju.step` 只带对应阶段的分组。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| count | 整数 | 总局数 |
| idx | 整数 | 当前局数 |
| PlayerInfo | 数组 | 三个玩家目前的总积分 `[0,0,0]` |
| step | 整数 | 牌桌状态:1发完牌叫分 2选主 3埋牌 4投降 5出牌 6结算 |
| MyCards | 数组 | 自己手上的牌 |
**CallRun(不在叫分阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| seat | 整数 | 当前叫分位置 |
| countdown | 整数 | 叫分倒计时 |
| nowcall | 整数 | 当前叫分 |
| multiple | 整数 | 倍数 |
| call | 数组 | 三家叫分 `[null,0,65]`,null还未叫分,0不叫,>0叫了多少分 |
**ChooseMain(不在选主阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| banker | 整数 | 庄家 |
| call | 整数 | 叫分 |
| multiple | 整数 | 倍数 |
| countdown | 整数 | 选主倒计时 |
| bottomcards | 数组 | 底牌 |
| touxiang | 整数 | 是否允许投降 0:不允许 1:允许 |
**BuryCards(不在埋牌阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| banker | 整数 | 庄家 |
| call | 整数 | 叫分 |
| multiple | 整数 | 倍数 |
| flower | 整数 | 主牌花色 |
| countdown | 整数 | 埋牌倒计时 |
| bottomcards | 数组 | 底牌 |
| touxiang | 整数 | 是否允许投降 0:不允许 1:允许 |
**Surrender(不在投降阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| banker | 整数 | 庄家 |
| call | 整数 | 叫分 |
| multiple | 整数 | 倍数 |
| flower | 整数 | 主牌花色 |
| countdown | 整数 | 出牌倒计时 |
| bottomcards | 数组 | 埋的牌,只有庄家有此属性 |
| touxiang | 整数 | 是否允许投降 0:不允许 1:允许 |
**PushCards(不在出牌阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| banker | 整数 | 庄家 |
| call | 整数 | 叫分 |
| multiple | 整数 | 倍数 |
| flower | 整数 | 主牌花色 |
| countdown | 整数 | 出牌倒计时 |
| bottomcards | 数组 | 埋的牌,只有庄家有此属性 |
| grade | 整数 | 当前的捡分分数 |
| gradecards | 数组 | 当前的捡分分牌,只有闲家有此属性 |
| playproc | json | 当前出牌情况 `o_paiju.playproc`:`round` 第几轮、`start` 本轮首出位置、`currseat` 当前出牌位置、`startcount` 首出张数、`startflower` 首出花色、`starttype` 首出牌型、`maxseat` 本轮最大者位置、`maxcard` 最大牌编码、`cards` 本轮三家各自出的牌(数组下标=位置序号) |
| seatlist | 数组 | 三家座位牌况,元素结构同上文 chupai 包的 `info`(每个玩家一个 5 元素数组:4 个花色的 `[无该花色,无对]` + 报副 `[剩余主牌数,剩余主对数]`) |
| pushlist | 数组 | 出牌历史 `[[[], [], []], [[], [], []], ...]`,外层下标=轮次,内层 3 个数组按位置序号存该轮各家出的牌id(已按大小排序) |
**Balance(不在结算阶段无此属性)**
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| readystate | 数组 | 所有玩家的准备状态 |
| aset | json | 单局结算包,同上面结算包中的 `aset`,只有当自己的准备状态为0时才有此属性 |
---
## 战绩(大局列表)gameinfo1
> 非客户端收发包:大局结束/解散时由 `class.desk.js` `get_desk_account` 组装,经 `import.save_grade(o_room, o_gameinfo1, o_gameinfo2, 1)` 传给平台战绩服务持久化(服务器→平台)。
| 参数名 | 类型 | 说明 |
| --- | --- | --- |
| roomcode | 整数 | 房号 |
| asetcount | 整数 | 实际局数 |
| createtime | 字符 | 开房时间 |
| makewartime | 字符 | 开战时间 |
| players | 数组 | 玩家列表,元素结构见下 |
`players` 数组元素结构:
```json
{
"seat": 0, // 座位
"playerid": 100001, // 玩家ID
"name": "", // 昵称
"avatar": "", // 头像
"score": 0 // 成绩
}
```
---
## 战绩(大局)gameinfo2
> 非客户端收发包:与 gameinfo1 同批,由 `import.save_grade` 的第 3 个参数传给平台战绩服务(服务器→平台)。
牌局列表,数组元素结构:
```json
{
"starttime": "", // 开始时间
"endtime": "", // 结束时间
"seatlist": [6, -6, 0], // 玩家成绩
"callproc": [], // 叫分过程,详情见代码中的注释
"banker": 0, // 庄
"call": 0, // 叫分
"flower": 0, // 主牌花色
"result": 0, // 牌局结果
"cards": [] // 发牌出牌情况,详情见代码中的注释
}
```
+457
View File
@@ -0,0 +1,457 @@
//////////////////////////////////////////////////////////////////
/////////////////////////////咻一咻/////////////////////////////
//////////////////////////////////////////////////////////////////
var youle_erqiwang = youle_erqiwang || cls_mod.new("youle_erqiwang", "erqiwang", youle_app);
//牌类
min_loadJsFile("games/erqiwang/class.pai.js", function(){
//牌局类
min_loadJsFile("games/erqiwang/class.paiju.js", function(){
//牌桌类
min_loadJsFile("games/erqiwang/class.desk.js", function(){
//算法类
min_loadJsFile("games/erqiwang/class.arith.js", function(){
//对内的输入接口类
min_loadJsFile("games/erqiwang/class.import.js", function(){
//对内的输出接口类
min_loadJsFile("games/erqiwang/class.export.js", function(){
});
});
});
});
});
});
//对内的输入接口类(需要的外部接口)
youle_erqiwang.import = null;
//对内的输出接口类(提供给外部使用的接口)
youle_erqiwang.export = null;
//叫分或不叫
youle_erqiwang.jiaofen = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
var call = parseInt(pack.data.call);
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
return;
}
//检查当前的游戏阶段是否是叫分阶段
if (o_paiju.step != 1){
return;
}
//检查当前的叫分位置
if (o_paiju.method.get_callgrade_seat() != seat){
return;
}
//检查当前的叫分分值必须大于0,且是5的整数倍
if (call < 0 || call % 5 != 0){
return;
}
var curr_call = o_paiju.method.get_callgrade_value();
//第一家不能不叫分
if (curr_call == 0 && call == 0){
return;
}
//叫分必须小于之前的叫分
if (curr_call != 0 && call >= curr_call){
return;
}
//叫分成功
o_paiju.method.do_callgrade(call);
//返回前端
if (o_paiju.step == 1){
//等待下一家叫分
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "jiaofen";
msg.data = {};
msg.data.seat = seat;
msg.data.call = call;
msg.data.currcall = o_paiju.method.get_callgrade_value();
msg.data.multiple = cls_youle_erqiwang_arith.get_multiple_bycall(msg.data.currcall);
msg.data.nextseat = o_paiju.method.get_callgrade_seat();
msg.data.countdown = o_desk.method.get_countdown_jiaofen();
o_room.method.sendpack_toother(msg, -1);
return;
}
if (o_paiju.step == 2){
//等待庄家叫主
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "shangzhuang";
msg.data = {};
msg.data.seat = seat;
msg.data.call = call;
msg.data.banker = o_paiju.banker;
msg.data.grade = o_paiju.call;
msg.data.multiple = cls_youle_erqiwang_arith.get_multiple_bycall(msg.data.grade);
msg.data.bottomcards = o_paiju.method.get_bottomcards();
msg.data.countdown = o_desk.method.get_countdown_xuanzhu();
if (o_paiju.call < 65){
msg.data.touxiang = 0;
} else {
msg.data.touxiang = 1;
}
for (var i = 0; i < o_room.seatlist.length; i++) {
msg.conmode = o_room.seatlist[i].conmode;
msg.fromid = o_room.seatlist[i].fromid;
if (i == o_paiju.banker){
msg.data.cards = o_paiju.method.get_seat_cards(i);
} else {
delete msg.data.cards;
}
youle_erqiwang.app.SendPack(msg);
}
return;
}
}
//选主
youle_erqiwang.xuanzhu = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
var flower = parseInt(pack.data.flower);
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
return;
}
//检查当前的游戏阶段是否是选主阶段
if (o_paiju.step != 2){
return;
}
//检查庄家位置
if (o_paiju.banker != seat){
return;
}
//检查花色
if (flower != 1 && flower != 2 && flower != 3 && flower != 4){
return;
}
//选主成功
o_paiju.method.do_choiceflower(flower);
//返回前端
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "xuanzhu";
msg.data = {};
msg.data.banker = o_paiju.banker;
msg.data.flower = flower;
msg.data.countdown = o_desk.method.get_countdown_maipai();
for (var i = 0; i < o_room.seatlist.length; i++) {
msg.conmode = o_room.seatlist[i].conmode;
msg.fromid = o_room.seatlist[i].fromid;
msg.data.cards = o_paiju.method.get_seat_cards(i);
youle_erqiwang.app.SendPack(msg);
}
}
//埋牌
youle_erqiwang.maipai = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
var cards = pack.data.cards;
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
return;
}
//检查当前的游戏阶段是否是埋牌阶段
if (o_paiju.step != 3){
return;
}
//检查庄家位置
if (o_paiju.banker != seat){
return;
}
//检查埋牌是否是8张
if (cards.length != 8){
return;
}
//检查埋牌是否都在庄家手上
if (!o_paiju.method.check_cards_inhand(cards, seat)){
return;
}
//埋牌成功
o_paiju.method.do_burycard(cards);
//返回前端
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "maipai";
msg.data = {};
msg.data.seat = o_paiju.playproc.currseat;
msg.data.countdown = o_desk.method.get_countdown_chupai();
for (var i = 0; i < o_room.seatlist.length; i++) {
msg.conmode = o_room.seatlist[i].conmode;
msg.fromid = o_room.seatlist[i].fromid;
if (i == o_paiju.banker){
msg.data.cards = o_paiju.method.get_seat_cards(i);
msg.data.bottomcards = cards;
} else {
delete msg.data.cards;
delete msg.data.bottomcards;
}
youle_erqiwang.app.SendPack(msg);
}
}
//投降
youle_erqiwang.touxiang = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
return;
}
//检查当前的游戏阶段是否是叫分、埋牌、投降阶段
if (o_paiju.step != 2 && o_paiju.step != 3 && o_paiju.step != 4){
return;
}
//检查庄家位置
if (o_paiju.banker != seat){
return;
}
//检查是否允许投降
if (o_paiju.call < 65){
return;
}
//投降结算
o_paiju.step = 6;
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.rpc = "jiesuan";
msg.data = {};
msg = o_paiju.method.get_paiju_account(1, msg);
o_room.method.sendpack_toother(msg, -1);
}
//出牌
youle_erqiwang.chupai = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
var cards = pack.data.cards;
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
return;
}
//检查当前的游戏阶段是否是出牌阶段
if (o_paiju.step != 4 && o_paiju.step != 5){
return;
}
//检查出牌位置
if (seat != o_paiju.playproc.currseat){
return;
}
//检查出牌
if (cards.length == 0){
return;
}
//检查出牌是否都在玩家手上
if (!o_paiju.method.check_cards_inhand(cards, seat)){
return;
}
//检查选中的牌是否可出,并出牌
var re = o_paiju.method.do_playcard(cards);
if (!re.result){
return;
}
//返回前端
var msg = {};
msg.app = "youle";
msg.route = "erqiwang";
msg.data = {};
msg.data.seat = seat;
msg.data.cards = cards;
msg.data.info = o_paiju.seatlist[seat];
msg.data.nextseat = o_paiju.playproc.currseat;
msg.data.countdown = o_desk.method.get_countdown_chupai();
if (o_paiju.method.have_baofu()){
msg.data.baozhu = 1;
} else {
msg.data.baozhu = 0;
}
switch (re.idx)
{
case 1: //第一个出牌
msg.rpc = "chupai1";
msg.data.count = re.count;
msg.data.flower = re.flower;
msg.data.cardtype = re.cardtype;
break;
case 2: //第二个出牌
msg.rpc = "chupai2";
break;
case 3: //第三个出牌
msg.rpc = "chupai3";
msg.data.maxseat = re.maxseat;
if (re.grade > 0){
msg.data.grade = re.grade;
// msg.data.gradecards = re.gradecards;
}
break;
}
if (o_paiju.step == 5){
for (var i = 0; i < o_room.seatlist.length; i++){
msg.conmode = o_room.seatlist[i].conmode;
msg.fromid = o_room.seatlist[i].fromid;
if (i == seat){
msg.data.cardsinhand = o_paiju.method.get_seat_cards(i);
} else {
delete msg.data.cardsinhand;
}
youle_erqiwang.app.SendPack(msg);
}
} else if (o_paiju.step == 6){
//小局结算
//出牌包的调整
msg.data.chupai = {};
msg.data.chupai.seat = msg.data.seat;
msg.data.chupai.cards = msg.data.cards;
msg.data.chupai.maxseat = msg.data.maxseat;
if (msg.data.grade){
msg.data.chupai.grade = msg.data.grade;
}
if (msg.data.gradecards){
msg.data.chupai.gradecards = msg.data.gradecards;
}
delete msg.data.seat;
delete msg.data.cards;
delete msg.data.info;
delete msg.data.nextseat;
delete msg.data.countdown;
delete msg.data.baozhu;
delete msg.data.cardsinhand;
delete msg.data.maxseat;
delete msg.data.grade;
delete msg.data.gradecards;
//获取抠底包
msg = o_paiju.method.get_bottom_account(msg);
//获取小局结算包
msg = o_paiju.method.get_paiju_account(0, msg);
o_room.method.sendpack_toother(msg, -1);
}
}
//准备
youle_erqiwang.zhunbei = function(pack){
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
//检查玩家的位置是否正确
var o_room = youle_erqiwang.import.check_player(agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room){
return;
}
//牌桌
var o_desk = o_room.o_desk;
if (!o_desk){
return;
}
//牌局
var o_paiju = o_desk.method.curr_paiju();
if (!o_paiju){
return;
}
//检查当前的游戏阶段是否是结算阶段
if (o_paiju.step != 6){
return;
}
//检查当前局数小于总局数
if (o_desk.paiju_list.length >= o_room.asetcount){
return;
}
//准备成功
o_desk.method.do_prepare(seat);
}