Tractor

来自Botzone Wiki
跳转至: 导航搜索

简介

双升又称拖拉机,为游戏升级的扩展玩法,是全国范围内广为流传的一种扑克竞技游戏。四名玩家使用两副扑克牌进行2v2对战。四人轮流出牌,根据牌点获得一定的分数,按照总分判定胜负。

游戏规则

庄家与闲家

四名玩家分为两队,对家为一队,通过报牌确定其中一队为庄家,另一方为闲家。

级牌与主牌

级牌是一种特定点数的牌,级牌的点数为,每局开局时确定(默认为2);主牌包括所有级牌、大小王和一种特定花色的牌,这种特定花色称为主花色(主花色也可为“无”,详见特殊规则-无主),在报牌过程中确定。特别地,主花色级牌称为主级牌

发牌与报牌

发牌阶段开始前,保留8张底牌,剩余100张牌分发给四人,每人25张,采用逐轮发牌的方式,每轮给四人各发一张。 在发牌过程中,手中有级牌的玩家可以亮出单张级牌进行报主,之后该玩家所在方成为庄家,亮出级牌的花色成为主花色。在已有玩家报主的情况下,四名玩家仍然可以亮出一对级牌(同花色)或一对大王/小王进行反主,之后该玩家所在方成为庄家,亮出级牌的花色为主花色,若亮出的是大王/小王,则无主(详见特殊规则-无主)。报主和反主每局各至多进行一次。

盖底牌

发牌阶段结束,出牌阶段开始前,庄家可将8张底牌加入手牌并且选择8张牌盖覆为新的底牌。

牌型

常规的出牌牌型包括单牌、对子、连对(拖拉机)。

单牌:任意一张牌
对子:两张牌,要求同花色同点数
连对:点数顺序上连续的2个及2个以上同花色对子

另有特殊牌型:甩牌(详见特殊规则-甩牌)

大小顺序

非级牌的点数顺序自小到大为2,3,4,5,...,10,J,Q,K,A(剔除级牌),总体大小为其他牌<非级牌主花色牌<非主花色级牌<主级牌<大王、小王

出牌规则

每轮四名玩家都必须依次出牌,不能跳过。每轮有牌权的玩家第一个出牌。第一回合的牌权在最后报主/反主的玩家手中。后续玩家必须优先出与第一名玩家出牌牌型和花色相同的牌。若没有这类牌,则需要在尽可能凑齐牌型的前提下以同花色的其他牌凑齐,称为“贴”,若同花色的牌数量不足,则在出满同花色牌的前提下以其他花色的牌凑齐,也称为“贴”。若没有同花色的牌且第一名玩家出的牌不是主牌,则可以用主牌“毙”(牌型必须匹配,若不匹配则视为贴牌)。一轮四名玩家都出完牌之后,若无毙牌情况,牌型花色与第一名玩家匹配并且点数最大的玩家所在方得分且获得下一轮牌权;若有毙牌情况,则毙牌中点数更大的玩家得分且获得下一轮牌权。

得分及胜负规则

一场游戏中庄家和闲家的任务不同。若游戏结束时闲家得分不少于80分,则闲家获胜;否则庄家获胜。 得分渠道包括:

出牌得分:点数为5,10,k的牌为“分”,一轮结束后,统计本轮出牌中包含的“分”,本轮得分方获得全部的“分”,每有一张“5”获得5分,有一张“10”或“k”获得10分。
扣底得分:最后一轮出牌结束后,新的牌权方将按一定权重获得底牌中包含的所有分。
权重 = 出牌中最大标准牌型的牌数 * 2

特殊规则

无主:若玩家使用一对大王/小王反主,则本局无主,没有主花色,主牌中仅包含所有级牌和大小王,且级牌四种花色没有大小之分。
先胜:相同点数相同牌型的牌,若花色也没有大小之分,则先打出的牌更大(例如:两人均打出红桃2,则先打出的更大)。
随机定主:若发牌阶段内无人报主,则盖底牌阶段前将随机指定庄家和主花色。
甩牌:每轮第一名玩家出牌时,若手中有多组标准牌型均为当前最大(即其他人没有更大的同花色同牌型牌,不考虑用主牌毙),则可以将这些牌同时打出。本轮其他人出牌时也必须优先出甩牌中的牌型。

