diff --git a/CLAUDE.md b/CLAUDE.md index e0a5963..e14d28b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,6 +88,11 @@ server/<游戏容器目录>/<游戏>/ ← 子游戏接入后**唯一 注意:`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`)。 - 服务端 `require` 只能写在文件开头的守卫块内(`if (typeof require !== 'undefined') { var X = require(...); }`)——禁止在函数体内中途 `require`;两个运行时之后统一按同一个全局名引用。 diff --git a/client/js/00_Surface/07_Desk.js b/client/js/00_Surface/07_Desk.js index 670ce82..0e4ba8c 100644 --- a/client/js/00_Surface/07_Desk.js +++ b/client/js/00_Surface/07_Desk.js @@ -911,7 +911,7 @@ Desk.send_text=function(_msg){//发送消息 if(!_msg.data.type){ var pobj = Desk.GetPlayerBySeat(_msg.data.seat); 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); }else if(_msg.data.type == 1){//全服公告 GameUI.CloseChat(); @@ -929,7 +929,7 @@ Desk.send_text=function(_msg){//发送消息 var idx = Number(_msg.data.text); _msg.data.text = Game_Config.Info.TextContent[(idx - 1)%Game_Config.Info.TextContent.length]; 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); } } diff --git a/client/js/00_Surface/11_GameUI.js b/client/js/00_Surface/11_GameUI.js index cab2169..03223a8 100644 --- a/client/js/00_Surface/11_GameUI.js +++ b/client/js/00_Surface/11_GameUI.js @@ -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); set_self(204, 7, "", 0, 0); if (v == 0) { @@ -6085,6 +6090,7 @@ GameUI.play_voice = function(_msg, type, v) { function gameui_play_voice(v) { v = Number(v); + var bTemp = false; if (v >= ConstVal.Com_History.content_tag) { bTemp = true; @@ -6092,6 +6098,10 @@ function gameui_play_voice(v) { if (bTemp) { play_ani(1, ConstVal.Com_History.f_spid + "add" + v, 43, 1, 3, 0, 600, 0, 0, 0, 0, 0, 0); } else { + if(Game_Modify.gameui_play_voice){ + Game_Modify.gameui_play_voice(v); + return; + } var x = Game_Config.Voice.VoiceLoc[v][0]; var y = Game_Config.Voice.VoiceLoc[v][1]; var dis_x = Game_Config.Voice.VoiceDis[0]; @@ -6128,10 +6138,15 @@ function gameui_stop_voice(v) { if (v >= ConstVal.Com_History.content_tag) { bTemp = true; } + if (bTemp) { 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); } else { + if(Game_Modify.gameui_stop_voice){ + Game_Modify.gameui_stop_voice(v); + return; + } set_self(612 + v, 37, 0, 0, 0); set_self(642 + v, 37, 0, 0, 0); play_ani(0, 642 + v, 43); diff --git a/client/js/01_SubGame/01_SubGame_modify.js b/client/js/01_SubGame/01_SubGame_modify.js index 94c2e67..5c7c958 100644 --- a/client/js/01_SubGame/01_SubGame_modify.js +++ b/client/js/01_SubGame/01_SubGame_modify.js @@ -46,6 +46,52 @@ Game_Modify.CreateRoomData = { 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) ---------- Game_Modify.utlmousedown = function (gameid, spid, downx, downy, no1, no2, no3, no4, no5, no6) { diff --git a/client/js/01_SubGame/02_SubGame_Input.js b/client/js/01_SubGame/02_SubGame_Input.js index 36bf396..0e11ef2 100644 --- a/client/js/01_SubGame/02_SubGame_Input.js +++ b/client/js/01_SubGame/02_SubGame_Input.js @@ -293,8 +293,210 @@ Game_Modify.getMaxPlayerCount = function (roomtype) { 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) { 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); +}; \ No newline at end of file diff --git a/client/js/gameabc-framework/templates/subgame-entry/00_SubGame_Config.template.js b/client/js/gameabc-framework/templates/subgame-entry/00_SubGame_Config.template.js index 85cbce1..9f0d41a 100644 --- a/client/js/gameabc-framework/templates/subgame-entry/00_SubGame_Config.template.js +++ b/client/js/gameabc-framework/templates/subgame-entry/00_SubGame_Config.template.js @@ -1,36 +1,38 @@ // ============================================================================ -// 00_SubGame_Config.template.js —— 平台配置项模板(结构由平台定义,子游戏填【值】) -// 这是配置文件(C 类,非转发壳)。复制后按本游戏界面/玩法调整各项值。 -// 严格 ES5。 +// 游戏配置 (Game Configuration) // ============================================================================ -var Game_Config = Game_Config || {}; +var Game_Config = Game_Config||{};//相关配置 -Game_Config.Debugger={//调试配置 +Game_Config.Debugger={//调试配置 isDebugger : true,// debugger模式下会将所有收发的包输出到控制台(正式发布改为false) AutoLogin : true,//debugger模式下是否需要记住登录状态自动登录(正式发布改为true) - isSubmitError : false,//是否需要服务器收集错误信息调试时可根据需要(正式发布改为true) - visitorLogin : true,//隐藏式游客登录 + isSubmitError : false,//是否需要服务器收集错误信息调试时可根据需要(正式发布改为true) + visitorLogin : true,//隐藏式游客登录 visiblePay:true,//审核通过后是否显示支付按钮 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 = { - SumOfRoomtype:3,//创建时房间类型roomtype数组长度 + SumOfRoomtype:3,//创建时房间类型roomtype数组长度 ShowChat:2000,//聊天停留时间 - PlayerCnt:4,//房间最大人数(按房间最大人数设置) + PlayerCnt:4,//房间最大人数 group:500,//游戏最大群组号 - showtime:10,//加载等待最少时间 - reconnecttime:30000,//唤醒游戏间隔(超过间隔断线重连) + showtime:10,//加载等待最少时间 + reconnecttime:30000,//唤醒游戏间隔(超过间隔断线重连) Mainnickname:10,//主界面玩家昵称显示长度(字符长度) - Infonickname:16//个人信息显示长度(字符长度) + Infonickname:16//个人信息显示长度(字符长度) }; Game_Config.Combat = { - height:80,//战绩页单行高度高度 - up_y:190,//战绩页裁剪y坐标 + height:80,//战绩页单行高度高度 + up_y:190,//战绩页裁剪y坐标 bannerheight:60,//战绩横幅高度 bannery:80,//战绩横幅Y左边(相对48精灵) btnbg:50 @@ -42,46 +44,46 @@ Game_Config.Info = { otherPositionDefault:true,//其他主界面玩家信息(昵称、积分)是否采用默认对齐方式对齐居中点为头像中点 myPositionDefault:true,//其他主界面玩家信息(昵称、积分)是否采用默认对齐方式对齐居中点为头像中点 myPosition:130,//自己信息对齐点(根据游戏界面自行修改) - position:[],//其他主界面玩家信息(昵称、积分)居中对齐的x坐标 注意是其他玩家不包括自己从下家开始 + position:[],//其他主界面玩家信息(昵称、积分)居中对齐的x坐标 注意是其他玩家不包括自己从下家开始 //TextContent:["1","2","3","4","5","6","7"],//常用语内容 - TextContent:["你好","谢谢","再来","快点","不要","加油",":)"],//常用语内容 - TextContentMp3:["","","","","","",""],//常用语对应音效 + TextContent:["你好","2","3","4","5","6","7"],//常用语内容 + TextContentMp3:["","","","","","",""],//常用语对应音效 }; -Game_Config.Share={//分享参数 - appdownload:"",//下载链接(无需配置,从服务器获取) - title:"游戏名",//(分享标题) - description:"",//分享描述 +Game_Config.Share={//分享参数 + appdownload:"",//下载链接(无需配置,从服务器获取) + title:"Test",//(分享标题) + description:"hello world",//分享描述 gameTitle:"",//游戏中的分享标题模板工程会自动将游戏名字分享出去、不必写在这个变量里 - gameDescription:""//游戏中分享描述 + gameDescription:""//游戏中分享描述 }; Game_Config.Chat={//游戏内聊天配置信息 LimitLength:40,//聊天最大长度(字节长度) textwidth:12.5,//聊天显示文字的宽度(未改动聊天显示文字大小无需改动无需修改) ChatDis:[30,12],//聊天气泡与内容的间隔0位置左右间隔(最好不要低于30)1位置上下间隔 - isLeft:[1,0,0,1,1],//聊天气泡是否为以左边为基准线(坐标/基准按各游戏界面调整) - ChatLoc:[[35,517],[1100,315],[1100,105],[175,105],[175,315]]//聊天气泡的基准点位置(注意是基准点位置!)(坐标/基准按各游戏界面调整) + isLeft:[1,0,0,1,1],//聊天气泡是否为以左边为基准线 + ChatLoc:[[35,517],[1100,315],[1100,105],[175,105],[175,315]]//聊天气泡的基准点位置(注意是基准点位置!) }; Game_Config.Voice={//游戏内聊天配置信息 VoiceTime:600,//播放语音动画的时间一般情况无需无需修改 VoiceDis:[30,12],//语音气泡与内容的间隔0位置左右间隔(最好不要低于30)1位置上下间隔 - isLeft:[1,0,0,1,1],//气泡是否为以左边为基准线(坐标/基准按各游戏界面调整) - VoiceLoc:[[35,517],[1100,315],[1100,105],[175,105],[175,315]]//气泡的基准点位置(注意是基准点位置!)(坐标/基准按各游戏界面调整) + isLeft:[1,0,0,1,1],//气泡是否为以左边为基准线 + VoiceLoc:[[35,517],[1100,315],[1100,105],[175,105],[175,315]]//气泡的基准点位置(注意是基准点位置!) }; Game_Config.Setting={ - Ads1:"本游戏仅供娱乐休闲使用,\n道具及虚拟货币不可兑换\n现金或实物,不代表任何\n真实财富价值。", - //Ads2:"\n 或客服号:\n", - board:"",//从后台获取设置无效 + Ads1:"本游戏仅为娱乐休\n闲使用,道具所有\n 游戏通用\n\n游戏问题请联系微\n 信公众号:\n", + //Ads2:"\n 或客服号:\n", + board:"",//从后台获取设置无效 //info:[" 客服信息","QQ:","",""],//客服QQ提醒 - info:[" 客服信息","QQ:","手机:","微信:"],//客服QQ提醒 - board_blength:24,//通知页面一行最大字节数(无需设置) - charge:"充值提示",//充值提示 + info:[" 客服信息","QQ:","手机:","微信:"],//客服QQ提醒 + board_blength:24,//通知页面一行最大字节数(无需设置) + charge:"关注友乐微信公众号充值",//充值提示 }; Game_Config.Protocol={ - x:270,//协议图片初始x坐标 + x:270,//协议图片初始x坐标 y:70,//协议图片初始y坐标 - h:560,//协议图片显示高度 - w:729//协议图片显示宽度 + h:560,//协议图片显示高度 + w:729//协议图片显示宽度 }; Game_Config.Help={//帮助 x:300,//帮助图片初始x坐标 @@ -89,27 +91,27 @@ Game_Config.Help={//帮助 w:729,//帮助图片显示高度 h:450//帮助图片显示宽度 }; -Game_Config.Notice={//滚动公告 - x1:0,//无需设置 - x2:0,//无需设置 - y:0,//无需设置 +Game_Config.Notice={//滚动公告 + x1:0,//无需设置 + x2:0,//无需设置 + y:0,//无需设置 h:0,//无需设置 - //以上参数用来截取滚动公告的显示,截取位置是与滚动公告底大小位置一致 - speed:0.1,//滚动公告的滚动速度 - width:15//滚动公告的内容字体的字节长度(汉字是两个字节) + //以上参数用来截取滚动公告的显示,截取位置是与滚动公告底大小位置一致 + speed:0.1,//滚动公告的滚动速度 + width:15//滚动公告的内容字体的字节长度(汉字是两个字节) }; -Game_Config.Feedback = {//反馈配置 - maxLen:250 //反馈内容最大长度 +Game_Config.Feedback = {//反馈配置 + maxLen:250 //反馈内容最大长度 }; -Game_Config.shakeList ={//摇一摇事件的回调ID、需要添加时在此处添加回调事件写在Utl_Input.js中的Utl.shakeEvent()中 +Game_Config.shakeList ={//摇一摇事件的回调ID、需要添加时在此处添加回调事件写在Utl_Input.js中的Utl.shakeEvent()中 nil:0, startwar:1 }; -Game_Config.soundList ={//声音资源名 - MenuSceneMusic:"",//大厅界面背景音 - MainSceneMusic:""//游戏主界面背景音 +Game_Config.soundList ={//声音资源名 + MenuSceneMusic:"",//大厅界面背景音 + MainSceneMusic:""//游戏主界面背景音 }; -Game_Config.ClickButton = {//需要设置点击音效的按钮(只有有弹窗的按钮设置此音效有效、按钮已经设置好、子游戏不允许修改) +Game_Config.ClickButton = {//需要设置点击音效的按钮(只有有弹窗的按钮设置此音效有效、按钮已经设置好、子游戏不允许修改) src_1:"",//点击时播放的声音资源文件( 不需要播放则不填,下同) src_2:""//弹窗时播放的音效 } @@ -120,11 +122,28 @@ Game_Config.loginButton = {//登录按钮信息 x3:488,//审核版本游客登录按钮x }; Game_Config.sysConfig = { - mainMenuButton:false,//是否进入主菜单界面隐藏四个按钮 - mainScenePlayerInfo:false,//是否隐藏主界面玩家信息 - mainSceneButton:false,//是否隐藏主界面按钮 - shareRoom:false,//接收到星星场是否屏蔽平台框架显示 - changeSeat:false,//是否屏蔽平台自动刷新换座后界面 - hideNotice:false,//隐藏公告栏 - vipInfinite:false,//是否为百人场 + mainMenuButton:false,//是否进入主菜单界面隐藏四个按钮 + mainScenePlayerInfo:false,//是否隐藏主界面玩家信息 + mainSceneButton:false,//是否隐藏主界面按钮 + shareRoom:false,//接收到星星场是否屏蔽平台框架显示 + changeSeat:false,//是否屏蔽平台自动刷新换座后界面 + hideNotice:false,//隐藏公告栏 + vipInfinite:false,//是否为百人场 } + + + + + + + + + + + + + + + + + diff --git a/client/js/gameabc-framework/templates/subgame-entry/01_SubGame_modify.template.js b/client/js/gameabc-framework/templates/subgame-entry/01_SubGame_modify.template.js index 94c2e67..5c7c958 100644 --- a/client/js/gameabc-framework/templates/subgame-entry/01_SubGame_modify.template.js +++ b/client/js/gameabc-framework/templates/subgame-entry/01_SubGame_modify.template.js @@ -46,6 +46,52 @@ Game_Modify.CreateRoomData = { 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) ---------- Game_Modify.utlmousedown = function (gameid, spid, downx, downy, no1, no2, no3, no4, no5, no6) { diff --git a/client/js/gameabc-framework/templates/subgame-entry/02_SubGame_Input.template.js b/client/js/gameabc-framework/templates/subgame-entry/02_SubGame_Input.template.js index 32d178b..0e11ef2 100644 --- a/client/js/gameabc-framework/templates/subgame-entry/02_SubGame_Input.template.js +++ b/client/js/gameabc-framework/templates/subgame-entry/02_SubGame_Input.template.js @@ -1,7 +1,7 @@ // ============================================================================ -// 02_SubGame_Input.template.js —— 平台输入/回调接口「转发壳」模板(框架维护,子游戏不碰) +// 02_SubGame_Input.js —— 平台输入/回调接口「转发壳」(框架维护,子游戏不碰) // 平台按全局名调用 gameHallImport.* / Game_Modify.*;本文件统一转发到 SubGameHooks 同名 hook。 -// 子游戏只需在 codes/ 的 SubGameHooks 里实现需要的接口(见 SubGameHooks.template.js)。 +// 子游戏只需在 codes/ 的 SubGameHooks 里实现需要的接口(见 codes/SubGameHooks.js)。 // 严格 ES5。接口清单与默认值依据 spec §9。 // ============================================================================ @@ -293,8 +293,210 @@ Game_Modify.getMaxPlayerCount = function (roomtype) { 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) { 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); +}; \ No newline at end of file diff --git a/server/games/erqiwang/class.arith.js b/server/games/erqiwang/class.arith.js new file mode 100644 index 0000000..3a73979 --- /dev/null +++ b/server/games/erqiwang/class.arith.js @@ -0,0 +1,1089 @@ +/////////////////////////////////////////////////// +////////// cls_youle_erqiwang_arith: 算法 ///////// +/////////////////////////////////////////////////// +var cls_youle_erqiwang_arith = cls_youle_erqiwang_arith || { + + //根据叫分计算倍数 + get_multiple_bycall: function(callgrade){ + if (callgrade > 60){ + return 1; + } + if (callgrade > 40){ + return 2; + } + if (callgrade > 0){ + return 4; + } + return 0; + }, + + //牌id转花色 + id_to_flower: function(cardid){ + if (cardid == 52 || cardid == 106){ + //小王 + return 5; + } + if (cardid == 53 || cardid == 107){ + //大王 + return 5; + } + return parseInt((cardid % 54) / 13) + 1; + }, + + //牌id转牌面数值 + id_to_number: function(cardid){ + if (cardid == 52 || cardid == 106){ + //小王 + return 53; + } + if (cardid == 53 || cardid == 107){ + //大王 + return 54; + } + return (cardid % 54) % 13 + 1; + }, + + //牌id转牌编码 + id_to_code: function(mainflower, cardid){ + if (cardid == 52 || cardid == 106){ + //小王 + return 9553; + } + if (cardid == 53 || cardid == 107){ + //大王 + return 9554; + } + var flower = cls_youle_erqiwang_arith.id_to_flower(cardid); + var number = cls_youle_erqiwang_arith.id_to_number(cardid); + var code = flower * 100 + number; + //特殊处理 + switch (number) + { + case 1: + code = code + 13; + break; + case 2: + code = code + 2000; + break; + case 7: + code = code + 7000; + break; + } + //主牌花色处理 + if (flower == mainflower){ + code = code + 1000; + } + return code; + }, + + //从大到小排序(冒泡排序) + order_cards: function(mainflower, aryCardIDs){ + if (aryCardIDs){ + for (var i = 0; i < aryCardIDs.length; i++) { + for (var j = i + 1; j < aryCardIDs.length; j++) { + var i_code = cls_youle_erqiwang_arith.id_to_code(mainflower, aryCardIDs[i]); + var j_code = cls_youle_erqiwang_arith.id_to_code(mainflower, aryCardIDs[j]); + if (i_code < j_code) { + var tmp = aryCardIDs[j]; + aryCardIDs[j] = aryCardIDs[i]; + aryCardIDs[i] = tmp; + } + } + } + } + return aryCardIDs; + }, + + //判断两个牌是否是连续的 + is_continuous: function(bigcode, smallcode){ + //是小王 + var is_xiaowang = function(_code){ + if (_code == 9553){ + return true; + } + return false; + } + //是正7 + var is_zheng7 = function(_code){ + if (_code > 8000 && _code < 9000){ + return true; + } + return false; + } + //是负7 + var is_fu7 = function(_code){ + if (_code > 7000 && _code < 8000){ + return true; + } + return false; + } + //是正2 + var is_zheng2 = function(_code){ + if (_code > 3000 && _code < 4000){ + return true; + } + return false; + } + //是负2 + var is_fu2 = function(_code){ + if (_code > 2000 && _code < 3000){ + return true; + } + return false; + } + //是主A + var is_zhuA = function(_code){ + if (_code > 1000 && _code < 2000 && _code % 100 == 14){ + return true; + } + return false; + } + //是8 + var is_8 = function(_code){ + if (_code % 100 == 8){ + return true; + } + return false; + } + //是6 + var is_6 = function(_code){ + if (_code % 100 == 6){ + return true; + } + return false; + } + + //普通的连牌 + if (bigcode - smallcode == 1){ + return true; + } + //小王、正7 + if (is_xiaowang(bigcode) && is_zheng7(smallcode)){ + return true; + } + //正7、负7 + if (is_zheng7(bigcode) && is_fu7(smallcode)){ + return true; + } + //负7、正2 + if (is_fu7(bigcode) && is_zheng2(smallcode)){ + return true; + } + //正2、负2 + if (is_zheng2(bigcode) && is_fu2(smallcode)){ + return true; + } + //负2、主A + if (is_fu2(bigcode) && is_zhuA(smallcode)){ + return true; + } + //8、6 + if (is_8(bigcode) && is_6(smallcode)){ + return true; + } + return false; + }, + + //获取对子列表,cards为同一花色的排过序的牌列表 + get_pairlist: function(mainflower, flowercards){ + var pairlist = []; + var i = 0; + while (i < flowercards.length - 1){ + //牌编码,两张牌两张牌一取 + var code_i = cls_youle_erqiwang_arith.id_to_code(mainflower, flowercards[i]); + var code_j = cls_youle_erqiwang_arith.id_to_code(mainflower, flowercards[i + 1]); + if (code_i == code_j){ + pairlist.push([flowercards[i], flowercards[i + 1]]); + i = i + 2; + } else { + i++; + } + } + return pairlist; + }, + + //获取拖拉机列表,pairlist为同一花色的排过序的对子列表,cardtype为拖拉机牌型 + get_tuolaji_list: function(mainflower, pairlist, cardtype){ + var tuolaji = []; + var count = cardtype - 300; //拖拉机的对子数量 + var i = 0; + for (var i = 0; i < pairlist.length - count + 1; i++){ + //每次按拖拉机的对子数量取N对 + var tmplist = pairlist.slice(i, i + count); + //判断是否是cardtype类型的拖拉机 + var is_tlj = true; + for (var j = 0; j < tmplist.length - 1; j++) { + var code_j1 = cls_youle_erqiwang_arith.id_to_code(mainflower, tmplist[j][0]); + var code_j2 = cls_youle_erqiwang_arith.id_to_code(mainflower, tmplist[j+1][0]); + if (!cls_youle_erqiwang_arith.is_continuous(code_j1, code_j2)){ + is_tlj = false; + break; + } + } + if (is_tlj){ + var tlj = []; + for (var k = 0; k < tmplist.length; k++){ + tlj = tlj.concat(tmplist[k]); + } + tuolaji.push(tlj); + } + } + return tuolaji; + }, + + //第一个位置是否可出牌 + can_playcard: function(mainflower, cards, selfseat, seatlist){ + /* + 参数: + mainflower: 主牌花色 + cards : 要出的牌id列表 + selfseat : 出牌者的位置 + seatlist : 座位列表,四维数组, + [ //第一层表示玩家,与位置序号对应 + [ //第二层表示花色,与花色-1对应 + [ + 0, //无该花色的牌标志 + 0 //该花色无对标志 + ], [0, 0], [0, 0], [0, 0] + ], + [[0, 0], [0, 0], [0, 0], [0, 0]], + [[0, 0], [0, 0], [0, 0], [0, 0]] + ] + 返回结果json: + result : true可出,false不可出 + flower : 花色,result=false时无此属性,下同 + cardtype : 牌型 + cardvalue: 牌型值,取满足牌型的最小牌的牌编码 + */ + + //返回的结果 + var can = {}; + can.result = true; + can.flower = null; + can.cardtype = null; + can.cardvalue = null; + + //返回不能出牌的结果 + var do_returnfalse = function(){ + can.result = false; + delete can.flower; + delete can.cardtype; + delete can.cardvalue; + } + + //规则外的约定,一次最多只能出14张牌 + if (cards.length > 14){ + do_returnfalse; + return can; + } + + /////////// 计算牌型 /////////// + //将牌从大到小排序 + cards = cls_youle_erqiwang_arith.order_cards(mainflower, cards); + var i = 0; + while (i < cards.length){ + //牌编码 + var code = cls_youle_erqiwang_arith.id_to_code(mainflower, cards[i]); + //牌的花色 + var flower = parseInt(code / 100); + if (code > 1000){ //大于1000是主牌 + flower = mainflower; + } + + //是第一张牌 + if (i == 0){ + can.flower = flower; + can.cardtype = 101; + can.cardvalue = code; + i++; + continue; + } + + //不是第一张牌 + //花色必须一致 + if (flower != can.flower){ + do_returnfalse(); + return can; + } + + //之前是单张牌型 + if (can.cardtype > 100 && can.cardtype < 200){ + if (can.cardtype == 101 && code == can.cardvalue){ + can.cardtype = 201; //是一对 + } else { + can.cardtype = can.cardtype + 1; //单张甩牌 + can.cardvalue = code; + } + i++; + continue; + } + + //之前是对子或拖拉机牌型,则两张两张牌一取 + //如果是最后一张牌,取不到后面一张牌了 + if (i == cards.length - 1){ + can.cardtype = 100 + i + 1; //单张甩牌 + can.cardvalue = code; + i++; + continue; + } + + //后面一张牌的编码 + var code_2 = cls_youle_erqiwang_arith.id_to_code(mainflower, cards[i + 1]); + //后面一张牌的花色 + var flower_2 = parseInt(code_2 / 100); + if (code_2 > 1000){ //大于1000是主牌 + flower_2 = mainflower; + } + //后面一张牌的花色必须一致 + if (flower_2 != flower){ + do_returnfalse(); + return can; + } + + //当前两张牌不是对子 + if (code_2 != code){ + can.cardtype = 100 + i + 2; //单张甩牌 + can.cardvalue = code_2; + i = i + 2; + continue; + } + + //当前两张牌是对子 + //之前是对子牌型 + if (can.cardtype > 200 && can.cardtype < 300){ + if (can.cardtype == 201 && cls_youle_erqiwang_arith.is_continuous(can.cardvalue, code)){ + //之前是一对,且现在是连牌,则是拖拉机 + can.cardtype = 302; + can.cardvalue = code; + } else { + //是多对 + can.cardtype = can.cardtype + 1; //多对甩牌 + can.cardvalue = code; + } + i = i + 2; + continue; + } + + //之前是拖拉机牌型 + if (can.cardtype > 300 && can.cardtype < 400){ + if (cls_youle_erqiwang_arith.is_continuous(can.cardvalue, code)){ + //现在是连牌,则还是拖拉机 + can.cardtype = can.cardtype + 1; + can.cardvalue = code; + } else { + //是多对 + can.cardtype = 200 + can.cardtype % 100 + 1; //多对甩牌 + can.cardvalue = code; + } + i = i + 2; + continue; + } + } + + /////////// 甩牌处理 /////////// + //其他玩家没有主牌 + var other_noflower = function(_flower){ + for (var i = 0; i < seatlist.length; i++){ + if (i != selfseat){ + if (seatlist[i][_flower - 1][0] == 0){ + return false; + } + } + } + return true; + } + + //其他玩家没有主对 + var other_noflowerpair = function(_flower){ + for (var i = 0; i < seatlist.length; i++){ + if (i != selfseat){ + if (seatlist[i][_flower - 1][1] == 0){ + return false; + } + } + } + return true; + } + + //不可甩牌判断 + if (can.cardtype > 101 && can.cardtype < 200){ //单张甩牌 + //其他玩家有主牌则不能甩牌 + if (!other_noflower(mainflower)){ + do_returnfalse(); + return can; + } + //副牌甩牌需要满足其他玩家没有主牌且其他玩家都没有该花色的牌 + if (!other_noflower(can.flower)){ + do_returnfalse(); + return can; + } + } else if (can.cardtype > 201 && can.cardtype < 300){ //对子甩牌 + if (can.flower == mainflower){ //甩主对 + //其他玩家有主牌对子则不能甩牌 + if (!other_noflowerpair(mainflower)){ + do_returnfalse(); + return can; + } + } else { //甩副牌对子 + //其他玩家有主牌则不能甩牌 + if (!other_noflower(mainflower)){ + do_returnfalse(); + return can; + } + //副牌甩牌需要其他玩家没有主牌且其他玩家没有该花色的对子 + if (!other_noflowerpair(can.flower)){ + do_returnfalse(); + return can; + } + } + } + + return can; + }, + + //根据第一个玩家的出牌获取后面玩家必出的和可出的牌 + get_followcard: function(mainflower, inhandcards, startcount, startflower, startcardtype){ + /* + 参数: + mainflower : 主牌花色 + inhandcards : 手上的牌id列表 + startcount : 第一个玩家出牌的数量 + startflower : 第一个玩家出牌的牌花色 + startcardtype: 第一个玩家出牌的牌型 + 返回结果json: + mustcard: 必出的牌列表 + cancard : 可出的牌列表 + cantype : 可出的牌中需要的牌型 + */ + //返回的结果 + var re = {}; + re.mustcard = []; + re.cancard = []; + re.cantype = null; + + //拷贝数组 + var _inhandcards = inhandcards.concat(); + //将牌从大到小排序 + _inhandcards = cls_youle_erqiwang_arith.order_cards(mainflower, _inhandcards); + + //如果手上的牌数量与要求的出牌数量一样则全是必出的牌 + if (_inhandcards.length == startcount){ + re.mustcard = _inhandcards; + re.cancard = []; + re.cantype = null; + return re; + } + + //获取相同花色的牌与其他花色的牌 + var _startflowercards = []; //相同花色的牌 + var _otherflowercards = []; //其他花色的牌 + for (var i = 0; i < _inhandcards.length; i++){ + //牌编码 + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _inhandcards[i]); + //牌的花色 + var _flower = parseInt(_code / 100); + if (_code > 1000){ //大于1000是主牌 + _flower = mainflower; + } + if (_flower == startflower){ + _startflowercards.push(_inhandcards[i]); + } else { + _otherflowercards.push(_inhandcards[i]); + } + } + + //如果没有相同花色的牌,则全部为可出的牌 + if (_startflowercards.length == 0){ + re.mustcard = []; + re.cancard = _inhandcards; + re.cantype = 100 + startcount; + return re; + } + + //如果相同花色的牌数量不够,则相同花色的牌为必出的牌,其他花色为可出的牌 + if (_startflowercards.length < startcount){ + re.mustcard = _startflowercards; + re.cancard = _otherflowercards; + re.cantype = 100 + startcount - re.mustcard.length; + return re; + } + + //如果相同花色的牌数量与要求的出牌数量一样则全是必出的牌,没有可出的牌 + if (_startflowercards.length == startcount){ + re.mustcard = _startflowercards; + re.cancard = []; + re.cantype = null; + return re; + } + + //如果相同花色的牌数量大于要求的出牌数量 + if (startcardtype == 101){ + //一张单张 + re.mustcard = []; + re.cancard = _startflowercards; + re.cantype = 100 + startcount; + return re; + } else if (startcardtype > 101 && startcardtype < 200){ + //多张单张 + re.mustcard = []; + re.cancard = _startflowercards; + re.cantype = 100 + startcount; + return re; + } else if (startcardtype == 201){ + //一对 + //获取对子列表 + var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _startflowercards); + //没有对子 + if (pairlist.length == 0){ + re.mustcard = []; + re.cancard = _startflowercards; + re.cantype = 100 + startcount; + return re; + } + //只有一对 + if (pairlist.length == 1){ + re.mustcard = pairlist[0]; + re.cancard = []; + re.cantype = null; + return re; + } + //有多对 + re.mustcard = []; + for (var i = 0; i < pairlist.length; i++){ + re.cancard = re.cancard.concat(pairlist[i]); + } + re.cantype = 201; + return re; + } else if (startcardtype > 201 && startcardtype < 300){ + //多对 + re.mustcard = []; + re.cancard = _startflowercards; + re.cantype = 100 + startcount; + return re; + } else if (startcardtype > 300 && startcardtype < 400){ + //拖拉机 + //获取对子列表 + var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _startflowercards); + + //没有对子 + if (pairlist.length == 0){ + re.mustcard = []; + re.cancard = _startflowercards; + re.cantype = 100 + startcount; + return re; + } + + //对子数量不够 + if (pairlist.length * 2 < startcount){ + for (var i = 0; i < pairlist.length; i++){ + re.mustcard = re.mustcard.concat(pairlist[i]); + } + re.cancard = min_ary_deduct(_startflowercards, re.mustcard); + re.cantype = 100 + startcount - re.mustcard.length; + return re; + } + + //对子数量刚好够 + if (pairlist.length * 2 == startcount){ + for (var i = 0; i < pairlist.length; i++){ + re.mustcard = re.mustcard.concat(pairlist[i]); + } + re.cancard = []; + re.cantype = null; + return re; + } + + //对子有多 + //拖拉机列表 + var tuolaji_list = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, pairlist, startcardtype); + + //有多个符合条件的拖拉机 + if (tuolaji_list.length > 1){ + re.mustcard = []; + re.cancard = []; + //这里要去重复项 + for (var i = 0; i < tuolaji_list.length; i++){ + for (var j = 0; j < tuolaji_list[i].length; j++) { + var found = false; + for (var k = 0; k < re.cancard.length; k++){ + if (re.cancard[k] == tuolaji_list[i][j]){ + found = true; + break; + } + } + if (!found){ + re.cancard.push(tuolaji_list[i][j]); + } + } + } + re.cantype = startcardtype; + return re; + } + + //只有一个符合条件的拖拉机 + if (tuolaji_list.length == 1){ + re.mustcard = tuolaji_list[0]; + re.cancard = []; + re.cantype = null; + return re; + } + + //没有符合条件的拖拉机 + if (tuolaji_list.length == 0){ + re.mustcard = []; + for (var i = 0; i < pairlist.length; i++){ + re.cancard = re.cancard.concat(pairlist[i]); + } + re.cantype = startcardtype - 100; + //如果是连两对拖拉机 + if (startcardtype == 302){ + return re; + } + + //如果是3连对及以上的拖拉机则降低拖拉机的规格再继续找 + var tlj_type = startcardtype; //需要的牌型 + var tlj_find = startcardtype - 1; //当前查找的牌型 + //再继续找 + while (tlj_type >= 302 && tlj_find >= 302){ + var tlj_list = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, pairlist, tlj_find); + if (tlj_list.length == 1){ + //找到一个 + re.mustcard = re.mustcard.concat(tlj_list[0]); + re.cantype = re.cantype - (tlj_find - 100) + 200; + if (re.cantype < 201){ + re.cancard = []; + re.cantype = null; + return re; + } + re.cancard = min_ary_deduct(re.cancard, tlj_list[0]); + //删除对子列表 + i = 0; + while (i < pairlist.length){ + if (min_ary_include(tlj_list[0], pairlist[i])){ + pairlist.splice(i, 1); + } else { + i++; + } + } + tlj_type = (tlj_type - tlj_find) + 300; + continue; + } + + if (tlj_list.length > 1){ + //找到多个 + if ((tlj_find - 300) * tlj_list.length <= (tlj_type - 300)){ + re.mustcard = re.mustcard.concat(tlj_list[0]); + re.cantype = re.cantype - (tlj_find - 100) + 200; + if (re.cantype < 201){ + re.cancard = []; + re.cantype = null; + return re; + } + re.cancard = min_ary_deduct(re.cancard, tlj_list[0]); + //删除对子列表 + i = 0; + while (i < pairlist.length){ + if (min_ary_include(tlj_list[0], pairlist[i])){ + pairlist.splice(i, 1); + } else { + i++; + } + } + tlj_type = (tlj_type - tlj_find) + 300; + continue; + } else { + return re; + } + } + tlj_find = tlj_find - 1; + } + return re; + } + } + }, + + //第二、三个位置是否可跟牌 + can_followcard: function(mainflower, inhandcards, followcards, startcount, startflower, startcardtype){ + /* + 参数: + mainflower : 主牌花色 + inhandcards : 手上的牌id列表 + followcards : 要跟的牌id列表 + startcount : 第一个玩家出牌的数量 + startflower : 第一个玩家出牌的牌花色 + startcardtype: 第一个玩家出牌的牌型 + 返回结果json: + result : true可跟,false不可跟 + cardvalue: 满足第一个玩家出牌的牌型值 + noflower : 出牌后没有了第一个玩家出牌花色的牌标志 + nopair : 出牌后没有了第一个玩家出牌花色的对子标志 + */ + + //返回的结果 + var can = {}; + can.result = true; + can.cardvalue = null; + can.noflower = false; + can.nopair = false; + + //返回不能出牌的结果 + var do_returnfalse = function(){ + can.result = false; + delete can.cardvalue; + delete can.noflower; + delete can.nopair; + } + + //牌的数量必须相等 + if (followcards.length != startcount){ + do_returnfalse(); + return can; + } + + //拷贝数组 + var _inhandcards = inhandcards.concat(); + var _followcards = followcards.concat(); + + //将牌从大到小排序 + _inhandcards = cls_youle_erqiwang_arith.order_cards(mainflower, _inhandcards); + _followcards = cls_youle_erqiwang_arith.order_cards(mainflower, _followcards); + + //根据第一个玩家的出牌获取后面玩家必出的和可出的牌 + var get = cls_youle_erqiwang_arith.get_followcard(mainflower, _inhandcards, startcount, startflower, startcardtype); + + ////////// 判断是否可跟牌 /////////// + //如果有必出的牌则必须包含必出的牌 + if (get.mustcard.length > 0){ + if (!min_ary_include(_followcards, get.mustcard)){ + do_returnfalse(); + return can; + } + //除去必出的牌还要出的牌 + _followcards = min_ary_deduct(_followcards, get.mustcard); + } + + if (_followcards.length > 0){ + //必须在可出的牌里面 + if (!min_ary_include(get.cancard, _followcards)){ + do_returnfalse(); + return can; + } + + //必须满足需要的牌型 + if (get.cantype > 100 && get.cantype < 200){ + //单张牌型 + //牌数量要正确 + if (_followcards.length != get.cantype - 100){ + do_returnfalse(); + return can; + } + } else if (get.cantype > 200 && get.cantype < 300){ + //对子牌型 + //牌数量要正确 + if (_followcards.length != (get.cantype - 200) * 2){ + do_returnfalse(); + return can; + } + //对子数量要正确 + var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _followcards); + if (pairlist.length != get.cantype - 200){ + do_returnfalse(); + return can; + } + } else if (get.cantype > 300 && get.cantype < 400){ + //拖拉机牌型 + //牌数量要正确 + if (_followcards.length != (get.cantype - 300) * 2){ + do_returnfalse(); + return can; + } + //要是正确的拖拉机牌型 + if (tuolaji_list.length == 0){ + do_returnfalse(); + return can; + } + } + } + + /////////// 可以跟牌 /////////// + //根据跟牌情况获取是否有相同花色的牌,是否有对子 + //如果第一张牌的花色与第一家的出牌花色不一样则认为没有了相同花色的牌 + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, followcards[0]); + var _flower = parseInt(_code / 100); + if (_code > 1000){ + _flower = mainflower; + } + if (_flower != startflower){ + can.noflower = true; + can.nopair = true; + } + //如果最后一张牌的花色与第一家的出牌花色不一样则认为没有了相同花色的牌 + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, followcards[followcards.length - 1]); + var _flower = parseInt(_code / 100); + if (_code > 1000){ + _flower = mainflower; + } + if (_flower != startflower){ + can.noflower = true; + can.nopair = true; + } + //如果第一家出的是对子或拖拉机牌型,而跟牌没有出对子则认为没有相同花色的对子 + if (startcardtype > 200){ + var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, followcards); + if (pairlist.length != startcardtype % 100){ + can.nopair = true; + } + } + + //根据第一家的出牌计算牌值大小 + //一张单张 + if (startcardtype == 101){ + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, followcards[0]); + var _flower = parseInt(_code / 100); + if (_code > 1000){ //大于1000是主牌 + _flower = mainflower; + } + + if (_flower == startflower || _flower == mainflower){ + if (_code > 7000 && _code < 8000){ //副7 + can.cardvalue = 7000; + } else if (_code > 2000 && _code < 3000){ //副2 + can.cardvalue = 2000; + } else { + can.cardvalue = _code; + } + } else { + can.cardvalue = 0; + } + return can; + } + + //甩多张单张 + if (startcardtype > 101 && startcardtype < 200){ + //第一个玩家是甩牌,则跟牌一定没有第一家玩家的牌大 + can.cardvalue = 0; + return can; + } + + //一对 + if (startcardtype == 201){ + var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, followcards); + if (pairlist.length != 1){ + can.cardvalue = 0; + return can; + } + + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, followcards[0]); + var _flower = parseInt(_code / 100); + if (_code > 1000){ + _flower = mainflower; + } + if (_flower == startflower || _flower == mainflower){ + if (_code > 7000 && _code < 8000){ //副7 + can.cardvalue = 7000; + } else if (_code > 2000 && _code < 3000){ //副2 + can.cardvalue = 2000; + } else { + can.cardvalue = _code; + } + } else { + can.cardvalue = 0; + } + return can; + } + + //甩多对 + if (startcardtype > 201 && startcardtype < 300){ + //第一个玩家是甩牌,则跟牌一定没有第一家玩家的牌大 + can.cardvalue = 0; + return can; + } + + //拖拉机 + if (startcardtype > 300 && startcardtype < 400){ + var pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, followcards); + var tuolaji_list = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, pairlist, startcardtype); + if (tuolaji_list.length != 1){ + can.cardvalue = 0; + return can; + } + + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, followcards[0]); + var _flower = parseInt(_code / 100); + if (_code > 1000){ + _flower = mainflower; + } + if (_flower == startflower || _flower == mainflower){ + can.cardvalue = _code; + } else { + can.cardvalue = 0; + } + return can; + } + }, + + //闲家抠底倍数 + get_bottom_multiple: function(mainflower, cards){ + //拷贝数组 + var _cards = cards.concat(); + //将牌从大到小排序 + _cards = cls_youle_erqiwang_arith.order_cards(mainflower, _cards); + + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[0]); + if (_code < 1000){ + return 0; + } + var _code = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[_cards.length - 1]); + if (_code < 1000){ + return 0; + } + + var _pairlist = cls_youle_erqiwang_arith.get_pairlist(mainflower, _cards); + if (_pairlist.length == 0){ + return 2; + } + + var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 302); + if (_tljlist.length == 0){ + return 4; + } + + var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 303); + if (_tljlist.length == 0){ + return 8; + } + + var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 304); + if (_tljlist.length == 0){ + return 16; + } + + var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 305); + if (_tljlist.length == 0){ + return 32; + } + + var _tljlist = cls_youle_erqiwang_arith.get_tuolaji_list(mainflower, _pairlist, 306); + if (_tljlist.length == 0){ + return 64; + } + }, + + //冲关计算关数 + get_chongguan: function(mainflower, cards){ + var re = {}; + re.count = 0; //关数 + re.wang = 0; //王牌数量 + re.cards = []; //王牌和冲关的牌 + if (cards.length < 28){ + return re; + } + + //拷贝数组 + var _cards = cards.concat(); + //将牌从大到小排序 + _cards = cls_youle_erqiwang_arith.order_cards(mainflower, _cards); + + var _wlist = []; //王 + var _2list = []; //2 + var _7list = []; //7 + var _incards = []; //是否添加得到了re.cards标志 + for (var i = 0; i < _cards.length; i++) { + var _number = cls_youle_erqiwang_arith.id_to_number(_cards[i]); + if (_number >= 53){ + _wlist.push(i); + re.wang = re.wang + 1; + re.cards.push(_cards[i]); + } else if (_number == 2){ + _2list.push(i); + } else if (_number == 7){ + _7list.push(i); + } + _incards.push(0); + } + + //添加到re.cards + var do_addlisttocards = function(_list){ + for (var i = 0; i < _list.length; i++){ + if (!_incards[_list[i]]){ + re.cards.push(_cards[_list[i]]); + _incards[_list[i]] = 1; + } + } + } + + //三王四王冲关判断 + if (_wlist.length >= 3){ + re.count = 1; + var _code = 9553; //当前的牌编码 + var i = 3; //后面查找的起始位置 + if (_wlist.length == 4){ + re.count = 3; + i = 4; + } + //往后查找,看是否还有冲关 + while (i < _cards.length - 1){ + //牌编码,两张牌两张牌一取 + var code_i = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[i]); + var code_j = cls_youle_erqiwang_arith.id_to_code(mainflower, _cards[i + 1]); + if (code_i == code_j){ + if (cls_youle_erqiwang_arith.is_continuous(_code, code_i)){ + re.count = re.count + 1; + re.cards.push(_cards[i]); + re.cards.push(_cards[i + 1]); + _incards[i] = 1; + _incards[i + 1] = 1; + _code = code_i; + } + i = i + 2; + } else { + i++; + } + } + } + + //六2六7判断 + if (_7list.length >= 6){ + re.count = re.count + _7list.length - 5; + do_addlisttocards(_7list); + } + if (_2list.length >= 6){ + re.count = re.count + _2list.length - 5; + do_addlisttocards(_2list); + } + + //10个老主判断 + var _total = _wlist.length + _7list.length + _2list.length; + if (_total >= 10){ + re.count = re.count + _total - 9; + do_addlisttocards(_7list); + do_addlisttocards(_2list); + } + + re.cards = cls_youle_erqiwang_arith.order_cards(mainflower, re.cards); + return re; + }, + + //计算升级级数 + get_upgrade: function(call, grade){ + //叫分的一半 + var _halfcall; + if (call % 2 == 0){ + _halfcall = call / 2; + } else { + _halfcall = (call + 5) / 2; + } + + if (grade == 0){ + return 4; + } + if (grade < _halfcall){ + return 2; + } + if (grade < call){ + return 1; + } + return -1 * (parseInt((grade - call) / _halfcall) + 1); + } +} + diff --git a/server/games/erqiwang/class.desk.js b/server/games/erqiwang/class.desk.js new file mode 100644 index 0000000..e504a74 --- /dev/null +++ b/server/games/erqiwang/class.desk.js @@ -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; + } +} diff --git a/server/games/erqiwang/class.export.js b/server/games/erqiwang/class.export.js new file mode 100644 index 0000000..8520d90 --- /dev/null +++ b/server/games/erqiwang/class.export.js @@ -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(); \ No newline at end of file diff --git a/server/games/erqiwang/class.import.js b/server/games/erqiwang/class.import.js new file mode 100644 index 0000000..4c09150 --- /dev/null +++ b/server/games/erqiwang/class.import.js @@ -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(); \ No newline at end of file diff --git a/server/games/erqiwang/class.pai.js b/server/games/erqiwang/class.pai.js new file mode 100644 index 0000000..a9e674e --- /dev/null +++ b/server/games/erqiwang/class.pai.js @@ -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; + } +} diff --git a/server/games/erqiwang/class.paiju.js b/server/games/erqiwang/class.paiju.js new file mode 100644 index 0000000..62c0f3a --- /dev/null +++ b/server/games/erqiwang/class.paiju.js @@ -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; + } +} diff --git a/server/games/erqiwang/docs/design/design.md b/server/games/erqiwang/docs/design/design.md new file mode 100644 index 0000000..e64a634 --- /dev/null +++ b/server/games/erqiwang/docs/design/design.md @@ -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. **有分**:提示对家"我手上有分"。 diff --git a/server/games/erqiwang/docs/protocol/packet_protocol.md b/server/games/erqiwang/docs/protocol/packet_protocol.md new file mode 100644 index 0000000..4ddc377 --- /dev/null +++ b/server/games/erqiwang/docs/protocol/packet_protocol.md @@ -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": [] // 发牌出牌情况,详情见代码中的注释 +} +``` diff --git a/server/games/erqiwang/docs/protocol/二七王接口.xlsx b/server/games/erqiwang/docs/protocol/二七王接口.xlsx new file mode 100644 index 0000000..cda4310 Binary files /dev/null and b/server/games/erqiwang/docs/protocol/二七王接口.xlsx differ diff --git a/server/games/erqiwang/mod.js b/server/games/erqiwang/mod.js new file mode 100644 index 0000000..ff1361d --- /dev/null +++ b/server/games/erqiwang/mod.js @@ -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); +} \ No newline at end of file