From 2434d0f15e352358d3eea98519b632829fe09ab3 Mon Sep 17 00:00:00 2001
From: yafeng2003 <3274412286@qq.com>
Date: Thu, 23 Jul 2026 16:50:12 +0800
Subject: [PATCH 01/27] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=99=BE?=
=?UTF-8?q?=E4=BD=99=E9=A1=B9DLC=E5=B9=B6=E5=85=A8=E9=9D=A2=E5=8D=87?=
=?UTF-8?q?=E7=BA=A7=E5=AF=B9=E5=B1=80=E4=BD=93=E9=AA=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 1 +
docs/archive/DLC.json | 263 +-
simple-bot/who_designed_adapter.py | 182 +
tractor-game-simulator/.gitignore | 2 +
tractor-game-simulator/client/package.json | 18 +-
.../client/playwright.config.js | 16 +
tractor-game-simulator/client/public/DLC.json | 277 +-
tractor-game-simulator/client/src/App.jsx | 63 +-
.../client/src/components/Game/Card.css | 639 +-
.../client/src/components/Game/Card.jsx | 189 +-
.../client/src/components/Game/GameBoard.css | 773 +-
.../client/src/components/Game/GameBoard.jsx | 8781 ++++++++++--
.../client/src/components/Game/GameTable.css | 3345 ++++-
.../client/src/components/Game/GameTable.jsx | 1960 ++-
.../client/src/components/Game/Hand.css | 107 +-
.../client/src/components/Game/Hand.jsx | 244 +-
.../src/components/Game/OpenHandPanel.jsx | 78 +
.../src/components/Game/RuleSelector.css | 101 +-
.../src/components/Game/RuleSelector.jsx | 312 +-
.../src/components/Game/TrumpDeclaration.css | 73 +-
.../src/components/Game/TrumpDeclaration.jsx | 66 +-
.../src/components/Room/CreateRoomModal.jsx | 34 +-
.../client/src/components/Room/RoomList.jsx | 4 +-
.../client/src/store/gameStore.js | 31 +-
.../client/src/styles/global.css | 2 +
.../client/src/utils/actionAvailability.js | 1082 ++
.../client/src/utils/cardPatternUtils.js | 1256 +-
.../client/src/utils/cardUtils.js | 69 +-
.../client/src/utils/constants.js | 85 +-
.../client/src/utils/gameViewUtils.js | 44 +
.../client/src/utils/ironEvidenceUtils.js | 8 +
.../client/src/utils/ruleCatalog.js | 115 +
.../client/src/utils/scoringUtils.js | 128 +
.../client/src/utils/trumpUtils.js | 65 +
.../client/test/actionAvailability.test.mjs | 2468 ++++
.../client/test/e2e/game-ui.spec.js | 3258 +++++
.../test/fixtures/enduring-preview.html | 78 +
.../client/test/fixtures/joker-preview.html | 46 +
.../test/fixtures/table-layout-preview.html | 75 +
.../fixtures/wooden-ox-layout-preview.html | 96 +
.../client/test/gameViewUtils.test.mjs | 76 +
.../client/test/global-setup.js | 77 +
.../client/test/ruleCatalog.test.mjs | 91 +
.../client/test/scoringUtils.test.mjs | 164 +
tractor-game-simulator/server/package.json | 4 +-
.../server/src/models/Card.js | 32 +-
.../server/src/models/GameState.js | 981 +-
.../server/src/models/Room.js | 90 +-
.../server/src/rules/ruleRegistry.js | 1418 ++
.../server/src/services/BotService.js | 381 +-
.../server/src/services/DeckService.js | 20 +
.../src/services/DrawingPhaseManager.js | 255 +-
.../server/src/services/GameEngine.js | 11389 +++++++++++++++-
.../server/src/services/RoundManager.js | 52 +-
.../src/services/whoDesignedStrategy.js | 226 +
.../src/socket/handlers/gameHandlers.js | 2273 ++-
.../src/socket/handlers/playerHandlers.js | 176 +-
.../src/socket/handlers/roomHandlers.js | 14 +-
.../server/src/utils/cardCooldownUtils.js | 167 +
.../server/src/utils/cardPatternUtils.js | 2070 ++-
.../server/src/utils/constants.js | 37 +-
.../server/src/utils/ironEvidenceUtils.js | 43 +
.../src/utils/oneCountryTwoSystemsUtils.js | 148 +
.../server/src/utils/pokerUtils.js | 153 +
.../server/src/utils/scoringUtils.js | 73 +-
.../src/utils/strengthCompensationUtils.js | 114 +
.../server/src/utils/threeTigersUtils.js | 59 +
.../server/test/botIntegration.mjs | 106 +
.../server/test/specialRules.test.mjs | 10988 +++++++++++++++
.../server/test/whoDesignedStrategy.test.mjs | 140 +
70 files changed, 55350 insertions(+), 2901 deletions(-)
create mode 100644 simple-bot/who_designed_adapter.py
create mode 100644 tractor-game-simulator/client/playwright.config.js
create mode 100644 tractor-game-simulator/client/src/components/Game/OpenHandPanel.jsx
create mode 100644 tractor-game-simulator/client/src/utils/actionAvailability.js
create mode 100644 tractor-game-simulator/client/src/utils/gameViewUtils.js
create mode 100644 tractor-game-simulator/client/src/utils/ironEvidenceUtils.js
create mode 100644 tractor-game-simulator/client/src/utils/ruleCatalog.js
create mode 100644 tractor-game-simulator/client/src/utils/scoringUtils.js
create mode 100644 tractor-game-simulator/client/test/actionAvailability.test.mjs
create mode 100644 tractor-game-simulator/client/test/e2e/game-ui.spec.js
create mode 100644 tractor-game-simulator/client/test/fixtures/enduring-preview.html
create mode 100644 tractor-game-simulator/client/test/fixtures/joker-preview.html
create mode 100644 tractor-game-simulator/client/test/fixtures/table-layout-preview.html
create mode 100644 tractor-game-simulator/client/test/fixtures/wooden-ox-layout-preview.html
create mode 100644 tractor-game-simulator/client/test/gameViewUtils.test.mjs
create mode 100644 tractor-game-simulator/client/test/global-setup.js
create mode 100644 tractor-game-simulator/client/test/ruleCatalog.test.mjs
create mode 100644 tractor-game-simulator/client/test/scoringUtils.test.mjs
create mode 100644 tractor-game-simulator/server/src/rules/ruleRegistry.js
create mode 100644 tractor-game-simulator/server/src/services/whoDesignedStrategy.js
create mode 100644 tractor-game-simulator/server/src/utils/cardCooldownUtils.js
create mode 100644 tractor-game-simulator/server/src/utils/ironEvidenceUtils.js
create mode 100644 tractor-game-simulator/server/src/utils/oneCountryTwoSystemsUtils.js
create mode 100644 tractor-game-simulator/server/src/utils/pokerUtils.js
create mode 100644 tractor-game-simulator/server/src/utils/strengthCompensationUtils.js
create mode 100644 tractor-game-simulator/server/src/utils/threeTigersUtils.js
create mode 100644 tractor-game-simulator/server/test/botIntegration.mjs
create mode 100644 tractor-game-simulator/server/test/specialRules.test.mjs
create mode 100644 tractor-game-simulator/server/test/whoDesignedStrategy.test.mjs
diff --git a/.gitignore b/.gitignore
index ea01206..bcad0a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ playing-cards-assets/
rotate_svg.py
rotate.py
.DS_Store
+/WhoDesigned/
diff --git a/docs/archive/DLC.json b/docs/archive/DLC.json
index f6c40b2..3da23f4 100644
--- a/docs/archive/DLC.json
+++ b/docs/archive/DLC.json
@@ -8,8 +8,9 @@
"content": "说一句话需要扣五分。"
},
{
+ "id": "ten_sided_ambush",
"name": "十面埋伏",
- "content": "庄家队友暗中指定一个点数具有赋分功能(+5,-5)。"
+ "content": "庄家队友暗选非级牌、非分牌点数。闲家赢得每张-5分,庄家方赢得每张给闲家+5分;底牌乘倍数同理,点数首次出现时公开。"
},
{
"name": "世事无常",
@@ -32,232 +33,280 @@
"content": "庄家16张底牌。闲家开局为60分。"
},
{
+ "id": "perfect_strategy",
"name": "算无遗策",
- "content": "庄家队友明手。闲家开局为10分。"
+ "content": "庄家埋底后,庄家队友明手且由庄家代为出牌。闲家开局为10分。"
},
{
+ "id": "know_yourself_and_enemy",
"name": "知己知彼",
"content": "摸牌后,与对家交换两张牌。"
},
{
+ "id": "news_minister_i",
"name": "新闻部长I",
"content": "摸牌后,每人给上家两张牌。"
},
{
+ "id": "news_minister_ii",
"name": "新闻部长II",
"content": "摸牌后,每人给下家两张牌。"
},
{
+ "id": "irresistible_force",
"name": "势如破竹",
"content": "本局庄家获胜可以连庄。"
},
{
+ "id": "substitute_sacrifice",
"name": "李代桃僵",
- "content": "玩家可以任意垫一次牌,需要声明(视为小)。"
+ "content": "每名玩家限一次,可在跟牌时主动声明垫牌:仍须出相同张数,但可无视花色与牌型要求,且本次出牌始终视为小。"
},
{
+ "id": "six_six_great_success",
"name": "六六大顺",
- "content": "本局具有同花顺(6张起步)。"
+ "content": "新增同花顺牌型,至少六张连续牌;主同花顺可包含级牌和王牌。"
},
{
+ "id": "single_step_debug",
"name": "单步调试",
"content": "本局移除甩牌。"
},
{
+ "id": "one_horse_leads",
"name": "一马当先",
- "content": "庄家队友先出牌。"
+ "content": "仅第一轮改由庄家队友先出牌;庄家身份、底牌归属及后续轮次均不改变。"
},
{
+ "id": "reform_and_opening_up",
"name": "改革开放",
- "content": "庄家队友可以再埋一次底牌。闲家获得40分。"
+ "content": "庄家首次埋底后,庄家队友拿起底牌并重新埋底,随后由庄家先出牌。闲家开局获得40分。"
},
{
+ "id": "heavy_fog",
"name": "迷雾重重",
- "content": "开局移除牌堆顶8张牌,闲家获得此8张牌中一半分数。"
+ "content": "开局暗中移除牌堆顶8张牌;全局出牌及其他分数结算完成后公开,闲家补得其中总分的一半。"
},
{
+ "id": "concealed_passage",
"name": "暗度陈仓",
- "content": "每名玩家限1次,若不是第一个出牌,可以背面向上打出。"
+ "content": "每名玩家每局限1次;跟牌时可暗置出牌,除自己外本轮结束前无人能看到。一轮可有多人发动,轮末同时公开并正常结算得分和下一轮牌权。"
},
{
"name": "冰山一角",
- "content": "始终保持两张牌明置。"
+ "content": "进入出牌阶段后,每名玩家自行选择并始终保持两张手牌明置;不足两张时全部明置。"
},
{
+ "id": "fatal_beauty",
"name": "红颜祸水",
- "content": "黑桃牌视为红桃牌,闲家开局为20分;若红桃为主花色,闲家开局为40分。"
+ "content": "黑桃牌视为红桃牌,但不新增任何牌型;闲家开局为20分,若红桃为主花色则为40分。"
},
{
+ "id": "stealing_beams",
"name": "偷梁换柱",
- "content": "四张王牌可以当做任意数字牌使用,若当做分数牌则不计分。"
+ "content": "每名玩家每局限1次;发动后点击要转换的王牌,先选花色再选点数,转换完成后再自行选牌按基本规则出牌。转换可取消,王牌替成分牌仍不计分,单出可保持王牌。"
},
{
+ "id": "cosmic_shift",
"name": "斗转星移",
- "content": "当手牌数首次变为不大于12张时所有玩家与队友交换手牌。"
+ "content": "一轮结束后,若四家手牌均首次不多于12张,所有玩家与队友交换全部手牌。"
},
{
+ "id": "last_stand",
"name": "绝处逢生",
- "content": "本局不能是无主局;当玩家没有主牌时,若此时手牌数不小于5且花色相同,可以令每张牌视为主牌。"
+ "content": "本局不能是无主局,也不能用一对王反主;无人亮主时系统随机指定主花色。手牌变化后,若一名玩家不少于5张、没有主牌且全为同一花色,可选择令全部手牌视为主牌;暂时拒绝后仍可在后续手牌变化时再次选择。"
},
{
+ "id": "late_mover_advantage",
"name": "后发制人",
- "content": "每名玩家限1次,作为3号位时可以令原本的4号位先出牌。"
+ "content": "每名玩家每局限1次;作为本轮3号位出牌前,可以令下家先出,自己改为本轮最后出牌。只改变出牌顺序,不改变牌的大小结算。"
},
{
+ "id": "frequent_fluctuation",
"name": "频繁波动",
"content": "有分数牌被打出的轮次结束后,每人交给下家一张牌,闲家开局-10分。"
},
{
+ "id": "minor_disturbance",
"name": "微小扰动",
"content": "没有分数牌被打出的轮次结束后,每人交给队友一张牌。"
},
{
+ "id": "lingering_discard",
"name": "弃掷逦迤",
- "content": "有分数牌被打出的轮次结束后,每人弃一张牌(庄家弃分则闲家加分)。"
+ "content": "有分数牌被打出的轮次结束后,四家各自暗弃一张牌;终局仅公开庄家方两人弃出的分牌,并补给闲家。"
},
{
+ "id": "time_reversal",
"name": "时间倒流",
- "content": "每名玩家限1次,在一轮出牌结束后可以选择回溯到上一轮出牌结束时的状态。"
+ "content": "每名玩家每局限一次,可在本轮及轮末额外停留的1秒内预备;轮末询问所有预备者,首个确认发动者令牌局回溯到本轮开始前。"
},
{
"name": "路线摇摆",
- "content": "有不小于10分的牌被打出的轮次结束后逆转出牌顺序。"
+ "content": "有单张价值不小于10分的牌被打出的轮次结束后,顺时针与逆时针出牌顺序互换。"
},
{
"name": "一带一路",
- "content": "可以使用“小甩牌”牌型(两张单牌)。"
+ "content": "每名玩家每局限一次,首发时可点击“一带一路”,再出同一有效花色的两张非对子单牌,无需满足甩牌必大条件;未发动时仍可按普通甩牌尝试出这两张牌。跟牌或毙牌不消耗次数。"
},
{
"name": "昼夜轮转",
- "content": "第 x 轮中,点数为 x % 13 + 1 的牌在其相应花色内视为最大,闲家开局为20分。"
+ "content": "第x轮将点数x % 13 + 1提升为相应花色内最大,其他牌序不变;若轮转到级牌则仍按A最大。闲家开局为20分。"
},
{
"name": "尊老爱幼",
- "content": "每轮打出最小牌的玩家在下一轮先出牌。"
+ "content": "每轮仍按常规规则结算大牌;另以一号位牌型为准寻找最小牌:同牌型按正常牌力,牌型不一致时先比结构贴合度,异花色牌越多越小(包括主牌),数量相同再从最小牌起按字典序比较,完全相同时后出者更小。最小者下一轮先出牌。"
},
{
+ "id": "rites_collapse",
"name": "礼崩乐坏",
- "content": "每轮的首位玩家不能主动打出A。"
+ "content": "每轮一号位主动出牌时不能选择任何A;跟牌时仍可正常打出A。若一号位手中只剩A,则可正常出牌。"
},
{
+ "id": "recommend_talent",
"name": "举贤任能",
- "content": "每名玩家限一次,作为1号位时可以将出牌权交给下家。"
+ "content": "每名玩家每局限一次;作为本轮一号位出牌前,可令下家先出,自己改为本轮最后出牌。只改变牌序,不改变牌的大小。"
},
{
+ "id": "accident_insurance",
"name": "意外保险",
- "content": "单轮得分超过30的部分由对方获得。"
+ "content": "单轮牌面分超过30时,超出部分归另一方:闲家赢牌最多计30分;庄家方赢牌则将超出30的部分补给闲家。"
},
{
"name": "为人坦荡",
- "content": "手牌数首次变化为不大于5时所有玩家明置手牌。"
+ "content": "一轮结束时,若所有玩家手牌均不多于5张,所有玩家同时明置剩余手牌。"
},
{
+ "id": "three_powers",
"name": "三权分立",
- "content": "游戏开始时,2、3、4号位玩家各暗中指定一个数字(不能是主点数),并令本局中这三者点数的牌分别代替原本10、5、K的分数."
+ "content": "庄家埋底后,2、3、4号位分别暗选一个非级牌点数,依次重载原10、5、K分牌。可选择原5、10、K,也可重复选择并叠加分值;对应点数首次出现时公开。"
},
{
+ "id": "gentleman_promise",
"name": "君子一言",
- "content": "每名玩家开局时声明自己最少的花色(包括0张)。"
+ "content": "庄家埋底后,每名玩家公开声明自己手牌最少的有效花色(包括0张)。所有主牌统一算作“主”,不再计入牌面原花色;最少花色唯一时由系统直接声明,并列时由玩家选择。"
},
{
+ "id": "repeated_exhaustion",
"name": "再衰三竭",
- "content": "每名玩家连续3次最大时,从第3轮开始,每次再作为一号位将失去依次失去5,10,15……分/"
+ "content": "同一名玩家连续赢得第3、4、5……轮时依次失去5、10、15……分;换人赢牌后由新赢家从第1轮重新累计。庄家方失分补给闲家,闲家失分则从闲家总分扣除。"
},
{
+ "id": "focus_figure",
"name": "焦点人物",
- "content": "游戏开始前在两方阵营中随机各选择一名成为“焦点”(仅队友相互知晓),本局焦点打出的分数牌分数翻倍,非焦点打出的分数牌不计分。"
+ "content": "埋底后,两队分别秘密表决本队焦点;有人反对就轮换候选并重新表决,直至队内两人一致同意。本局闲家拿到的焦点人物分牌按双倍计分,非焦点人物分牌不计分;底牌正常结算,焦点与实际总分在终局揭晓。"
},
{
+ "id": "cooldown_time",
"name": "冷却时间",
- "content": "每轮的出牌不能包含上轮自己出牌含有的点数(除非只有该点数)。"
+ "content": "每轮的出牌不能包含上轮自己出牌含有的点数;若其余可用牌不足以完成本次出牌,则解除冷却。"
},
{
+ "id": "time_cooling",
"name": "时间冷却",
- "content": "每轮的出牌不能包含上轮自己出牌含有的花色(除非只有该花色)。"
+ "content": "每轮的出牌不能包含上轮自己出牌含有的有效花色,所有主牌统一视为“主”;若其余可用牌不足以完成本次出牌,则解除冷却。"
},
{
+ "id": "planned_economy",
"name": "计划经济",
- "content": "起始手牌为20张,每轮结束后再摸一轮牌直到没有牌剩余。"
+ "content": "起始每人20张手牌,另封存20张;庄家埋底并开始出牌后,每轮结束四家各摸1张,直至封存牌摸完。"
},
{
+ "id": "go_with_the_flow",
"name": "随波逐流",
- "content": "手牌数首次不大于16时将手牌交给下家,首次不大于9时再交给下家。"
+ "content": "一轮结束后,若四家手牌均首次不多于16张则把全部手牌交给下家;均首次不多于9张时再交换一次。"
},
{
+ "id": "equivalent_reciprocity",
"name": "等价互惠",
- "content": "每名玩家限一次,作为首位出牌时可以与另一名玩家拼点(比较规则为无主局的大小顺序,不区分花色),输的人失去5分,之后交换两张拼点牌。"
+ "content": "每名玩家每局限一次;作为本轮一号位出牌前,可与另一名玩家各暗选一张牌拼点。主牌大于副牌,副牌只按点数比较;输家使本方损失5分,平局不失分,随后交换两张拼点牌。"
},
{
+ "id": "enduring",
"name": "经久不衰",
- "content": "单牌/对子的大小视为本轮和上轮同牌型的最大值。"
+ "content": "每名玩家本轮出牌与自己上轮的有效花色、牌型、张数及组合结构完全一致时,本轮该组合的比较牌力视为两轮中的最大值;主牌统一视为“主”,甩牌必须逐项匹配同样的组件结构。"
},
{
+ "id": "average_pooling",
"name": "平均池化",
"content": "单牌/对子的大小视为自身和队友的平均值(垫牌、散牌不参与计算)。"
},
{
+ "id": "dream_killing",
"name": "梦中杀人",
- "content": "玩家没有主牌时可以暗置所有牌,之后出牌完全随机,若随机出的牌与首位玩家花色相同,则视为最大并“醒来”。"
+ "content": "玩家没有主牌时可以暗置所有牌,之后出牌完全随机;若随机出的牌与首位玩家花色或点数相同,则视为最大并“醒来”。若有两名及以上玩家成功,以先出者为大。"
},
{
+ "id": "joint_harmony",
"name": "珠联璧合",
- "content": "若一方打出完全相同的牌,则视为最大。"
+ "content": "若一方打出完全相同的牌,则视为最大,下一轮由该方后出者获得牌权;若两队同时珠联璧合,则按牌面正常结算。"
},
{
+ "id": "divine_weapon",
"name": "神兵天降",
- "content": "游戏开始时另外随机抽取2张牌并亮出,每名玩家可以将一张花色或点数与之相同的牌当作此牌打出并再翻出2张(每名玩家限一次)。"
+ "content": "从另一副不含王的牌中随机亮出2张神兵牌。每名玩家每局限一次,可将一张与所选神兵牌花色或点数相同的手牌当作该牌打出,牌面与牌力改变但仍按原牌计分,且跟牌义务按转化前的真实手牌判断。本轮有人发动后其他人不能再发动;轮末两张神兵牌一起替换,未发动则两张都保留。"
},
{
"name": "魔术戏法",
- "content": "每名玩家限一次,在作为一号位出牌前可以暗中选择两名玩家,交换本轮二者打出的牌。"
+ "content": "每名玩家限一次,在作为一号位出牌前可以暗中选择两名其他玩家,不能选择自己。两人仍按自己的手牌正常出牌,仅在本轮最终结算得分、胜负与下轮牌权时交换二者的出牌结果。"
},
{
"name": "戛然而止",
- "content": "手牌数首次小于5时游戏结束,闲家获得庄家剩余手牌中的分数。"
+ "content": "一整轮结束后,若任一玩家手牌少于5张,则该轮成为最后一轮。闲家获得庄家本人剩余手牌牌面分的一半;底牌仍按最后一轮胜负正常结算。"
},
{
"name": "聚类分析",
- "content": "可以将一张非分数、非主点数的数字牌当作与其绝对值不超过1的牌打出(转化后的牌也不能是分数牌、主点数)。"
+ "content": "发动后可依次点击多张非分数、非主点数的普通牌,明确选择各自要临时视为的相邻点数,转换完成后再自行选牌按基本规则出牌;转化前后均不得为分牌或主点数。甩牌时,其他玩家能借此组成更大组件也会令甩牌失败。"
},
{
+ "id": "meticulous_accounting",
"name": "锱铢必较",
- "content": "本局的分数牌是A,2,3,4,5,6,7,闲家开局-10分。"
+ "content": "本局的分数牌改为 A、2、3、4、5、6、7,分别计 1、2、3、4、5、6、7 分;闲家以 -10 分开局。"
},
{
+ "id": "lost_in_fog",
"name": "如堕云雾",
- "content": "禁止查看已打出的牌和已获得的分数。"
+ "content": "出牌阶段不保留上一轮出牌记录,并隐藏本轮分值、闲家总分及已获得的分牌;终局结算时统一公开。"
},
{
+ "id": "birds_gone_bow_hidden",
"name": "鸟尽弓藏",
- "content": "一种花色的分数牌全部打出后,不能再主动打出该花色。"
+ "content": "某一有效花色的所有5、10、K分牌全部实际打出后,一号位不能再主动打出该有效花色;跟牌不受影响。带分级牌统一计入主花色,不计入原牌面副花色。若手中只剩已弓藏花色,则解除限制。"
},
{
+ "id": "odd_even_scoring",
"name": "无独有偶",
- "content": "奇数轮次的分数为0,偶数轮次的分数为原分数的两倍。"
+ "content": "奇数轮的分牌不进入计分区且计0分;偶数轮的分牌进入计分区,并按牌面原分的两倍计入。底牌仍按常规抠底规则结算。"
},
{
+ "id": "second_battlefield",
"name": "第二战场",
- "content": "每当每名玩家打出的牌数不小于5时,使用这些牌进行一次德州扑克,获胜者所属的阵营获得5分。"
+ "content": "每场先公开5张公共牌。各家打出且尚未参赛的牌会明置在各自桌前并跨轮累计;只在轮末且四家均累计至少5张时由系统判定德州最佳牌型,胜者阵营获得5分,随后收走参赛牌、更换公共牌并重新累计。某场累计达到5张时,只有四家剩余手牌都严格少于5张才延至全部出完后进行最终判定;正好剩5张仍照常判定。王不参与牌型;双副牌的重复实体分别计算,五条高于同花顺;同阵营并列只奖励一次,跨阵营并列时双方各得5分。"
},
{
+ "id": "tai_chi_four_symbols",
"name": "太极四象",
- "content": "新增牌型:四张花色各不相同且点数相同的牌,此牌型按点数比较大小,若没有任何副花色则可以用四张主花色毙之。"
+ "content": "新增牌型:四张花色各不相同且点数相同的牌。四象只按点数比较;没有任何副牌时,可以用四张主牌毙之。"
},
{
+ "id": "forbidden_magic",
"name": "禁术秘法",
- "content": "玩家可以在一轮开始时声明将自己所有主牌不再视为主牌,可以将其当作任意花色点数相同的牌打出(王当作任意牌),限一次。"
+ "content": "每名玩家每局限一次,可在出牌阶段随时预备,并在轮首依次确认是否发动;确认后本局永久生效且不能撤销。发动者原有的全部主牌不再视为主牌,并可将其改作任意花色的原点数牌;王可改作任意普通点数,但有主局不能改作主花色。"
},
{
+ "id": "one_country_two_systems",
"name": "一国两制",
- "content": "庄家方、闲家方各自亮出主花色。"
- },
- {
- "name": "取长补短",
- "content": "玩家编号为0,1,2,3,第x轮中,编号为x%4的玩家的牌大小级别+1,编号为(x+2)%4的玩家的牌大小级别-1。"
+ "content": "庄家方与闲家方分别在本方内亮主、反主,双方声明互不覆盖。任意玩家亮一对王,或双方都未亮主时,双方无主;只有一方亮花色时,双方共用该主花色;双方亮出不同花色时,两种花色按阵营互换后统一进行牌型、跟牌、缺门、毙牌和大小判定。"
},
+ {
+ "id": "strength_compensation",
+ "name": "取长补短",
+ "content": "以庄家为0号位,逆时针依次为1、2、3号位。第x轮中,x%4号位的全部手牌沿当前牌力序列升一级,(x+2)%4号位降一级,轮末轮换。副牌留在原花色并跳过级牌点数,越过A成为F、越过最低普通牌成为1;主牌依次为主普通牌、副级牌、主级牌、小王、大王、白王,升降不会打乱原有内部次序。牌面变化不改变实体牌原有分值。"
+ },
{
+ "id": "wooden_ox_flowing_horse",
"name": "木牛流马",
- "content": "游戏开始时,2、3号位玩家获得“木牛流马”,持有木牛流马的玩家可以在一轮开始时将一张牌置入其中(已有时改为替换)并交给队友,队友可以如手牌般打出此牌。木牛流马中的牌不计入手牌花色限制,每局每人可以传递两次。当手牌不等对游戏造成影响时,玩家必须传递/收回木牛流马中的牌以使牌数正确。"
+ "content": "游戏开始时2、3号位各持有一具木牛流马(每队一具)。每轮首张牌打出前,持有者可以跳过操作、将其中原有的牌原样交给队友,或放入/替换一张手牌并必须立即交给队友;首张牌打出后本轮不得再操作。接收者可以跨轮保留其中的牌,并在自己的正常出牌回合将它如手牌般打出;该牌不计入持有者的跟牌花色义务。若队友已经无牌可出,持有者必须在轮首将木牛流马交给队友以恢复牌数。每队的木牛流马每局最多往返两次(共四次单程传递)。"
},
{
"name": "再续前缘",
@@ -268,88 +317,105 @@
"content": "移除4张王牌并令所有主点数牌大小视为原本的数字大小。"
},
{
+ "id": "mutual_support",
"name": "同舟共济",
- "content": "每名玩家限一次,任意轮次轮到某玩家出牌时,可以要求队友交给自己0-2张牌或选择交给队友1-2张牌,本轮结束时获得牌的玩家再交给给出牌的玩家等量手牌。"
+ "content": "每名玩家每局限一次;轮到自己且尚未出牌时,可以要求队友交给自己0至2张牌,或选择1至2张手牌交给队友。本轮结束时,获得牌的玩家再选择等量手牌返还给原持有者。"
},
{
+ "id": "candle_to_dawn",
"name": "烛尽天明",
- "content":"游戏开始时庄家队友选择是否点燃“烛”。每轮出牌结束时根据本轮四号位的出牌调整“烛”的状态:若其打出的牌只含有黑色,则熄灭“烛”,若只含有红色,则点燃“烛”,若均不满足则不改变“烛”的状态。“烛”处于点燃的轮次,红色分数牌具有的的分数+5,黑色分数牌具有的分数-5;“烛”处于熄灭的轮次,红色分数牌具有的的分数-5,黑色分数牌具有的分数+5。"
+ "content":"游戏开始时庄家队友选择是否点燃“烛”。每轮先按该轮开始时的烛态结算分数,结算完成后再根据本轮四号位的出牌决定下轮烛态:若其打出的牌只含黑色,则熄灭“烛”;若只含红色,则点燃“烛”;否则不变。“烛”点燃的轮次,红色分数牌每张+5分,黑色分数牌每张-5分;“烛”熄灭的轮次则相反。小王是黑色,大王是红色。"
},
{
+ "id": "cultural_revolution",
"name": "文化革命",
- "content": "每名玩家限一次,作为一号位出牌时可以选择声明一种花色/点数(不能为10/K),两轮之内该花色/点数视为主花色/主点数,若发动时已经处于此效果生效区间内则覆盖之。"
+ "content": "每名玩家每局限一次;作为一号位出牌前,先选择“革花色”或“革点数”,再声明一种花色或2至A的一种点数(包括10、K)。发动当轮及下一轮内,所选花色替换原主花色,或所选点数替换原级牌点数;被替换的原主暂按副牌处理。生效期间的新声明会整体覆盖旧声明,并重新持续两轮。"
},
{
+ "id": "three_tigers",
"name": "三人成虎",
- "content": "一轮中,若至少3名玩家打出的牌花色均相同,则该花色在本轮结算时大小视为主花色,但大小-4。"
+ "content": "一轮中,每名玩家本次打出的全部牌均为同一副花色的副牌时,为该花色计一人;同一副花色累计达到三人后立即成虎。主花色牌、级牌和王不计人数,也不转换。该轮桌上及此后打出的该副花色牌全部视为主牌,并沿剔除本局级牌后的普通牌序列降低4级;例如4为级牌时,降级过程跨过4这一档,最低为−2。转换只影响本轮牌力,不改变实体牌原有分值。"
},
{
"name": "势者制权",
"content":"游戏开始时庄家队友获得“权”并选择一名对方玩家,令其获得“势”。在一轮中,若持有“权”的玩家位于持有“势”玩家的前置位,则后者本轮打出的分数在计算时不会超过前者;若持有“权”的玩家位于持有“势”玩家的后置位,则前者本轮打出牌的大小在计算时不会小于后者且视为先手(均为垫牌则不触发此条)。以上两条效果任意之一被触发的轮次结束时,持有“权”的玩家需将“权”交给持有“势”的玩家,后者再选择对方的一名玩家交给其“势”。"
},
{
+ "id": "invite_into_urn",
"name": "请君入瓮",
- "content": "每名玩家限一次,作为首置位时可以指定一名玩家和一张牌,若其本轮打出了此牌,则失去5分。"
+ "content": "每名玩家每局限一次;作为一号位出牌前,可指定一名其他玩家和一种实体牌面。本轮只要目标玩家打出至少一张该牌面,其阵营失去5分;无论同一次打出一张还是多张,都只扣5分。若目标属于庄家方,等价为闲家增加5分。"
},
{
+ "id": "teammate_cheer",
"name": "队友加油",
- "content": "没有主牌时可以声明之并令队友在本局游戏中的所有牌大小级别+1。"
+ "content": "每次出牌后,若该玩家还持有至少一张牌、剩余手牌中已经没有主牌且尚未发动过,系统询问是否声明“队友加油”。确认后其队友获得永久加油 Buff,剩余手牌及之后打出的牌沿当前完整牌力序列全部提升一级;副牌不会跨入主牌链,A之上为F,大王之上为白王。获得 Buff 的玩家全场标记,实体牌原有分值不变;暂不发动后可在后续满足条件的出牌后再次选择。"
},
{
+ "id": "old_horse_still_has_strength",
"name": "老骥伏枥",
- "content": "最后一个获得出牌权的玩家的第一次出牌视为必定最大。"
+ "content": "开局首发者视为已经获得牌权。四名玩家都至少获得过一次牌权时,最后首次获得牌权的玩家,其随后第一次合法首发视为绝对最大,任何后手都不能压过;该次出牌仍须完整遵守首发牌型与甩牌规则。"
},
{
"name": "第三战场",
"content": "一号和二号位使用每轮打出的四张牌(王牌则不触发)进行加减乘除24点游戏,率先算出者+3分(四号位须在20s内出牌)。"
},
{
+ "id": "trump_wins",
"name": "Trump wins",
- "content": "打出分数最多的玩家下轮先出牌。"
+ "content": "每轮仍按正常牌力判定胜者并结算分数,但下一轮改由本轮实体出牌总分最高的玩家先出;若最高分有多人并列,则其中先出牌的玩家获得牌权。"
},
{
+ "id": "openly_revealed",
"name": "昭然若揭",
- "content": "底牌始终保持明置。"
+ "content": "底牌从摸牌开始便在牌桌中央明置;庄家重新埋底后,明置内容同步换成新底牌。整局所有玩家都可随时查看当前底牌。"
},
{
+ "id": "straw_boat_borrowing_arrows",
"name": "草船借箭",
- "content": "若首置位的玩家打出的牌中包含分数之和不小于10且这些分数牌没有被其所属阵营获得,其可以在此轮结束时弃置一张非分数牌并获得本轮打出的非分数牌中最大的一张。"
+ "content": "若首置位玩家本轮打出的实体分牌合计不少于10分,且这些分牌最终被对方阵营获得,轮末该玩家可以选择发动:向全场公开弃置一张剩余手牌中的非分牌,并获得本轮打出的、按当前主牌体系牌力最大的单张非分牌。若手中无可弃牌或本轮没有非分牌,则不能发动。"
},
{
+ "id": "bush_gate",
"name": "布什戈门",
- "content": "每名玩家限一次,作为二号位时若认为一号位打出的牌过于离谱,可以令其收回并重新打出另一(些)合法的牌。"
+ "content": "每名玩家每局限一次;作为本轮二号位且尚未出牌时,可以迫使一号位收回本轮首发并重新合法首发。被收回的每一张牌在这次重新首发中均不可使用;重新首发成功后限制立即解除,后续轮次可正常使用。若一号位没有其他手牌则不能发动。"
},
{
"name": "千金散尽",
"content": "闲家的的分数为 200 - 实际得到的分数,不能低于0分。"
},
{
+ "id": "illusion_and_reality",
"name": "虚虚实实",
- "content": "每名玩家限一次,若某副花色的牌剩余奇数张,出牌时可以视为没有此花色的牌(若利用此规则,则不能打出该花色的牌)。"
+ "content": "每名玩家每局限一次。跟随副花色时,若自己手中该副花色恰好剩余奇数张,可以发动并将这些牌虚置,本次出牌完全视为手中没有该花色;被虚置的牌不能在本次出牌中打出,仍须从其余手牌中打出与首家相同的张数。"
},
{
+ "id": "strive_upstream",
"name": "力争上游",
- "content": "每轮的出牌顺序为上轮打出牌的大小顺序(垫牌按照字典序比较单张大小,考虑主花色)。"
+ "content": "每轮的出牌顺序按上轮四家出牌由大到小排列。以一号位的牌型为准:同牌型按正常牌力比较;牌型不一致时,先比与首家牌型的贴合程度,异花色牌越多越小(包括主牌),数量相同再从最小牌起按字典序比较;完全相同时后出者更小。"
},
{
+ "id": "afterglow",
"name": "回光返照",
- "content": "当玩家的主牌不大于 3 张时,其出牌可以不受花色限制且打出的主牌大小+1。若使用此规则进行出牌,则之后的每一轮必须打出主牌直到没有主牌为止。"
+ "content": "每名玩家每局限一次。开局一号位若持有1至3张主牌,可在首次出牌前直接发动;其余情况下,每次出牌后若该玩家仍有手牌且只剩1至3张主牌,系统询问是否发动。确认后剩余主牌立即沿当前完整主牌序列提升一级;从下一次出牌起,只要手中仍有主牌便无视通常的跟牌要求,但每次出牌只能打出主牌、不能混入副牌。主牌出尽后效果结束;实体牌原有分值不变,暂不发动不消耗机会。"
},
{
+ "id": "outward_harmony_inner_division",
"name": "貌合神离",
"content": "任意一方两名玩家在一轮中若打出的牌型不一致(不考虑花色),则对方阵营获得5分。"
},
{
+ "id": "ambiguous",
"name": "模棱两可",
- "content": "每名玩家限一次,出牌时可以展示两种不同的合法出牌方式,在本轮结束时选择其中一种(多人发动则后续玩家再发动无须消耗次数,并从后置位向前选择)。"
+ "content": "每名玩家每局限一次,仅本轮二、三号位可发动:同时公开两种不同且各自合法的出牌方案,四家出完后再选择其中一种结算。若二、三号位都发动,则三号位作为后续发动者不消耗次数,并由三号位先选、二号位后选。"
},
{
"name": "良禽择木",
"content": "游戏开始前,将所有 A K 移出游戏并在摸牌结束后平均分配给玩家。"
},
{
+ "id": "two_ghosts_knock_door",
"name": "二鬼拍门",
- "content": "摸到不小于2张王牌时须将其全部亮出。"
+ "content": "摸牌阶段中,任一玩家手牌里的王达到至少两张时,立即将其持有的全部王作为明置手牌公开;之后再摸到的王也继续明置。王仍属于原玩家手牌,打出或离开手牌后便从明置区消失。"
},
{
"name": "正大光明",
@@ -357,47 +423,55 @@
},
{
"name": "互通有无",
- "content": "队友的手牌对你可见。"
+ "content": "进入出牌阶段后,队友的手牌仅对你可见。"
},
{
+ "id": "double_happiness",
"name": "双喜临门",
- "content":"重新从三个规则中选择两个(不矛盾的)规则,于本局游戏中同时生效。"
+ "content": "重新从三个规则中选择两个规则,于本局游戏中同时生效。房主可以刷新其中任意一条候选。"
},
{
+ "id":"people_commune",
"name":"人民公社",
- "content":"每名玩家各埋两张底牌,闲家开局-20分。抄底时,双方均获得且仅获得对方所埋的底牌中的分数。"
+ "content":"摸牌时不留底牌,四名玩家各摸27张,再各自埋两张;每人可随时查看自己埋的两张牌及其分值。闲家开局-20分。抄底时只结算对方阵营埋下的四张牌:闲家拿底获得庄家方埋分;庄家方拿底则抄走闲家埋分,使闲家按底牌倍数扣分。"
},
{
"name":"市场经济",
"content":"初始时市场具有8张牌,每过3轮每人将一张牌投入市场,根据本轮的大小依次从市场中获得一张牌。游戏结束时闲家获得市场中的一半分数。"
},
{
+ "id":"remove_firewood_from_under_cauldron",
"name":"釜底抽薪",
- "content":"被反主的玩家可以在反主结算后与其交换手牌。(反多次则由后向前结算)"
+ "content":"每次反主后,被反主的玩家获得一次选择:亮主与反主全部结算后,可以与反主者交换当前全部手牌,也可以不交换。若发生多次反主,按反主发生顺序由后向前依次询问和结算。"
},
{
+ "id":"mainstay",
"name":"中流砥柱",
- "content":"摸牌结束后,若本局不是无主局,则从一号位开始,若玩家的主牌数不大于5,则可以将包括所有主牌在内的5张牌交给队友,然后队友再交给其5张牌。"
+ "content":"摸牌结束后,若本局不是无主局,则从一号位开始依次处理每名玩家。轮到某玩家时,若其当前主牌数不大于5,可以将包括全部主牌在内的5张牌交给队友,再由队友返还5张牌;每名玩家均按轮到自己时的当前手牌独立判断,因此同队两人都可以发动。"
},
{
+ "id":"happy_twins",
"name":"欢乐成双",
- "content":"庄家与上家交换位置。"
+ "content":"庄家锁定后与上家交换位置,但双方原有组队关系不变;本局结束后恢复原座次。若庄家方获胜,下一局仍由庄家的固定队友上庄;若闲家方获胜,则由换位后庄家的下家(即换位前的庄家上家)上庄。"
},
{
+ "id":"encircle_three_missing_one",
"name":"围三阙一",
- "content":"将除王牌与级牌之外的所有牌分为四种花色,记录游戏中最先出现的三种花色,若最后剩余的一种花色不为主花色,则该花色从下一轮开始替换原本的主花色,然后无论是否发生替换,删除所有记录并重新开始记录。"
+ "content":"摸牌阶段照常亮主。进入出牌阶段后,忽略王牌和级牌,按实际出牌顺序记录最先出现的三种普通花色;缺少的第四种花色若不是当前主花色,则从下一轮起替换当前主花色。无论是否换主,随后均清空记录并重新统计。"
},
{
"name":"三六九等",
"content":"本局除亮主外,玩家可以额外亮出一种主花色以使该花色的牌成为“劣花色”,之后游戏中普通副牌相对于劣牌的关系等同于主牌相对于副牌的关系,同时该花色的级牌小于副花色的级牌(即主花色>副花色>劣花色)。亮出劣花色的过程遵循与主牌相同的反主规则,对王即无劣花色,一张/一对级牌/王牌只能用于亮主与亮劣其中之一。"
},
{
+ "id":"iron_evidence",
"name":"铁证如山",
- "content":"若在一轮中出现了小王、红桃Q、黑桃J、梅花J,则本轮打出的分数翻倍,每多出现一张则倍数额外+1;若两张大王均已被打出,则改为本轮不计分。"
+ "content":"每轮开始时锁定铁证牌的效果。若此前两张大王尚未全部打出,本轮每出现一张小王、红桃Q、黑桃J或梅花J,本轮牌面总分的倍数便在1倍基础上加1;若本轮开始前两张大王均已打出,则本轮只在出现至少一张上述铁证牌时清零牌面总分,没有铁证牌则正常计分。本轮内才打出的第二张大王不影响本轮效果,只从下一轮开始生效。"
},
{
+ "id":"waiting_rabbit",
"name":"守株待兔",
- "content":"游戏开始时,每名玩家可以暗中指定一张牌(不能是王牌、主点数),然后当此牌被其他玩家打出时,当轮座次最先指定了此牌的玩家可以用一张非分数牌与之交换(只交换一张,一张牌的分数只在其首次被打出时计算一次)。"
+ "content":"埋底完成后,每名玩家暗中指定一张有花色的目标牌,不能指定王或当前级牌,但可以指定5、10、K。当其他玩家在一轮中打出该牌面时,目标牌先正常参与本轮牌力与首次计分;本轮结算完成后,由本轮座次最先指定它且尚未成功交换的玩家决定是否发动,可以用手里一张非分牌与其中一张目标牌交换:目标牌进入其手牌,非分牌原位顶替留在桌上的目标牌,但不倒改本轮赢家或得分。每张实体分牌只在全局首次上桌时计分,被换回手牌后再次打出只保留牌面大小,不再计分。"
},
{
"name":"破釜沉舟",
@@ -408,31 +482,48 @@
"content":"摸牌时随机一部分牌正面向上。"
},
{
+ "id":"hidden_dragon_in_abyss",
"name":"潜龙在渊",
- "content":"游戏开始时每名玩家指定自己最多的点数(主点数除外),手牌数首次不大于12时,若玩家从未打出过该点数,则所属阵营获得10分。"
+ "content":"埋底完成后,每名玩家公开声明自己手牌中数量最多的一个点数(本局级牌除外);若最多点数唯一则由系统自动声明,并列时由玩家自行选择。玩家的手牌数首次不大于12时,若其此前及本次均未打出过所声明的点数,则所属阵营获得10分。"
},
{
+ "id":"administrative_review",
"name":"行政审查",
- "content":"庄家12张底牌,游戏开始前,庄家下家指定一种副花色,庄家上家指定一种点数,直到庄家方打出过该花色和该点数的牌才可埋底牌。"
+ "content":"本局有12张底牌,四家各持24张牌开始出牌。开始出牌前,庄家下家公开指定一种副花色,庄家上家公开指定一种点数;底牌继续封存,庄家不可查看。庄家或其队友累计打出过指定副花色和指定点数后,庄家才收到并查看12张底牌,立即埋12张后继续当前牌局。"
},
{
+ "id":"political_review",
"name":"政治审查",
- "content":"每名玩家限一次,可以令队友收回其打出的牌并重新打出(可以继续选择同样的出牌)。"
+ "content":"每名玩家每局限一次。在该玩家尚未发动前,其队友每次提交出牌后都暂停牌局并询问该玩家是否令队友收回。选择收回才消耗次数;收回只传递反对信号,不产生任何禁出限制,队友可以立即原样重新出牌。选择放行不消耗次数。"
},
{
+ "id":"no_one_survives",
"name":"无人生还",
- "content":"除一号位的出牌均暗置,在一轮结束后统一展示。"
+ "content":"每轮一号位的出牌正常明置;二、三、四号位的出牌在本人视角始终明置,对其他玩家自动暗置且只公开张数,本轮牌面总分也不提前显示。四家出完后统一公开暗牌与本轮总分,再按正常规则结算胜负和下轮牌权。"
},
{
+ "id":"lure_tiger_from_mountain",
"name": "调虎离山",
- "content": "每方阵营限一次,一轮开始前可以指定一名非一号位玩家,令其本轮无法响应甩牌询问,且打出的牌不计大小与分数。"
+ "content": "每方阵营每局限发动一次。每名玩家都可在出牌阶段随时预备,系统在目标轮开始、首张牌打出前按本轮座次依次询问预备者;同队首名确认并选定目标的玩家抢占本方次数,其队友不能再发动。发动者指定一名本轮非一号位玩家,使其本轮无法参与甩牌询问,且本轮打出的牌不计大小与分数。"
},
{
+ "id": "defense_as_offense",
"name": "以守为攻",
- "content": "每轮若一号位玩家打出的牌每小于一名其他玩家,则其下轮打出的牌大小+1。"
+ "content": "每轮结束后,按“力争上游”的完整牌力顺序比较四家出牌;本轮一号位每小于一名其他玩家,其下一轮全部手牌牌面便提升一级,并在玩家名旁标记对应的“+X”。加成只持续下一轮,实体牌原有分值不变;F与白王不再提升,其他牌按完整牌力序列提升并在白王处封顶,因此大王即使+3仍为白王。"
},
{
+ "id": "antinomy",
"name": "二律背反",
- "content": "游戏开始前每名玩家各自指定一张牌(花色+点数)并同时亮出,被指定的牌本局中视为两张不同的牌(即无法成对),若有多名玩家指定了同一张牌,则不执行此效果。每轮结束后,本轮中指定牌被打出过的所有玩家重新指定一张牌并同时亮出。"
+ "content": "庄家埋完底、正式开始出牌前,四名玩家各自选择一张普通花色牌面(花色+点数),收齐后同时亮出。仅被一名玩家指定的牌面,其两张实体牌本局视为不同牌,不能组成对子或拖拉机;同一牌面被多人指定时不执行拆对效果。每轮结束后,本轮中自己所指定牌面曾被任何玩家打出的声明者重新选择,收齐后同时更新;完成前不能开始下一轮出牌。"
+ },
+ {
+ "id": "change_rice_to_mulberry",
+ "name": "改稻为桑",
+ "content": "庄家埋底后、正式出牌前,两名闲家分别选择自己手牌中⌊分牌数÷2⌋张分牌;原本为副牌的变为同花色A,原本为主牌的变为大王。被改造的实体牌永久失去分值,只按新牌面参与牌型、跟牌和大小结算。"
+ },
+ {
+ "id": "destroy_dyke_flood_fields",
+ "name": "毁堤淹田",
+ "content": "庄家每局限发动一次。闲家赢得一轮后、该轮计分前,庄家可以令本轮所有分数作废并记录作废分数。接下来的三轮为灾期,期间照常计分;若闲家在灾期内累计获得不少于20分,或游戏在灾期结束前结束,则视为事发,闲家取回作废分数并额外获得20分,随后结束灾期。若牌局恰于灾期第三轮结束,且闲家拿底,也视为事发。若三轮结束时仍未事发,记录的分数永久作废。"
}
-]
\ No newline at end of file
+]
diff --git a/simple-bot/who_designed_adapter.py b/simple-bot/who_designed_adapter.py
new file mode 100644
index 0000000..5504439
--- /dev/null
+++ b/simple-bot/who_designed_adapter.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+"""Adapter between the Tractor server snapshot and the WhoDesigned strategy."""
+
+import json
+import os
+import sys
+import types
+
+
+# mvGen imports numpy but never uses it. Supplying a tiny placeholder keeps the
+# deployment dependency-free while leaving the original bot repository intact.
+sys.modules.setdefault("numpy", types.ModuleType("numpy"))
+
+WHO_DESIGNED_DIR = os.path.abspath(
+ os.path.join(os.path.dirname(__file__), "..", "WhoDesigned")
+)
+sys.path.insert(0, WHO_DESIGNED_DIR)
+
+from mvGen import move_generator # noqa: E402
+from myutils import get_action_options, setMajor # noqa: E402
+
+
+CARD_SCALE = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "0", "J", "Q", "K"]
+SUITS = ["s", "h", "c", "d"]
+
+
+def card_base_id(card_name):
+ if card_name == "jo":
+ return 52
+ if card_name == "Jo":
+ return 53
+ return CARD_SCALE.index(card_name[1]) * 4 + SUITS.index(card_name[0])
+
+
+class CardAllocator:
+ def __init__(self):
+ self.available = set(range(108))
+
+ def allocate(self, card_name):
+ base = card_base_id(card_name)
+ for card_id in (base, base + 54):
+ if card_id in self.available:
+ self.available.remove(card_id)
+ return card_id
+ raise ValueError(f"Too many copies of card {card_name}")
+
+
+def normalize_level(level):
+ return "0" if str(level) == "10" else str(level)
+
+
+def protect_point_trick_with_trump(generator, hand_ids, current_trick, history_ids, action):
+ """Keep WhoDesigned's heuristics, but close its unsafe single-card point gap.
+
+ When this player is void in the led side suit, protect table points if an
+ opponent is currently winning, or if a known-void opponent still acts after
+ a winning teammate. This uses the same empty-suit memory as the original
+ full-request constructor.
+ """
+ if not current_trick or any(len(cards) != 1 for cards in history_ids):
+ return action
+
+ lead = generator.Num2Poker(history_ids[0][0])
+ if generator.isMajor(lead):
+ return action
+ lead_suit = generator.get_suit(lead)
+
+ hand_pokers = [generator.Num2Poker(card_id) for card_id in hand_ids]
+ if any(not generator.isMajor(card) and card[0] == lead_suit for card in hand_pokers):
+ return action
+
+ if not any(
+ generator.Num2Poker(card_id)[1] in ("5", "0", "K")
+ for cards in history_ids
+ for card_id in cards
+ ):
+ return action
+
+ winner_position = 0
+ winner = generator.Num2Poker(history_ids[0][0])
+ for position in range(1, len(history_ids)):
+ challenger = generator.Num2Poker(history_ids[position][0])
+ if challenger != winner and generator.bigger_card(winner, challenger) == challenger:
+ winner = challenger
+ winner_position = position
+
+ winner_id = int(current_trick[winner_position]["playerIndex"])
+ should_trump = winner_id != generator.teammate_id
+ if not should_trump and winner_id == generator.teammate_id:
+ already_played = {int(play["playerIndex"]) for play in current_trick}
+ future_opponents = [
+ player_id
+ for player_id in range(4)
+ if player_id not in already_played
+ and player_id != generator.selfid
+ and player_id != generator.teammate_id
+ ]
+ should_trump = any(
+ lead_suit in generator.empty_suits[player_id]
+ and generator.major not in generator.empty_suits[player_id]
+ for player_id in future_opponents
+ )
+
+ if not should_trump:
+ return action
+
+ trumps = [
+ card_id
+ for card_id in hand_ids
+ if generator.isMajor(generator.Num2Poker(card_id))
+ ]
+ trumps.sort(key=lambda card_id: generator.card_level(generator.Num2Poker(card_id)))
+ for card_id in trumps:
+ poker = generator.Num2Poker(card_id)
+ if not generator.isMajor(winner) or generator.card_level(poker) > generator.card_level(winner):
+ return [card_id]
+ return action
+
+
+def main():
+ data = json.loads(sys.stdin.read())
+ player_id = int(data.get("id", 0))
+ hand_names = data.get("deck", [])
+ played_names = data.get("played", [[], [], [], []])
+ current_trick = data.get("history", [])
+ empty_suits = data.get("emptySuits", [[], [], [], []])
+ level = normalize_level(data.get("level", "2"))
+ major = data.get("trumpSuit") or "n"
+
+ allocator = CardAllocator()
+ played_ids = [[], [], [], []]
+ for index in range(4):
+ for card_name in played_names[index]:
+ played_ids[index].append(allocator.allocate(card_name))
+
+ hand_ids = [allocator.allocate(card_name) for card_name in hand_names]
+
+ # Current-trick cards are the tail of each player's cumulative played list.
+ history_ids = []
+ for play in current_trick:
+ index = int(play["playerIndex"])
+ count = len(play["cards"])
+ history_ids.append(played_ids[index][-count:] if count else [])
+
+ setMajor(major, level)
+ generator = move_generator(level, major)
+ generator.selfid = player_id
+ generator.teammate_id = (player_id + 2) % 4
+ generator.next_id = (player_id + 1) % 4
+ generator.prev_id = (player_id + 3) % 4
+ generator.hold = hand_ids[:]
+ generator.played = played_ids
+ generator.empty_suits = [
+ list(dict.fromkeys(empty_suits[index])) if index < len(empty_suits) else []
+ for index in range(4)
+ ]
+ generator.cards_left = sorted(allocator.available)
+ generator.organized_hold_cards = generator.organize_cards(generator.hold)
+ generator.organized_left_cards = generator.organize_cards(generator.cards_left)
+
+ options = get_action_options(hand_ids, history_ids, level, generator)
+ action = options[0] if options and options[0] else []
+ if not isinstance(action, list):
+ action = [action]
+ action = protect_point_trick_with_trump(
+ generator,
+ hand_ids,
+ current_trick,
+ history_ids,
+ action,
+ )
+ action_names = [generator.Num2Poker(card_id) for card_id in action]
+
+ print(json.dumps({"player": player_id, "action": action_names}))
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except Exception as exc:
+ print(f"WhoDesigned adapter failed: {exc}", file=sys.stderr)
+ raise
diff --git a/tractor-game-simulator/.gitignore b/tractor-game-simulator/.gitignore
index c286901..81d2e3b 100644
--- a/tractor-game-simulator/.gitignore
+++ b/tractor-game-simulator/.gitignore
@@ -33,3 +33,5 @@ Thumbs.db
# Temporary files
*.tmp
.cache/
+test-results/
+playwright-report/
diff --git a/tractor-game-simulator/client/package.json b/tractor-game-simulator/client/package.json
index 5c6e8a6..b4bb986 100644
--- a/tractor-game-simulator/client/package.json
+++ b/tractor-game-simulator/client/package.json
@@ -6,20 +6,24 @@
"scripts": {
"dev": "vite",
"build": "vite build",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "test:view": "node --test test/gameViewUtils.test.mjs",
+ "test:unit": "node --test test/*.test.mjs",
+ "test:e2e": "playwright test"
},
"dependencies": {
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "antd": "^5.12.0",
- "socket.io-client": "^4.7.2",
- "zustand": "^4.4.7",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
- "axios": "^1.6.2"
+ "antd": "^5.12.0",
+ "axios": "^1.6.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "socket.io-client": "^4.7.2",
+ "zustand": "^4.4.7"
},
"devDependencies": {
+ "@playwright/test": "^1.61.1",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.1",
diff --git a/tractor-game-simulator/client/playwright.config.js b/tractor-game-simulator/client/playwright.config.js
new file mode 100644
index 0000000..355e60c
--- /dev/null
+++ b/tractor-game-simulator/client/playwright.config.js
@@ -0,0 +1,16 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './test/e2e',
+ globalSetup: './test/global-setup.js',
+ timeout: 45_000,
+ expect: {
+ timeout: 8_000
+ },
+ use: {
+ baseURL: 'http://127.0.0.1:3002',
+ trace: 'retain-on-failure',
+ screenshot: 'only-on-failure',
+ ...devices['Desktop Chrome']
+ }
+});
diff --git a/tractor-game-simulator/client/public/DLC.json b/tractor-game-simulator/client/public/DLC.json
index f6c40b2..8fd5d88 100644
--- a/tractor-game-simulator/client/public/DLC.json
+++ b/tractor-game-simulator/client/public/DLC.json
@@ -1,355 +1,428 @@
[
{
+ "id": "reverse_rank_order",
"name": "倒反天罡",
- "content": "每个花色内的大小顺序颠倒。"
+ "content": "除级牌和王外,每个花色内普通牌的大小顺序颠倒。"
},
{
"name": "万马齐喑",
"content": "说一句话需要扣五分。"
},
{
+ "id": "ten_sided_ambush",
"name": "十面埋伏",
- "content": "庄家队友暗中指定一个点数具有赋分功能(+5,-5)。"
+ "content": "庄家队友暗选非级牌、非分牌点数。闲家赢得每张-5分,庄家方赢得每张给闲家+5分;底牌乘倍数同理,点数首次出现时公开。"
},
{
+ "id": "normal_game",
"name": "世事无常",
"content": "正常对局。"
},
{
+ "id": "abundant_harvest",
"name": "五谷丰登",
"content": "庄家12张底牌。闲家开局为30分。"
},
{
+ "id": "half_realm",
"name": "江山半壁",
"content": "庄家4张底牌。闲家开局为-10分。"
},
{
+ "id": "shared_prosperity",
"name": "与民同乐",
"content": "没有底牌。闲家开局为-20分。"
},
{
+ "id": "extreme_challenge",
"name": "极限挑战",
"content": "庄家16张底牌。闲家开局为60分。"
},
{
+ "id": "perfect_strategy",
"name": "算无遗策",
- "content": "庄家队友明手。闲家开局为10分。"
+ "content": "庄家埋底后,庄家队友明手且由庄家代为出牌。闲家开局为10分。"
},
{
+ "id": "know_yourself_and_enemy",
"name": "知己知彼",
"content": "摸牌后,与对家交换两张牌。"
},
{
+ "id": "news_minister_i",
"name": "新闻部长I",
"content": "摸牌后,每人给上家两张牌。"
},
{
+ "id": "news_minister_ii",
"name": "新闻部长II",
"content": "摸牌后,每人给下家两张牌。"
},
{
+ "id": "irresistible_force",
"name": "势如破竹",
"content": "本局庄家获胜可以连庄。"
},
{
+ "id": "substitute_sacrifice",
"name": "李代桃僵",
- "content": "玩家可以任意垫一次牌,需要声明(视为小)。"
+ "content": "每名玩家限一次,可在跟牌时主动声明垫牌:仍须出相同张数,但可无视花色与牌型要求,且本次出牌始终视为小。"
},
{
+ "id": "six_six_great_success",
"name": "六六大顺",
- "content": "本局具有同花顺(6张起步)。"
+ "content": "新增同花顺牌型,至少六张连续牌;主同花顺可包含级牌和王牌。"
},
{
+ "id": "single_step_debug",
"name": "单步调试",
"content": "本局移除甩牌。"
},
{
+ "id": "one_horse_leads",
"name": "一马当先",
- "content": "庄家队友先出牌。"
+ "content": "仅第一轮改由庄家队友先出牌;庄家身份、底牌归属及后续轮次均不改变。"
},
{
+ "id": "reform_and_opening_up",
"name": "改革开放",
- "content": "庄家队友可以再埋一次底牌。闲家获得40分。"
+ "content": "庄家首次埋底后,庄家队友拿起底牌并重新埋底,随后由庄家先出牌。闲家开局获得40分。"
},
{
+ "id": "heavy_fog",
"name": "迷雾重重",
- "content": "开局移除牌堆顶8张牌,闲家获得此8张牌中一半分数。"
+ "content": "开局暗中移除牌堆顶8张牌;全局出牌及其他分数结算完成后公开,闲家补得其中总分的一半。"
},
{
+ "id": "concealed_passage",
"name": "暗度陈仓",
- "content": "每名玩家限1次,若不是第一个出牌,可以背面向上打出。"
+ "content": "每名玩家每局限1次;跟牌时可暗置出牌,除自己外本轮结束前无人能看到。一轮可有多人发动,轮末同时公开并正常结算得分和下一轮牌权。"
},
{
"name": "冰山一角",
- "content": "始终保持两张牌明置。"
+ "content": "进入出牌阶段后,每名玩家自行选择并始终保持两张手牌明置;不足两张时全部明置。"
},
{
+ "id": "fatal_beauty",
"name": "红颜祸水",
- "content": "黑桃牌视为红桃牌,闲家开局为20分;若红桃为主花色,闲家开局为40分。"
+ "content": "黑桃牌视为红桃牌,但不新增任何牌型;闲家开局为20分,若红桃为主花色则为40分。"
},
{
+ "id": "stealing_beams",
"name": "偷梁换柱",
- "content": "四张王牌可以当做任意数字牌使用,若当做分数牌则不计分。"
+ "content": "每名玩家每局限1次;发动后点击要转换的王牌,先选花色再选点数,转换完成后再自行选牌按基本规则出牌。转换可取消,王牌替成分牌仍不计分,单出可保持王牌。"
},
{
+ "id": "cosmic_shift",
"name": "斗转星移",
- "content": "当手牌数首次变为不大于12张时所有玩家与队友交换手牌。"
+ "content": "一轮结束后,若四家手牌均首次不多于12张,所有玩家与队友交换全部手牌。"
},
{
+ "id": "last_stand",
"name": "绝处逢生",
- "content": "本局不能是无主局;当玩家没有主牌时,若此时手牌数不小于5且花色相同,可以令每张牌视为主牌。"
+ "content": "本局不能是无主局,也不能用一对王反主;无人亮主时系统随机指定主花色。手牌变化后,若一名玩家不少于5张、没有主牌且全为同一花色,可选择令全部手牌视为主牌;暂时拒绝后仍可在后续手牌变化时再次选择。"
},
{
+ "id": "late_mover_advantage",
"name": "后发制人",
- "content": "每名玩家限1次,作为3号位时可以令原本的4号位先出牌。"
+ "content": "每名玩家每局限1次;作为本轮3号位出牌前,可以令下家先出,自己改为本轮最后出牌。只改变出牌顺序,不改变牌的大小结算。"
},
{
+ "id": "frequent_fluctuation",
"name": "频繁波动",
"content": "有分数牌被打出的轮次结束后,每人交给下家一张牌,闲家开局-10分。"
},
{
+ "id": "minor_disturbance",
"name": "微小扰动",
"content": "没有分数牌被打出的轮次结束后,每人交给队友一张牌。"
},
{
+ "id": "lingering_discard",
"name": "弃掷逦迤",
- "content": "有分数牌被打出的轮次结束后,每人弃一张牌(庄家弃分则闲家加分)。"
+ "content": "有分数牌被打出的轮次结束后,四家各自暗弃一张牌;终局仅公开庄家方两人弃出的分牌,并补给闲家。"
},
{
+ "id": "time_reversal",
"name": "时间倒流",
- "content": "每名玩家限1次,在一轮出牌结束后可以选择回溯到上一轮出牌结束时的状态。"
+ "content": "每名玩家每局限一次,可在本轮及轮末额外停留的1秒内预备;轮末询问所有预备者,首个确认发动者令牌局回溯到本轮开始前。"
},
{
"name": "路线摇摆",
- "content": "有不小于10分的牌被打出的轮次结束后逆转出牌顺序。"
+ "content": "有单张价值不小于10分的牌被打出的轮次结束后,顺时针与逆时针出牌顺序互换。"
},
{
"name": "一带一路",
- "content": "可以使用“小甩牌”牌型(两张单牌)。"
+ "content": "每名玩家每局限一次,首发时可点击“一带一路”,再出同一有效花色的两张非对子单牌,无需满足甩牌必大条件;未发动时仍可按普通甩牌尝试出这两张牌。跟牌或毙牌不消耗次数。"
},
{
"name": "昼夜轮转",
- "content": "第 x 轮中,点数为 x % 13 + 1 的牌在其相应花色内视为最大,闲家开局为20分。"
+ "content": "第x轮将点数x % 13 + 1提升为相应花色内最大,其他牌序不变;若轮转到级牌则仍按A最大。闲家开局为20分。"
},
{
"name": "尊老爱幼",
- "content": "每轮打出最小牌的玩家在下一轮先出牌。"
+ "content": "每轮仍按常规规则结算大牌;另以一号位牌型为准寻找最小牌:同牌型按正常牌力,牌型不一致时先比结构贴合度,异花色牌越多越小(包括主牌),数量相同再从最小牌起按字典序比较,完全相同时后出者更小。最小者下一轮先出牌。"
},
{
+ "id": "rites_collapse",
"name": "礼崩乐坏",
- "content": "每轮的首位玩家不能主动打出A。"
+ "content": "每轮一号位主动出牌时不能选择任何A;跟牌时仍可正常打出A。若一号位手中只剩A,则可正常出牌。"
},
{
+ "id": "recommend_talent",
"name": "举贤任能",
- "content": "每名玩家限一次,作为1号位时可以将出牌权交给下家。"
+ "content": "每名玩家每局限一次;作为本轮一号位出牌前,可令下家先出,自己改为本轮最后出牌。只改变牌序,不改变牌的大小。"
},
{
+ "id": "accident_insurance",
"name": "意外保险",
- "content": "单轮得分超过30的部分由对方获得。"
+ "content": "单轮牌面分超过30时,超出部分归另一方:闲家赢牌最多计30分;庄家方赢牌则将超出30的部分补给闲家。"
},
{
"name": "为人坦荡",
- "content": "手牌数首次变化为不大于5时所有玩家明置手牌。"
+ "content": "一轮结束时,若所有玩家手牌均不多于5张,所有玩家同时明置剩余手牌。"
},
{
+ "id": "three_powers",
"name": "三权分立",
- "content": "游戏开始时,2、3、4号位玩家各暗中指定一个数字(不能是主点数),并令本局中这三者点数的牌分别代替原本10、5、K的分数."
+ "content": "庄家埋底后,2、3、4号位分别暗选一个非级牌点数,依次重载原10、5、K分牌。可选择原5、10、K,也可重复选择并叠加分值;对应点数首次出现时公开。"
},
{
+ "id": "gentleman_promise",
"name": "君子一言",
- "content": "每名玩家开局时声明自己最少的花色(包括0张)。"
+ "content": "庄家埋底后,每名玩家公开声明自己手牌最少的有效花色(包括0张)。所有主牌统一算作“主”,不再计入牌面原花色;最少花色唯一时由系统直接声明,并列时由玩家选择。"
},
{
+ "id": "repeated_exhaustion",
"name": "再衰三竭",
- "content": "每名玩家连续3次最大时,从第3轮开始,每次再作为一号位将失去依次失去5,10,15……分/"
+ "content": "同一名玩家连续赢得第3、4、5……轮时依次失去5、10、15……分;换人赢牌后由新赢家从第1轮重新累计。庄家方失分补给闲家,闲家失分则从闲家总分扣除。"
},
{
+ "id": "focus_figure",
"name": "焦点人物",
- "content": "游戏开始前在两方阵营中随机各选择一名成为“焦点”(仅队友相互知晓),本局焦点打出的分数牌分数翻倍,非焦点打出的分数牌不计分。"
+ "content": "埋底后,两队分别秘密表决本队焦点;有人反对就轮换候选并重新表决,直至队内两人一致同意。本局闲家拿到的焦点人物分牌按双倍计分,非焦点人物分牌不计分;底牌正常结算,焦点与实际总分在终局揭晓。"
},
{
+ "id": "cooldown_time",
"name": "冷却时间",
- "content": "每轮的出牌不能包含上轮自己出牌含有的点数(除非只有该点数)。"
+ "content": "每轮的出牌不能包含上轮自己出牌含有的点数;若其余可用牌不足以完成本次出牌,则解除冷却。"
},
{
+ "id": "time_cooling",
"name": "时间冷却",
- "content": "每轮的出牌不能包含上轮自己出牌含有的花色(除非只有该花色)。"
+ "content": "每轮的出牌不能包含上轮自己出牌含有的有效花色,所有主牌统一视为“主”;若其余可用牌不足以完成本次出牌,则解除冷却。"
},
{
+ "id": "planned_economy",
"name": "计划经济",
- "content": "起始手牌为20张,每轮结束后再摸一轮牌直到没有牌剩余。"
+ "content": "起始每人20张手牌,另封存20张;庄家埋底并开始出牌后,每轮结束四家各摸1张,直至封存牌摸完。"
},
{
+ "id": "go_with_the_flow",
"name": "随波逐流",
- "content": "手牌数首次不大于16时将手牌交给下家,首次不大于9时再交给下家。"
+ "content": "一轮结束后,若四家手牌均首次不多于16张则把全部手牌交给下家;均首次不多于9张时再交换一次。"
},
{
+ "id": "equivalent_reciprocity",
"name": "等价互惠",
- "content": "每名玩家限一次,作为首位出牌时可以与另一名玩家拼点(比较规则为无主局的大小顺序,不区分花色),输的人失去5分,之后交换两张拼点牌。"
+ "content": "每名玩家每局限一次;作为本轮一号位出牌前,可与另一名玩家各暗选一张牌拼点。主牌大于副牌,副牌只按点数比较;输家使本方损失5分,平局不失分,随后交换两张拼点牌。"
},
{
+ "id": "enduring",
"name": "经久不衰",
- "content": "单牌/对子的大小视为本轮和上轮同牌型的最大值。"
+ "content": "每名玩家本轮出牌与自己上轮的有效花色、牌型、张数及组合结构完全一致时,本轮该组合的比较牌力视为两轮中的最大值;主牌统一视为“主”,甩牌必须逐项匹配同样的组件结构。"
},
{
+ "id": "average_pooling",
"name": "平均池化",
"content": "单牌/对子的大小视为自身和队友的平均值(垫牌、散牌不参与计算)。"
},
{
+ "id": "dream_killing",
"name": "梦中杀人",
- "content": "玩家没有主牌时可以暗置所有牌,之后出牌完全随机,若随机出的牌与首位玩家花色相同,则视为最大并“醒来”。"
+ "content": "玩家没有主牌时可以暗置所有牌,之后出牌完全随机;若随机出的牌与首位玩家花色或点数相同,则视为最大并“醒来”。若有两名及以上玩家成功,以先出者为大。"
},
{
+ "id": "joint_harmony",
"name": "珠联璧合",
- "content": "若一方打出完全相同的牌,则视为最大。"
+ "content": "若一方打出完全相同的牌,则视为最大,下一轮由该方后出者获得牌权;若两队同时珠联璧合,则按牌面正常结算。"
},
{
+ "id": "divine_weapon",
"name": "神兵天降",
- "content": "游戏开始时另外随机抽取2张牌并亮出,每名玩家可以将一张花色或点数与之相同的牌当作此牌打出并再翻出2张(每名玩家限一次)。"
+ "content": "从另一副不含王的牌中随机亮出2张神兵牌。每名玩家每局限一次,可将一张与所选神兵牌花色或点数相同的手牌当作该牌打出,牌面与牌力改变但仍按原牌计分,且跟牌义务按转化前的真实手牌判断。本轮有人发动后其他人不能再发动;轮末两张神兵牌一起替换,未发动则两张都保留。"
},
{
"name": "魔术戏法",
- "content": "每名玩家限一次,在作为一号位出牌前可以暗中选择两名玩家,交换本轮二者打出的牌。"
+ "content": "每名玩家限一次,在作为一号位出牌前可以暗中选择两名其他玩家,不能选择自己。两人仍按自己的手牌正常出牌,仅在本轮最终结算得分、胜负与下轮牌权时交换二者的出牌结果。"
},
{
"name": "戛然而止",
- "content": "手牌数首次小于5时游戏结束,闲家获得庄家剩余手牌中的分数。"
+ "content": "一整轮结束后,若任一玩家手牌少于5张,则该轮成为最后一轮。闲家获得庄家本人剩余手牌牌面分的一半;底牌仍按最后一轮胜负正常结算。"
},
{
"name": "聚类分析",
- "content": "可以将一张非分数、非主点数的数字牌当作与其绝对值不超过1的牌打出(转化后的牌也不能是分数牌、主点数)。"
+ "content": "发动后可依次点击多张非分数、非主点数的普通牌,明确选择各自要临时视为的相邻点数,转换完成后再自行选牌按基本规则出牌;转化前后均不得为分牌或主点数。甩牌时,其他玩家能借此组成更大组件也会令甩牌失败。"
},
{
+ "id": "meticulous_accounting",
"name": "锱铢必较",
- "content": "本局的分数牌是A,2,3,4,5,6,7,闲家开局-10分。"
+ "content": "本局的分数牌改为 A、2、3、4、5、6、7,分别计 1、2、3、4、5、6、7 分;闲家以 -10 分开局。"
},
{
+ "id": "lost_in_fog",
"name": "如堕云雾",
- "content": "禁止查看已打出的牌和已获得的分数。"
+ "content": "出牌阶段不保留上一轮出牌记录,并隐藏本轮分值、闲家总分及已获得的分牌;终局结算时统一公开。"
},
{
+ "id": "birds_gone_bow_hidden",
"name": "鸟尽弓藏",
- "content": "一种花色的分数牌全部打出后,不能再主动打出该花色。"
+ "content": "某一有效花色的所有5、10、K分牌全部实际打出后,一号位不能再主动打出该有效花色;跟牌不受影响。带分级牌统一计入主花色,不计入原牌面副花色。若手中只剩已弓藏花色,则解除限制。"
},
{
+ "id": "odd_even_scoring",
"name": "无独有偶",
- "content": "奇数轮次的分数为0,偶数轮次的分数为原分数的两倍。"
+ "content": "奇数轮的分牌不进入计分区且计0分;偶数轮的分牌进入计分区,并按牌面原分的两倍计入。底牌仍按常规抠底规则结算。"
},
{
+ "id": "second_battlefield",
"name": "第二战场",
- "content": "每当每名玩家打出的牌数不小于5时,使用这些牌进行一次德州扑克,获胜者所属的阵营获得5分。"
+ "content": "每场先公开5张公共牌。各家打出且尚未参赛的牌会明置在各自桌前并跨轮累计;只在轮末且四家均累计至少5张时由系统判定德州最佳牌型,胜者阵营获得5分,随后收走参赛牌、更换公共牌并重新累计。某场累计达到5张时,只有四家剩余手牌都严格少于5张才延至全部出完后进行最终判定;正好剩5张仍照常判定。王不参与牌型;双副牌的重复实体分别计算,五条高于同花顺;同阵营并列只奖励一次,跨阵营并列时双方各得5分。"
},
{
+ "id": "tai_chi_four_symbols",
"name": "太极四象",
- "content": "新增牌型:四张花色各不相同且点数相同的牌,此牌型按点数比较大小,若没有任何副花色则可以用四张主花色毙之。"
+ "content": "新增牌型:四张花色各不相同且点数相同的牌。四象只按点数比较;没有任何副牌时,可以用四张主牌毙之。"
},
{
+ "id": "forbidden_magic",
"name": "禁术秘法",
- "content": "玩家可以在一轮开始时声明将自己所有主牌不再视为主牌,可以将其当作任意花色点数相同的牌打出(王当作任意牌),限一次。"
+ "content": "每名玩家每局限一次,可在出牌阶段随时预备,并在轮首依次确认是否发动;确认后本局永久生效且不能撤销。发动者原有的全部主牌不再视为主牌,并可将其改作任意花色的原点数牌;王可改作任意普通点数,但有主局不能改作主花色。"
},
{
+ "id": "one_country_two_systems",
"name": "一国两制",
- "content": "庄家方、闲家方各自亮出主花色。"
- },
- {
- "name": "取长补短",
- "content": "玩家编号为0,1,2,3,第x轮中,编号为x%4的玩家的牌大小级别+1,编号为(x+2)%4的玩家的牌大小级别-1。"
+ "content": "庄家方与闲家方分别在本方内亮主、反主,双方声明互不覆盖。任意玩家亮一对王,或双方都未亮主时,双方无主;只有一方亮花色时,双方共用该主花色;双方亮出不同花色时,两种花色按阵营互换后统一进行牌型、跟牌、缺门、毙牌和大小判定。"
},
+ {
+ "id": "strength_compensation",
+ "name": "取长补短",
+ "content": "以庄家为0号位,逆时针依次为1、2、3号位。第x轮中,x%4号位的全部手牌沿当前牌力序列升一级,(x+2)%4号位降一级,轮末轮换。副牌留在原花色并跳过级牌点数,越过A成为F、越过最低普通牌成为1;主牌依次为主普通牌、副级牌、主级牌、小王、大王、白王,升降不会打乱原有内部次序。牌面变化不改变实体牌原有分值。"
+ },
{
+ "id": "wooden_ox_flowing_horse",
"name": "木牛流马",
- "content": "游戏开始时,2、3号位玩家获得“木牛流马”,持有木牛流马的玩家可以在一轮开始时将一张牌置入其中(已有时改为替换)并交给队友,队友可以如手牌般打出此牌。木牛流马中的牌不计入手牌花色限制,每局每人可以传递两次。当手牌不等对游戏造成影响时,玩家必须传递/收回木牛流马中的牌以使牌数正确。"
+ "content": "游戏开始时2、3号位各持有一具木牛流马(每队一具)。每轮首张牌打出前,持有者可以跳过操作、将其中原有的牌原样交给队友,或放入/替换一张手牌并必须立即交给队友;首张牌打出后本轮不得再操作。接收者可以跨轮保留其中的牌,并在自己的正常出牌回合将它如手牌般打出;该牌不计入持有者的跟牌花色义务。若队友已经无牌可出,持有者必须在轮首将木牛流马交给队友以恢复牌数。每队的木牛流马每局最多往返两次(共四次单程传递)。"
},
{
"name": "再续前缘",
"content": "打出一张牌时,若另一张相同的牌在本轮或上轮已经被打出,其所属阵营获得2分。"
},
{
+ "id": "unarmed",
"name": "手无寸铁",
- "content": "移除4张王牌并令所有主点数牌大小视为原本的数字大小。"
+ "content": "开局移除4张王牌。级牌仍可用于亮主、反主;进入出牌阶段后不再因级牌点数成为主牌,而是回到自身花色并按原点数大小参与跟牌、牌型和胜负比较。"
},
{
+ "id": "mutual_support",
"name": "同舟共济",
- "content": "每名玩家限一次,任意轮次轮到某玩家出牌时,可以要求队友交给自己0-2张牌或选择交给队友1-2张牌,本轮结束时获得牌的玩家再交给给出牌的玩家等量手牌。"
+ "content": "每名玩家每局限一次;轮到自己且尚未出牌时,可以要求队友交给自己0至2张牌,或选择1至2张手牌交给队友。本轮结束时,获得牌的玩家再选择等量手牌返还给原持有者。"
},
{
+ "id": "candle_to_dawn",
"name": "烛尽天明",
- "content":"游戏开始时庄家队友选择是否点燃“烛”。每轮出牌结束时根据本轮四号位的出牌调整“烛”的状态:若其打出的牌只含有黑色,则熄灭“烛”,若只含有红色,则点燃“烛”,若均不满足则不改变“烛”的状态。“烛”处于点燃的轮次,红色分数牌具有的的分数+5,黑色分数牌具有的分数-5;“烛”处于熄灭的轮次,红色分数牌具有的的分数-5,黑色分数牌具有的分数+5。"
+ "content":"游戏开始时庄家队友选择是否点燃“烛”。每轮先按该轮开始时的烛态结算分数,结算完成后再根据本轮四号位的出牌决定下轮烛态:若其打出的牌只含黑色,则熄灭“烛”;若只含红色,则点燃“烛”;否则不变。“烛”点燃的轮次,红色分数牌每张+5分,黑色分数牌每张-5分;“烛”熄灭的轮次则相反。小王是黑色,大王是红色。"
},
{
+ "id": "cultural_revolution",
"name": "文化革命",
- "content": "每名玩家限一次,作为一号位出牌时可以选择声明一种花色/点数(不能为10/K),两轮之内该花色/点数视为主花色/主点数,若发动时已经处于此效果生效区间内则覆盖之。"
+ "content": "每名玩家每局限一次;作为一号位出牌前,先选择“革花色”或“革点数”,再声明一种花色或2至A的一种点数(包括10、K)。发动当轮及下一轮内,所选花色替换原主花色,或所选点数替换原级牌点数;被替换的原主暂按副牌处理。生效期间的新声明会整体覆盖旧声明,并重新持续两轮。"
},
{
+ "id": "three_tigers",
"name": "三人成虎",
- "content": "一轮中,若至少3名玩家打出的牌花色均相同,则该花色在本轮结算时大小视为主花色,但大小-4。"
+ "content": "一轮中,每名玩家本次打出的全部牌均为同一副花色的副牌时,为该花色计一人;同一副花色累计达到三人后立即成虎。主花色牌、级牌和王不计人数,也不转换。该轮桌上及此后打出的该副花色牌全部视为主牌,并沿剔除本局级牌后的普通牌序列降低4级;例如4为级牌时,降级过程跨过4这一档,最低为−2。转换只影响本轮牌力,不改变实体牌原有分值。"
},
{
"name": "势者制权",
"content":"游戏开始时庄家队友获得“权”并选择一名对方玩家,令其获得“势”。在一轮中,若持有“权”的玩家位于持有“势”玩家的前置位,则后者本轮打出的分数在计算时不会超过前者;若持有“权”的玩家位于持有“势”玩家的后置位,则前者本轮打出牌的大小在计算时不会小于后者且视为先手(均为垫牌则不触发此条)。以上两条效果任意之一被触发的轮次结束时,持有“权”的玩家需将“权”交给持有“势”的玩家,后者再选择对方的一名玩家交给其“势”。"
},
{
+ "id": "invite_into_urn",
"name": "请君入瓮",
- "content": "每名玩家限一次,作为首置位时可以指定一名玩家和一张牌,若其本轮打出了此牌,则失去5分。"
+ "content": "每名玩家每局限一次;作为一号位出牌前,可指定一名其他玩家和一种实体牌面。本轮只要目标玩家打出至少一张该牌面,其阵营失去5分;无论同一次打出一张还是多张,都只扣5分。若目标属于庄家方,等价为闲家增加5分。"
},
{
+ "id": "teammate_cheer",
"name": "队友加油",
- "content": "没有主牌时可以声明之并令队友在本局游戏中的所有牌大小级别+1。"
+ "content": "每次出牌后,若该玩家还持有至少一张牌、剩余手牌中已经没有主牌且尚未发动过,系统询问是否声明“队友加油”。确认后其队友获得永久加油 Buff,剩余手牌及之后打出的牌沿当前完整牌力序列全部提升一级;副牌不会跨入主牌链,A之上为F,大王之上为白王。获得 Buff 的玩家全场标记,实体牌原有分值不变;暂不发动后可在后续满足条件的出牌后再次选择。"
},
{
+ "id": "old_horse_still_has_strength",
"name": "老骥伏枥",
- "content": "最后一个获得出牌权的玩家的第一次出牌视为必定最大。"
+ "content": "开局首发者视为已经获得牌权。四名玩家都至少获得过一次牌权时,最后首次获得牌权的玩家,其随后第一次合法首发视为绝对最大,任何后手都不能压过;该次出牌仍须完整遵守首发牌型与甩牌规则。"
},
{
"name": "第三战场",
"content": "一号和二号位使用每轮打出的四张牌(王牌则不触发)进行加减乘除24点游戏,率先算出者+3分(四号位须在20s内出牌)。"
},
{
+ "id": "trump_wins",
"name": "Trump wins",
- "content": "打出分数最多的玩家下轮先出牌。"
+ "content": "每轮仍按正常牌力判定胜者并结算分数,但下一轮改由本轮实体出牌总分最高的玩家先出;若最高分有多人并列,则其中先出牌的玩家获得牌权。"
},
{
+ "id": "openly_revealed",
"name": "昭然若揭",
- "content": "底牌始终保持明置。"
+ "content": "底牌从摸牌开始便在牌桌中央明置;庄家重新埋底后,明置内容同步换成新底牌。整局所有玩家都可随时查看当前底牌。"
},
{
+ "id": "straw_boat_borrowing_arrows",
"name": "草船借箭",
- "content": "若首置位的玩家打出的牌中包含分数之和不小于10且这些分数牌没有被其所属阵营获得,其可以在此轮结束时弃置一张非分数牌并获得本轮打出的非分数牌中最大的一张。"
+ "content": "若首置位玩家本轮打出的实体分牌合计不少于10分,且这些分牌最终被对方阵营获得,轮末该玩家可以选择发动:向全场公开弃置一张剩余手牌中的非分牌,并获得本轮打出的、按当前主牌体系牌力最大的单张非分牌。若手中无可弃牌或本轮没有非分牌,则不能发动。"
},
{
+ "id": "bush_gate",
"name": "布什戈门",
- "content": "每名玩家限一次,作为二号位时若认为一号位打出的牌过于离谱,可以令其收回并重新打出另一(些)合法的牌。"
+ "content": "每名玩家每局限一次;作为本轮二号位且尚未出牌时,可以迫使一号位收回本轮首发并重新合法首发。被收回的每一张牌在这次重新首发中均不可使用;重新首发成功后限制立即解除,后续轮次可正常使用。若一号位没有其他手牌则不能发动。"
},
{
"name": "千金散尽",
"content": "闲家的的分数为 200 - 实际得到的分数,不能低于0分。"
},
{
+ "id": "illusion_and_reality",
"name": "虚虚实实",
- "content": "每名玩家限一次,若某副花色的牌剩余奇数张,出牌时可以视为没有此花色的牌(若利用此规则,则不能打出该花色的牌)。"
+ "content": "每名玩家每局限一次。跟随副花色时,若自己手中该副花色恰好剩余奇数张,可以发动并将这些牌虚置,本次出牌完全视为手中没有该花色;被虚置的牌不能在本次出牌中打出,仍须从其余手牌中打出与首家相同的张数。"
},
{
+ "id": "strive_upstream",
"name": "力争上游",
- "content": "每轮的出牌顺序为上轮打出牌的大小顺序(垫牌按照字典序比较单张大小,考虑主花色)。"
+ "content": "每轮的出牌顺序按上轮四家出牌由大到小排列。以一号位的牌型为准:同牌型按正常牌力比较;牌型不一致时,先比与首家牌型的贴合程度,异花色牌越多越小(包括主牌),数量相同再从最小牌起按字典序比较;完全相同时后出者更小。"
},
{
+ "id": "afterglow",
"name": "回光返照",
- "content": "当玩家的主牌不大于 3 张时,其出牌可以不受花色限制且打出的主牌大小+1。若使用此规则进行出牌,则之后的每一轮必须打出主牌直到没有主牌为止。"
+ "content": "每名玩家每局限一次。开局一号位若持有1至3张主牌,可在首次出牌前直接发动;其余情况下,每次出牌后若该玩家仍有手牌且只剩1至3张主牌,系统询问是否发动。确认后剩余主牌立即沿当前完整主牌序列提升一级;从下一次出牌起,只要手中仍有主牌便无视通常的跟牌要求,但每次出牌只能打出主牌、不能混入副牌。主牌出尽后效果结束;实体牌原有分值不变,暂不发动不消耗机会。"
},
{
+ "id": "outward_harmony_inner_division",
"name": "貌合神离",
"content": "任意一方两名玩家在一轮中若打出的牌型不一致(不考虑花色),则对方阵营获得5分。"
},
{
+ "id": "ambiguous",
"name": "模棱两可",
- "content": "每名玩家限一次,出牌时可以展示两种不同的合法出牌方式,在本轮结束时选择其中一种(多人发动则后续玩家再发动无须消耗次数,并从后置位向前选择)。"
+ "content": "每名玩家每局限一次,仅本轮二、三号位可发动:同时公开两种不同且各自合法的出牌方案,四家出完后再选择其中一种结算。若二、三号位都发动,则三号位作为后续发动者不消耗次数,并由三号位先选、二号位后选。"
},
{
"name": "良禽择木",
"content": "游戏开始前,将所有 A K 移出游戏并在摸牌结束后平均分配给玩家。"
},
{
+ "id": "two_ghosts_knock_door",
"name": "二鬼拍门",
- "content": "摸到不小于2张王牌时须将其全部亮出。"
+ "content": "摸牌阶段中,任一玩家手牌里的王达到至少两张时,立即将其持有的全部王作为明置手牌公开;之后再摸到的王也继续明置。王仍属于原玩家手牌,打出或离开手牌后便从明置区消失。"
},
{
"name": "正大光明",
@@ -357,47 +430,56 @@
},
{
"name": "互通有无",
- "content": "队友的手牌对你可见。"
+ "content": "进入出牌阶段后,队友的手牌仅对你可见。"
},
{
+ "id": "double_happiness",
"name": "双喜临门",
- "content":"重新从三个规则中选择两个(不矛盾的)规则,于本局游戏中同时生效。"
+ "content": "重新从三个规则中选择两个规则,于本局游戏中同时生效。房主可以刷新其中任意一条候选。"
},
{
+ "id":"people_commune",
"name":"人民公社",
- "content":"每名玩家各埋两张底牌,闲家开局-20分。抄底时,双方均获得且仅获得对方所埋的底牌中的分数。"
+ "content":"摸牌时不留底牌,四名玩家各摸27张,再各自埋两张;每人可随时查看自己埋的两张牌及其分值。闲家开局-20分。抄底时只结算对方阵营埋下的四张牌:闲家拿底获得庄家方埋分;庄家方拿底则抄走闲家埋分,使闲家按底牌倍数扣分。"
},
{
"name":"市场经济",
"content":"初始时市场具有8张牌,每过3轮每人将一张牌投入市场,根据本轮的大小依次从市场中获得一张牌。游戏结束时闲家获得市场中的一半分数。"
},
{
+ "id":"remove_firewood_from_under_cauldron",
"name":"釜底抽薪",
- "content":"被反主的玩家可以在反主结算后与其交换手牌。(反多次则由后向前结算)"
+ "content":"每次反主后,被反主的玩家获得一次选择:亮主与反主全部结算后,可以与反主者交换当前全部手牌,也可以不交换。若发生多次反主,按反主发生顺序由后向前依次询问和结算。"
},
{
+ "id":"mainstay",
"name":"中流砥柱",
- "content":"摸牌结束后,若本局不是无主局,则从一号位开始,若玩家的主牌数不大于5,则可以将包括所有主牌在内的5张牌交给队友,然后队友再交给其5张牌。"
+ "content":"摸牌结束后,若本局不是无主局,则从一号位开始依次处理每名玩家。轮到某玩家时,若其当前主牌数不大于5,可以将包括全部主牌在内的5张牌交给队友,再由队友返还5张牌;每名玩家均按轮到自己时的当前手牌独立判断,因此同队两人都可以发动。"
},
{
+ "id":"happy_twins",
"name":"欢乐成双",
- "content":"庄家与上家交换位置。"
+ "content":"庄家锁定后与上家交换位置,但双方原有组队关系不变;本局结束后恢复原座次。若庄家方获胜,下一局仍由庄家的固定队友上庄;若闲家方获胜,则由换位后庄家的下家(即换位前的庄家上家)上庄。"
},
{
+ "id":"encircle_three_missing_one",
"name":"围三阙一",
- "content":"将除王牌与级牌之外的所有牌分为四种花色,记录游戏中最先出现的三种花色,若最后剩余的一种花色不为主花色,则该花色从下一轮开始替换原本的主花色,然后无论是否发生替换,删除所有记录并重新开始记录。"
+ "content":"摸牌阶段照常亮主。进入出牌阶段后,忽略王牌和级牌,每次出牌后即时记录本次出现的全部普通花色;若一次更新后四种花色齐全,则立即清空全部记录,并从下一位玩家出牌重新统计。每轮结束时若恰好记录了三种花色,缺少的第四种花色若不是当前主花色,则从下一轮起替换当前主花色;无论是否换主,随后均清空记录并重新统计。"
},
{
+ "id":"three_six_nine_grades",
"name":"三六九等",
- "content":"本局除亮主外,玩家可以额外亮出一种主花色以使该花色的牌成为“劣花色”,之后游戏中普通副牌相对于劣牌的关系等同于主牌相对于副牌的关系,同时该花色的级牌小于副花色的级牌(即主花色>副花色>劣花色)。亮出劣花色的过程遵循与主牌相同的反主规则,对王即无劣花色,一张/一对级牌/王牌只能用于亮主与亮劣其中之一。"
+ "content":"亮牌阶段分为亮主与亮劣两条独立的反亮链,均可用一张或一对本花色级牌并按通常强度加固、反亮;王只能亮主。任一普通花色一旦用于亮主或亮劣,其他玩家便不能再在任一链使用该花色。若最终没有普通花色主(无人亮主或对王无主),或无人亮劣,则本局没有劣花色。出牌时主牌大于普通副牌,普通副牌可以像主牌毙副牌一样毙劣牌;劣花色级牌小于其他副花色级牌。"
},
{
+ "id":"iron_evidence",
"name":"铁证如山",
- "content":"若在一轮中出现了小王、红桃Q、黑桃J、梅花J,则本轮打出的分数翻倍,每多出现一张则倍数额外+1;若两张大王均已被打出,则改为本轮不计分。"
+ "content":"每轮开始时锁定铁证牌的效果。若此前两张大王尚未全部打出,本轮每出现一张小王、红桃Q、黑桃J或梅花J,本轮牌面总分的倍数便在1倍基础上加1;若本轮开始前两张大王均已打出,则本轮只在出现至少一张上述铁证牌时清零牌面总分,没有铁证牌则正常计分。本轮内才打出的第二张大王不影响本轮效果,只从下一轮开始生效。"
},
{
+ "id":"waiting_rabbit",
"name":"守株待兔",
- "content":"游戏开始时,每名玩家可以暗中指定一张牌(不能是王牌、主点数),然后当此牌被其他玩家打出时,当轮座次最先指定了此牌的玩家可以用一张非分数牌与之交换(只交换一张,一张牌的分数只在其首次被打出时计算一次)。"
+ "content":"埋底完成后,每名玩家暗中指定一张有花色的目标牌,不能指定王或当前级牌,但可以指定5、10、K。当其他玩家在一轮中打出该牌面时,目标牌先正常参与本轮牌力与首次计分;本轮结算完成后,由本轮座次最先指定它且尚未成功交换的玩家决定是否发动,可以用手里一张非分牌与其中一张目标牌交换:目标牌进入其手牌,非分牌原位顶替留在桌上的目标牌,但不倒改本轮赢家或得分。每张实体分牌只在全局首次上桌时计分,被换回手牌后再次打出只保留牌面大小,不再计分。"
},
{
"name":"破釜沉舟",
@@ -408,31 +490,48 @@
"content":"摸牌时随机一部分牌正面向上。"
},
{
+ "id":"hidden_dragon_in_abyss",
"name":"潜龙在渊",
- "content":"游戏开始时每名玩家指定自己最多的点数(主点数除外),手牌数首次不大于12时,若玩家从未打出过该点数,则所属阵营获得10分。"
+ "content":"埋底完成后,每名玩家公开声明自己手牌中数量最多的一个点数(本局级牌除外);若最多点数唯一则由系统自动声明,并列时由玩家自行选择。玩家的手牌数首次不大于12时,若其此前及本次均未打出过所声明的点数,则所属阵营获得10分。"
},
{
+ "id":"administrative_review",
"name":"行政审查",
- "content":"庄家12张底牌,游戏开始前,庄家下家指定一种副花色,庄家上家指定一种点数,直到庄家方打出过该花色和该点数的牌才可埋底牌。"
+ "content":"本局有12张底牌,四家各持24张牌开始出牌。开始出牌前,庄家下家公开指定一种副花色,庄家上家公开指定一种点数;底牌继续封存,庄家不可查看。庄家或其队友累计打出过指定副花色和指定点数后,庄家才收到并查看12张底牌,立即埋12张后继续当前牌局。"
},
{
+ "id":"political_review",
"name":"政治审查",
- "content":"每名玩家限一次,可以令队友收回其打出的牌并重新打出(可以继续选择同样的出牌)。"
+ "content":"每名玩家每局限一次。在该玩家尚未发动前,其队友每次提交出牌后都暂停牌局并询问该玩家是否令队友收回。选择收回才消耗次数;收回只传递反对信号,不产生任何禁出限制,队友可以立即原样重新出牌。选择放行不消耗次数。"
},
{
+ "id":"no_one_survives",
"name":"无人生还",
- "content":"除一号位的出牌均暗置,在一轮结束后统一展示。"
+ "content":"每轮一号位的出牌正常明置;二、三、四号位的出牌在本人视角始终明置,对其他玩家自动暗置且只公开张数,本轮牌面总分也不提前显示。四家出完后统一公开暗牌与本轮总分,再按正常规则结算胜负和下轮牌权。"
},
{
+ "id":"lure_tiger_from_mountain",
"name": "调虎离山",
- "content": "每方阵营限一次,一轮开始前可以指定一名非一号位玩家,令其本轮无法响应甩牌询问,且打出的牌不计大小与分数。"
+ "content": "每方阵营每局限发动一次。每名玩家都可在出牌阶段随时预备,系统在目标轮开始、首张牌打出前按本轮座次依次询问预备者;同队首名确认并选定目标的玩家抢占本方次数,其队友不能再发动。发动者指定一名本轮非一号位玩家,使其本轮无法参与甩牌询问,且本轮打出的牌不计大小与分数。"
},
{
+ "id": "defense_as_offense",
"name": "以守为攻",
- "content": "每轮若一号位玩家打出的牌每小于一名其他玩家,则其下轮打出的牌大小+1。"
+ "content": "每轮结束后,按“力争上游”的完整牌力顺序比较四家出牌;本轮一号位每小于一名其他玩家,其下一轮全部手牌牌面便提升一级,并在玩家名旁标记对应的“+X”。加成只持续下一轮,实体牌原有分值不变;F与白王不再提升,其他牌按完整牌力序列提升并在白王处封顶,因此大王即使+3仍为白王。"
},
{
+ "id": "antinomy",
"name": "二律背反",
- "content": "游戏开始前每名玩家各自指定一张牌(花色+点数)并同时亮出,被指定的牌本局中视为两张不同的牌(即无法成对),若有多名玩家指定了同一张牌,则不执行此效果。每轮结束后,本轮中指定牌被打出过的所有玩家重新指定一张牌并同时亮出。"
+ "content": "庄家埋完底、正式开始出牌前,四名玩家各自选择一张普通花色牌面(花色+点数),收齐后同时亮出。仅被一名玩家指定的牌面,其两张实体牌本局视为不同牌,不能组成对子或拖拉机;同一牌面被多人指定时不执行拆对效果。每轮结束后,本轮中自己所指定牌面曾被任何玩家打出的声明者重新选择,收齐后同时更新;完成前不能开始下一轮出牌。"
+ },
+ {
+ "id": "change_rice_to_mulberry",
+ "name": "改稻为桑",
+ "content": "庄家埋底后、正式出牌前,两名闲家分别选择自己手牌中⌊分牌数÷2⌋张分牌;原本为副牌的变为同花色A,原本为主牌的变为大王。被改造的实体牌永久失去分值,只按新牌面参与牌型、跟牌和大小结算。"
+ },
+ {
+ "id": "destroy_dyke_flood_fields",
+ "name": "毁堤淹田",
+ "content": "庄家每局限发动一次。闲家赢得一轮后、该轮计分前,庄家可以令本轮所有分数作废并记录作废分数。接下来的三轮为灾期,期间照常计分;若闲家在灾期内累计获得不少于20分,或游戏在灾期结束前结束,则视为事发,闲家取回作废分数并额外获得20分,随后结束灾期。若牌局恰于灾期第三轮结束,且闲家拿底,也视为事发。若三轮结束时仍未事发,记录的分数永久作废。"
}
-]
\ No newline at end of file
+]
diff --git a/tractor-game-simulator/client/src/App.jsx b/tractor-game-simulator/client/src/App.jsx
index bf4c7dc..1307d5a 100644
--- a/tractor-game-simulator/client/src/App.jsx
+++ b/tractor-game-simulator/client/src/App.jsx
@@ -1,12 +1,14 @@
import { useState, useEffect } from 'react';
-import { Layout, Typography, Button, message, Space, Tabs, Tag, Divider, Modal, InputNumber } from 'antd';
+import { Layout, Typography, Button, message, Space, Tabs, Tag, Divider, Modal, InputNumber, Checkbox, Select } from 'antd';
import socketService from './services/socket';
import { useGameStore } from './store/gameStore';
import CreateRoomModal from './components/Room/CreateRoomModal';
import JoinRoomModal from './components/Room/JoinRoomModal';
import RoomList from './components/Room/RoomList';
import GameBoard from './components/Game/GameBoard';
-import { SOCKET_EVENTS, GamePhases } from './utils/constants';
+import { SOCKET_EVENTS } from './utils/constants';
+import { shouldShowGameBoard } from './utils/gameViewUtils';
+import { RULE_SELECT_OPTIONS } from './utils/ruleCatalog';
import './styles/App.css';
const { Header, Content } = Layout;
@@ -20,8 +22,9 @@ function App() {
const [selectedRoomId, setSelectedRoomId] = useState('');
const [loadingRooms, setLoadingRooms] = useState(false);
const [showConfigModal, setShowConfigModal] = useState(false);
- const [newBottomCardsCount, setNewBottomCardsCount] = useState(8);
const [newDealInterval, setNewDealInterval] = useState(500);
+ const [testMode, setTestMode] = useState(false);
+ const [testRuleId, setTestRuleId] = useState('normal_game');
useEffect(() => {
// 连接Socket
@@ -131,8 +134,9 @@ function App() {
name: values.roomName,
playerName: values.playerName,
config: {
- bottomCardsCount: values.bottomCardsCount,
- dealInterval: values.dealInterval
+ dealInterval: values.dealInterval,
+ testMode: values.testMode === true,
+ testRuleId: values.testMode ? values.testRuleId : null
}
});
};
@@ -184,10 +188,6 @@ function App() {
};
const handleUpdateConfig = () => {
- if (newBottomCardsCount < 1 || newBottomCardsCount > 20) {
- messageApi.warning('底牌数量必须在1-20之间');
- return;
- }
if (newDealInterval < 10 || newDealInterval > 5000) {
messageApi.warning('发牌间隔必须在10-5000毫秒之间');
return;
@@ -196,8 +196,9 @@ function App() {
socket.emit(SOCKET_EVENTS.UPDATE_CONFIG, {
roomId: currentRoom.id,
config: {
- bottomCardsCount: newBottomCardsCount,
- dealInterval: newDealInterval
+ dealInterval: newDealInterval,
+ testMode,
+ testRuleId: testMode ? testRuleId : null
}
});
setShowConfigModal(false);
@@ -213,17 +214,16 @@ function App() {
// 同步房间配置
useEffect(() => {
if (currentRoom?.config) {
- setNewBottomCardsCount(currentRoom.config.bottomCardsCount);
setNewDealInterval(currentRoom.config.dealInterval);
+ setTestMode(currentRoom.config.testMode === true);
+ setTestRuleId(currentRoom.config.testRuleId || 'normal_game');
}
}, [currentRoom]);
// 如果在房间内,显示房间界面或游戏界面
if (currentRoom) {
- // 如果游戏已开始(不在WAITING阶段)或正在等待准备,显示游戏界面
- const gamePhase = currentRoom.gameState?.phase || GamePhases.WAITING;
- const isWaitingForReady = currentRoom.gameState?.isWaitingForReady || false;
- if (gamePhase !== GamePhases.WAITING || isWaitingForReady) {
+ // 规则选择属于两局之间的牌桌流程,不能因为阶段暂时回到 WAITING 就卸载牌桌。
+ if (shouldShowGameBoard(currentRoom.gameState)) {
return
底牌数量: {currentRoom.config.bottomCardsCount} 张
+默认底牌: 8 张(特殊规则可能调整)
发牌间隔: {currentRoom.config.dealInterval} 毫秒
+规则模式: {currentRoom.config.testMode + ? `测试模式(${RULE_SELECT_OPTIONS.find(option => option.value === currentRoom.config.testRuleId)?.label || currentRoom.config.testRuleId})` + : '正常随机二选一'}
你是庄家队友,请选择第1轮的烛态。
++ 点燃:红色分牌每张 +5,黑色分牌每张 −5; + 熄灭则相反。小王是黑色,大王是红色。 +
+第一步:选择这张王要变成的花色
++ 请选择这张牌要临时视为的相邻点数。分牌和级牌已自动排除。 +
++ {cardTransformationDialog.isJoker + ? '第一步:选择王要变成的副牌花色。当前主花色不可选。' + : `选择这张 ${cardTransformationDialog.sourceRank} 要视为的花色;点数保持不变。`} +
++ 未主动改变花色的普通主牌会按牌面原花色、原点数作为副牌;级牌同样恢复原点数。 +
+ > : <> ++ 是否同意由 {focusFigureVote?.nomineePlayerName || ''} 担任本队焦点人物? +
++ 选择不同意不会结束表决;系统会把候选切换给本队另一名玩家,再由两人重新投票。 + 候选与票型只在本队内部可见。 +
++ 你目前有 {lastStandDecision?.cardsCount || 0} 张同花色手牌且没有主牌, + 是否令这些牌全部视为主牌? +
++ 暂不发动不会消耗机会;下次手牌变化后若仍满足条件,会再次询问。 +
++ 你出牌后已经没有主牌。是否为队友{' '} + {teammateCheerDecision?.teammatePlayerName || ''} 加油, + 令其余下牌面永久全部提升一级? +
++ 副牌不会跨入主牌链;副A升为F,大王升为白王,实体牌原有分值不变。 + 暂不发动不会消耗机会,之后仍满足条件时会再次询问。 +
++ 队友 {politicalReviewDecision?.teammatePlayerName || ''} 打出了: +
++ 选择收回才会消耗你本局唯一一次政治审查。收回仅表示你不赞成这手牌, + 不会禁用其中任何牌;队友仍可立刻把完全相同的牌再出一次。选择放行不消耗次数, + 以后队友出牌时仍会继续询问。 +
++ {afterglowDecision?.triggerTiming === 'before_first_play' + ? `你是开局一号位,手中有 ${afterglowDecision?.trumpCount || 0} 张主牌,是否在首次出牌前直接发动回光返照?` + : `你出牌后还剩 ${afterglowDecision?.trumpCount || 0} 张主牌,是否发动回光返照?`} +
++ 确认后剩余主牌会立即沿完整主牌序列提升一级。从下一次出牌起,只要手中仍有主牌 + 便无视通常的跟牌要求,但整次出牌只能由主牌组成,不能混入任何副牌;实体牌原有分值不变。 + 主牌出尽后效果结束, + 暂不发动不会消耗机会。 +
++ 四家已经出完。请选择本轮最终采用的出牌方案;确认后不能更改。 +
++ {ambiguousChoice?.usageConsumed + ? '本次发动会消耗你每局一次的机会。' + : '你是本轮后续发动者,本次不消耗发动次数。'} +
++ 是否令本轮一号位收回刚才的全部首发牌,并重新合法首发? +
++ 被收回的每一张牌都不能用于紧接着的重新首发;一号位成功改出其他牌后,限制立即解除。 + 选择暂不发动不会消耗每局一次的机会。 +
++ 是否令下家先出牌,并把自己改为本轮最后出牌? +
++ 选择“否”不会消耗本局唯一一次发动机会;本技能只改变牌序,不改变牌的大小。 +
++ 先选择本次革命的性质。两种效果二选一,不会同时改变主花色和级牌点数。 +
++ 发动当轮和下一轮生效;原主对应项暂时变回副牌。若期间另一名玩家发动,新声明会覆盖旧声明并重新计两轮。 +
+ 你当前的主牌不超过5张,可以发动中流砥柱。 +
++ 发动后须将包含当前全部主牌的5张牌交给队友,再由队友选择5张牌返还。 + 队友稍后轮到自己时仍会按当时的手牌重新判断,也可以再次发动并把主牌交回来。 +
+ > + ) : ( + <> ++ {ownMainstayAction?.stage === 'return' + ? `请选择5张牌返还给 ${currentRoom?.players?.find(player => player.id === ownMainstayAction?.actorPlayerId)?.name || '队友'}。` + : `请选择5张牌交给 ${currentRoom?.players?.find(player => player.id === ownMainstayAction?.teammatePlayerId)?.name || '队友'};当前全部主牌已预先选中且不可取消。`} +
++ 已选择 {mainstaySelectedCardIds.length} 张,本次必须正好选择5张。 +
+ > + )} ++ 选择本次同舟共济的方向。向队友要牌时,队友可以交给你0至2张,也可以不给; + 主动给牌时,你必须交给队友1至2张。 +
++ 无论哪个方向,本轮结束时都由当前收牌的一方选择等量手牌返还给原持有者。 + 只有实际确认发动才会消耗每局一次的机会。 +
+ {mutualSupportMaxGiveCount < 1 && ( ++ 你目前必须保留全部手牌完成本轮出牌,因此暂时只能向队友要牌。 +
+ )} ++ {mutualSupportSelection?.stage === 'return' + ? `本轮结束,请选择${mutualSupportSelection?.requiredCards || 0}张手牌返还给 ${mutualSupportSelection?.otherPlayerName || '队友'}。` + : mutualSupportSelection?.stage === 'request' + ? `${mutualSupportSelection?.otherPlayerName || '队友'} 请求你交牌;你可以选择0至${mutualSupportSelection?.maxCards || 0}张。` + : `请选择1至${mutualSupportSelection?.maxCards || 2}张手牌交给 ${mutualSupportSelection?.otherPlayerName || '队友'}。`} +
++ 已选择 {mutualSupportSelectedCardIds.length} 张; + {mutualSupportSelection?.minCards === mutualSupportSelection?.maxCards + ? ` 本次必须正好选择${mutualSupportSelection?.minCards || 0}张。` + : ` 本次可以选择${mutualSupportSelection?.minCards || 0}至${mutualSupportSelection?.maxCards || 0}张。`} +
++ 是否与 {equivalentReciprocityTarget?.name || ''} 拼点? +
++ 确认后双方各自秘密选择一张牌;主牌大于副牌,副牌只比较点数。 + 输家一方失去5分,点数相同则无人失分,但拼点牌仍会交换。 +
+ + ++ 是否暗中交换 + {' '} + + {magicTrickTargetIds + .map(playerId => currentRoom?.players?.find(player => player.id === playerId)?.name) + .filter(Boolean) + .join(' 与 ')} + + {' '}本轮的结算出牌? +
++ 两人仍按各自真实手牌正常出牌;交换只在四家出完后影响本轮胜负、得分归属和下轮牌权。 +
++ 请暗选一张牌,与 {equivalentReciprocitySelection?.opponentPlayerName || ''} 拼点。 +
++ 你的选择在双方都提交前不会公开;提交后不能更改。 +
++ 你首置的 {strawBoatDecision?.leadingPoints || 0} 分未被本方获得,可以弃置一张非分数牌, + 换取本轮打出的最大非分数牌: +
+ {strawBoatDecision?.borrowedCard && ( ++ 请选择恰好一张非分数牌。弃掉的牌和获得的牌都会向全场公开;选择放弃则不交换任何牌。 +
++ 是否在第 {lureTigerDecision.round} 轮发动调虎离山? +
++ 确认后还需选择一名本轮非一号位玩家。选定目标才会抢占本方阵营唯一一次机会; + 暂不发动不会占用次数,同轮其他预备者会继续依次确认。 +
++ 请选择本轮要沉默的非一号位玩家 +
++ 目标本轮不参与甩牌询问,且整次出牌不计大小与分数;下一轮自动解除。 +
++ 是否从第 {forbiddenMagicDecision?.round || ''} 轮起发动禁术秘法? +
++ 确认后技能将立即生效并持续到本局结束,不能撤销;暂不发动不会消耗机会,以后仍可再次预备。 + 同轮其他预备者会继续依次确认,不受你的选择影响。 +
++ {removeFirewoodDecision?.counteringPlayerName || ''} 反了你的主。 + 是否与其交换当前的全部手牌? +
++ 这是一次可选机会,并非必须发动;选择“不交换”不会改动双方手牌。 + 若本局发生多次反主,系统会继续按照由后向前的顺序逐项询问。 +
++ 是否回到第 {timeReversalDecision?.round || ''} 轮开始前,收回本轮四家的出牌并重新出牌? +
++ 选择保留结果不会消耗技能;多名玩家预备时,首个确认倒流的人发动成功。 +
++ 请选择恰好{riceToMulberrySelection?.requiredCount || 0}张分牌。 + 副牌会变为同花色A,主牌会变为大王;改造后的实体牌永久计0分。 +
++ 闲家赢得第{destroyDykeDecision?.round || ''}轮,本轮原本获得 + {destroyDykeDecision?.roundPoints || 0}分。 +
+