补充说明

拖拉机的“点数顺序”是剔除级牌的。例如,假设本局级牌为5,那么同花色的4466可以组成拖拉机,而4455、5566或445566等均不构成拖拉机。级牌不参与任何拖拉机的构成。

交互方式

人类玩家交互方式

人类玩家可以通过点击选中手牌后点击“报主”/“反主”/“出牌”来行动。

bot交互方式

本游戏与Botzone上其他游戏一样,使用相同的交互方式:Bot#交互

具体交互内容

交互中,游戏使用的108张牌是由0-107共108个整数进行编号的,对应关系为(0-h1 1-d1 2-s1 3-c1) (4-h2 5-d2 6-s2 7-c2) …… 52-joker 53-Joker(h-红桃,d-方片,s-黑桃,c-草花),对54~107有相同的排列。 每回合只有一个Bot会收到request。Bot收到的request是一个JSON对象。通常的request会包含一个"stage",告知bot当前所处的阶段。在不同阶段bot会获得不同的request并需要进行不同的response。

发牌:bot接收到"stage"为"deal",request同时包含了一个"deliver"键,其值为本回合发给这个玩家的牌。bot输出的response为报主/反主的牌(如果报主/反主的话),否则只需要输出一个空列表。
盖底牌:庄家bot接收到"stage"为"cover","deliver"包含8张底牌,需要输出的response为一个长为8的列表,包含8张新盖的底牌。
出牌:bot接收到"stage"为"play",request同时包含"history"键,其值为一个长为4的列表,前两个元素为最近2个回合所有玩家的历史行动,后面两个元素分别为最近两个回合牌权方(首个行动玩家)的id。(第一个回合内request["history"][0]为空)。

request内总是包含"global",其值为一个dict,存储了玩家需要了解的全局信息,包含级牌信息"level"与报主与反主情况"banking"(值为一个dict,"called"为报主玩家id,若无报主玩家则记为-1,"snatched"为反主玩家id,若无反主玩家记为-1;"major"为主花色,"n"表示无主;"banker"为当前的庄家id)。出牌阶段,"global"中将包含"total_score",记录闲家已经获得的分数。

交互样例

以下为bot不同阶段获得的request样例,请注意bot始终只需要输出一个列表作为response。

发牌:
{
	"stage": "deal",
	"deliver": [43],
	"global": {
		"level": "2",
		"banking": {
			"called": 1,
			"snatched": -1,
			"major": "c",
			"banker": 1
			}
		}
	},
盖底牌:
{
	"stage": "cover",
	"deliver": [12, 33, 47, 56, 89, 91, 102, 103],
	"global": {
		"level": "2",
		"banking": {
			"called": 1,
			"snatched": -1,
			"major": "c",
			"banker": 1
			}
		}
	},
出牌:
{
	"stage": "play",
	"history": [
	        	[
		        	[
					54
				],
				[
					62
				],
				[
					40
				],
				[
					44
				]
			],
			[
				[
					73
				]
			],
			1,
			1
		    ],
	"global": {
			"level": "2",
			"banking": {
				"called": 1,
				"snatched": 2,
				"major": "d",
				"banker": 2
			},
			"total_score": 0
		}
	},

样例程序

Python 版本

#######################################################
# 双升(Tractor)样例程序-By JackSimbol
# 其中有裁判程序的大量复用
#######################################################


import json
from collections import Counter
import os

cardscale = ['A','2','3','4','5','6','7','8','9','0','J','Q','K']
suitset = ['s','h','c','d']
Major = ['jo', 'Jo']
pointorder = ['3','4','5','6','7','8','9','0','J','Q','K','A','2']

def Num2Poker(num): # num: int-[0,107]
    # Already a poker
    if type(num) is str and (num in Major or (num[0] in suitset and num[1] in cardscale)):
        return num
    # Locate in 1 single deck
    NumInDeck = num % 54
    # joker and Joker:
    if NumInDeck == 52:
        return "jo"
    if NumInDeck == 53:
        return "Jo"
    # Normal cards:
    pokernumber = cardscale[NumInDeck // 4]
    pokersuit = suitset[NumInDeck % 4]
    return pokersuit + pokernumber

def Poker2Num(poker, deck): # poker: str
    NumInDeck = -1
    if poker[0] == "j":
        NumInDeck = 52
    elif poker[0] == "J":
        NumInDeck = 53
    else:
        NumInDeck = cardscale.index(poker[1])*4 + suitset.index(poker[0])
    if NumInDeck in deck:
        return NumInDeck
    else:
        return NumInDeck + 54

def checkPokerType(poker, level): #poker: list[int]
    poker = [Num2Poker(p) for p in poker]
    if len(poker) == 1:
        return "single" #一张牌必定为单牌
    if len(poker) == 2:
        if poker[0] == poker[1]:
            return "pair" #同点数同花色才是对子
        else:
            return "suspect" #怀疑是甩牌
    if len(poker) % 2 == 0: #其他情况下只有偶数张牌可能是整牌型(连对)
    # 连对:每组两张;各组花色相同;各组点数在大小上连续(需排除大小王和级牌)
        count = Counter(poker)
        if "jo" in count.keys() and "Jo" in count.keys() and count['jo'] == 2 and count['Jo'] == 2:
            return "tractor"
        elif "jo" in count.keys() or "Jo" in count.keys(): # 排除大小王
            return "suspect"
        for v in count.values(): # 每组两张
            if v != 2:
                return "suspect"
        pointpos = []
        suit = list(count.keys())[0][0] # 花色相同
        for k in count.keys():
            if k[0] != suit or k[1] == level: # 排除级牌
                raise ValueError("INVALID INPUT POKERTYPE")
            pointpos.append(pointorder.index(k[1])) # 点数在大小上连续
        pointpos.sort()
        for i in range(len(pointpos)-1):
            if pointpos[i+1] - pointpos[i] != 1:
                return "suspect"
        return "tractor" # 说明是拖拉机
    
    return "suspect"

def setMajor(major, level):
    global Major
    if major != 'n': # 非无主
        Major = [major+point for point in cardscale if point != level] + [suit + level for suit in suitset if suit != major] + [major + level] + Major
    else: # 无主
        Major = [suit + level for suit in suitset] + Major
    pointorder.remove(level)

# 检查是否有可应手牌型
# return: Exist(True/False)
def checkRes(poker, own, level): # poker: list[int]
    pok = [Num2Poker(p) for p in poker]
    own_pok = [Num2Poker(p) for p in own]
    if pok[0] in Major:
        major_pok = [pok for pok in own_pok if pok in Major]
        count = Counter(major_pok)
        if len(poker) <= 2:
            for k,v in count.items():
                if v >= len(poker):
                    if len(poker) == 1:
                        return [Poker2Num(k, own)]
                    else:
                        return [Poker2Num(k, own), Poker2Num(k,own) + 54]  
                        
        else: # 拖拉机 
            pos = []
            for k, v in count.items():
                if v == 2:
                    if k != 'jo' and k != 'Jo' and k[1] != level: # 大小王和级牌当然不会参与拖拉机
                        pos.append(pointorder.index(k[1]))
            if len(pos) >= 2:
                pos.sort()
                tmp = 0
                suc_flag = False
                for i in range(len(pos)-1):
                    if pos[i+1]-pos[i] == 1:
                        if not suc_flag:
                            tmp = 2
                            suc_flag = True
                        else:
                            tmp += 1
                        if tmp >= len(poker)/2:
                            out = []
                            for j in range(i+2):
                                out.extend([Poker2Num(pok[0][0]+cardscale[pointorder[j]], own), Poker2Num(pok[0][0]+cardscale[pointorder[j]], own)+54])
                            return out
                    elif suc_flag:
                        tmp = 0
                        suc_flag = False
    else:
        suit = pok[0][0]
        suit_pok = [pok for pok in own_pok if pok[0] == suit and pok[1] != level]
        count = Counter(suit_pok)
        if len(poker) <= 2:
            for k,v in count.items():
                if v >= len(poker):
                    if len(poker) == 1:
                        return [Poker2Num(k, own)]
                    else:
                        return [Poker2Num(k, own), Poker2Num(k,own) + 54]  
        else:
            pos = []
            for k, v in count.items():
                if v == 2:
                    pos.append(pointorder.index(k[1]))
            if len(pos) >= 2:
                pos.sort()
                tmp = 0
                suc_flag = False
                for i in range(len(pos)-1):
                    if pos[i+1]-pos[i] == 1:
                        if not suc_flag:
                            tmp = 2
                            suc_flag = True
                        else:
                            tmp += 1
                        if tmp >= len(poker)/2:
                            out = []
                            for j in range(i+2):
                                out.extend([Poker2Num(suit+cardscale[pointorder[j]], own), Poker2Num(suit+cardscale[pointorder[j]], own)+54])
                            return out
                    elif suc_flag:
                        tmp = 0
                        suc_flag = False
    return False

def parsePoker(poker, level):
# poker: 甩牌牌型 list[int]
# own: 各家持牌 list
# level & major: 级牌、主花色
    pok = [Num2Poker(p) for p in poker]
    outpok = []
    failpok = []
    count = Counter(pok)
    # 优先检查整牌型(拖拉机)
    pos = []
    tractor = []
    suit = ''
    for k, v in count.items():
        if v == 2:
            if k != 'jo' and k != 'Jo' and k[1] != level: # 大小王和级牌当然不会参与拖拉机
                pos.append(pointorder.index(k[1]))
                suit = k[0]
    if len(pos) >= 2:
        pos.sort()
        tmp = []
        suc_flag = False
        for i in range(len(pos)-1):
            if pos[i+1]-pos[i] == 1:
                if not suc_flag:
                    tmp = [suit + pointorder[pos[i]], suit + pointorder[pos[i]], suit + pointorder[pos[i+1]], suit + pointorder[pos[i+1]]]
                    del count[suit + pointorder[pos[i]]]
                    del count[suit + pointorder[pos[i+1]]] # 已计入拖拉机的,从牌组中删去
                    suc_flag = True
                else:
                    tmp.extend([suit + pointorder[pos[i+1]], suit + pointorder[pos[i+1]]])
                    del count[suit + pointorder[pos[i+1]]]
            elif suc_flag:
                tractor.append(tmp)
                suc_flag = False
        if suc_flag:
            tractor.append(tmp)
    # 对牌型作基础的拆分 
    for k,v in count.items(): 
        outpok.append([k for i in range(v)])
    outpok.extend(tractor)

    finalpok = outpok

    return finalpok 




##################################################################################
def call_Snatch(get_card, deck, called, snatched, level):
# get_card: new card in this turn (int)
# deck: your deck (list[int]) before getting the new card
# called & snatched: player_id, -1 if not called/snatched
# level: level
# return -> list[int]
    response = []
## 目前的策略是一拿到牌立刻报/反,之后不再报/反
## 不反无主
    deck_poker = [Num2Poker(id) for id in deck]
    get_poker = Num2Poker(get_card)
    if get_poker[1] == level:
        if called == -1:
            response = [get_card]
        elif snatched == -1:
            if (get_card + 54) % 108 in deck:
                response = [get_card, (get_card + 54) % 108]
    return response

def cover_Pub(old_public, deck):
# old_public: raw publiccard (list[int])
## 直接盖回去
    return old_public

def playCard(history, deck, level):
# history: raw history (list[list[int]])
# deck: your deck (list[int])
# level: level
# major: major (str)
# return -> move(list[int])
    if len(history) == 0:
        return [deck[0]] # 首发就乱出
    poker_deck = [Num2Poker(id) for id in deck]
    standard_move = history[0]
    standard_poker = [Num2Poker(id) for id in standard_move]
    #print(checkPokerType(standard_move, level))
    if checkPokerType(standard_move, level) != "suspect": # 不是甩牌
        response = checkRes(standard_move, deck, level)
        if response:
            return response
        else:
            if standard_poker[0] in Major:
                #print("major")
                deck_Major = [pok for pok in poker_deck if pok in Major]
                if len(deck_Major) < len(standard_poker):
                    out = deck_Major
                    deck_nMajor = [pok for pok in poker_deck if pok not in Major]
                    for i in range(len(standard_poker) - len(deck_Major)):
                        out.append(deck_nMajor[i])
                    attach_resp = []
                    _deck = deck + []
                    for pok in out:
                        cardid = Poker2Num(pok, _deck)
                        _deck.remove(cardid)
                        attach_resp.append(cardid)
                    return attach_resp
                else: 
                    target_parse = parsePoker(deck_Major, level)
                    target_parse.sort(key=lambda x: len(x), reverse=True)
                    target_len = len(standard_poker)
                    out = []
                    for poks in target_parse:
                        if target_len == 0:
                            break
                        if len(poks) >= target_len:
                            out.extend(poks[:target_len])
                            target_len = 0
                        else:
                            out.extend(poks)
                            target_len -= len(poks)
                    resp = []
                    _deck = deck + []
                    for pok in out:
                        cardid = Poker2Num(pok, _deck)
                        _deck.remove(cardid)
                        resp.append(cardid)
                    return resp
            else:
                #print("not_major")
                suit = standard_poker[0][0]
                deck_suit = [pok for pok in poker_deck if pok[0] == suit and pok[1] != level]
                if len(deck_suit) < len(standard_poker):
                    out = deck_suit
                    deck_nsuit = [pok for pok in poker_deck if pok not in deck_suit]
                    for i in range(len(standard_poker) - len(deck_suit)):
                        out.append(deck_nsuit[i])
                    attach_resp = []
                    _deck = deck + []
                    for pok in out:
                        cardid = Poker2Num(pok, _deck)
                        _deck.remove(cardid)
                        attach_resp.append(cardid)
                    return attach_resp
                else: 
                    target_parse = parsePoker(deck_suit, level)
                    target_parse.sort(key=lambda x: len(x), reverse=True)
                    target_len = len(standard_poker)
                    out = []
                    for poks in target_parse:
                        if target_len == 0:
                            break
                        if len(poks) >= target_len:
                            out.extend(poks[:target_len])
                            target_len = 0
                        else:
                            out.extend(poks)
                            target_len -= len(poks)
                    resp = []
                    _deck = deck + []
                    for pok in out:
                        cardid = Poker2Num(pok, _deck)
                        _deck.remove(cardid)
                        resp.append(cardid)
                    return resp
    else:
        if standard_poker[0] in Major:
            #print("major")
            deck_Major = [pok for pok in poker_deck if pok in Major]
            if len(deck_Major) < len(standard_poker):
                deck_nMajor = [pok for pok in poker_deck if pok not in Major]
                out = deck_Major
                for i in range(len(standard_poker) - len(deck_Major)):
                    out.append(deck_nMajor[i])
                attach_resp = []
                _deck = deck + []
                for pok in out:
                    cardid = Poker2Num(pok, _deck)
                    _deck.remove(cardid)
                    attach_resp.append(cardid)
                return attach_resp
            else:
                deck_parse = parsePoker(deck_Major, level)
                target_parse = parsePoker(standard_move, level)
                deck_parse.sort(key=lambda x: len(x), reverse=True)
                target_parse.sort(key=lambda x: len(x), reverse=True)
                out = []
                for target_unit in target_parse:
                    unit_len = len(target_unit)
                    for deck_unit in deck_parse:
                        if len(deck_unit) >= unit_len:
                            out.extend(deck_unit[:unit_len])
                            unit_len = 0
                            deck_unit = deck_unit[unit_len+1:]
                            deck_parse.sort(key=lambda x: len(x), reverse=True)
                        else:
                            out.extend(deck_unit)
                            unit_len -= len(deck_unit)
                            deck_unit = []
                            deck_parse.sort(key=lambda x: len(x), reverse=True)
                resp = []
                _deck = deck + []
                for pok in out:
                    cardid = Poker2Num(pok, _deck)
                    _deck.remove(cardid)
                    resp.append(cardid)
                return resp
        else:
            suit = standard_poker[0][0]
            deck_suit = [pok for pok in poker_deck if pok[0] == suit and pok[1] != level]
            if len(deck_suit) < len(standard_poker):
                deck_nsuit = [pok for pok in poker_deck if pok not in deck_suit]
                out = deck_suit
                for i in range(len(standard_poker) - len(deck_suit)):
                    out.append(deck_nsuit[i])
                attach_resp = []
                _deck = deck + []
                for pok in out:
                    cardid = Poker2Num(pok, _deck)
                    _deck.remove(cardid)
                    attach_resp.append(cardid)
                return attach_resp
            else:
                deck_parse = parsePoker(deck_suit, level)
                target_parse = parsePoker(standard_move, level)
                deck_parse.sort(key=lambda x: len(x), reverse=True)
                target_parse.sort(key=lambda x: len(x), reverse=True)
                out = []
                for target_unit in target_parse:
                    unit_len = len(target_unit)
                    for deck_unit in deck_parse:
                        if len(deck_unit) >= unit_len:
                            out.extend(deck_unit[:unit_len])
                            unit_len = 0
                            deck_unit = deck_unit[unit_len+1:]
                            deck_parse.sort(key=lambda x: len(x), reverse=True)
                        else:
                            out.extend(deck_unit)
                            unit_len -= len(deck_unit)
                            deck_unit = []
                            deck_parse.sort(key=lambda x: len(x), reverse=True)
                    resp = []
                    _deck = deck + []
                    for pok in out:
                        cardid = Poker2Num(pok, _deck)
                        _deck.remove(cardid)
                        resp.append(cardid)
                    return resp

_online = os.environ.get("USER", "") == "root"
if _online:
    full_input = json.loads(input())
else:
    with open("log_forAI.json") as fo:
        full_input = json.load(fo)
hold = []
for i in range(len(full_input["requests"])-1):
    req = full_input["requests"][i]
    if req["stage"] == "deal":
        hold.extend(req["deliver"])
    elif req["stage"] == "cover":
        hold.extend(req["deliver"])
        action_cover = full_input["responses"][i]
        for id in action_cover:
            hold.remove(id)
    elif req["stage"] == "play":
        history = req["history"]
        selfid = (history[3] + len(history[1])) % 4
        if len(history[0]) != 0:
            self_move = history[0][(selfid-history[2]) % 4]
            #print(hold)
            #print(self_move)
            for id in self_move:
                hold.remove(id)
curr_request = full_input["requests"][-1]
if curr_request["stage"] == "deal":
    get_card = curr_request["deliver"][0]
    called = curr_request["global"]["banking"]["called"]
    snatched = curr_request["global"]["banking"]["snatched"]
    level = curr_request["global"]["level"]
    response = call_Snatch(get_card, hold, called, snatched, level)
elif curr_request["stage"] == "cover":
    publiccard = curr_request["deliver"]
    response = cover_Pub(publiccard, hold)
elif curr_request["stage"] == "play":
    history = curr_request["history"]
    selfid = (history[3] + len(history[1])) % 4
    if len(history[0]) != 0:
        self_move = history[0][(selfid-history[2]) % 4]
        #print(hold)
        #print(self_move)
        for id in self_move:
            hold.remove(id)
    history_curr = history[1]
    level = curr_request["global"]["level"]
    setMajor(curr_request["global"]["banking"]["major"], level)
    response = playCard(history_curr, hold, level)

print(json.dumps({
    "response": response
}))