From 4763f9a6302409d6a8ccab190db9e43478d629b5 Mon Sep 17 00:00:00 2001 From: jianghaiying Date: Thu, 9 Oct 2025 15:27:17 +0800 Subject: [PATCH] shortdeck1.2 --- .../__pycache__/card.cpython-313.pyc | Bin 2984 -> 3338 bytes .../__pycache__/game_stage.cpython-313.pyc | Bin 4069 -> 4068 bytes .../hand_evaluator.cpython-313.pyc | Bin 0 -> 6958 bytes .../__pycache__/hand_ranking.cpython-313.pyc | Bin 0 -> 6281 bytes .../__pycache__/side_pot.cpython-313.pyc | Bin 0 -> 5360 bytes .../__pycache__/simulation.cpython-313.pyc | Bin 13667 -> 26149 bytes shortdeck_arena/card.py | 8 + shortdeck_arena/game_stage.py | 12 +- shortdeck_arena/hand_evaluator.py | 9 +- shortdeck_arena/hand_ranking.py | 28 +- shortdeck_arena/side_pot.py | 104 +++++ shortdeck_arena/simulation.py | 369 ++++++++++++++++-- .../__pycache__/arena_adapter.cpython-313.pyc | Bin 8238 -> 10382 bytes shortdeck_server/arena_adapter.py | 274 ++++++------- shortdeck_server/game_stage.py | 18 +- .../test_game.cpython-313-pytest-8.4.2.pyc | Bin 5641 -> 6639 bytes shortdeck_server/tests/test_game.py | 7 +- .../__pycache__/run_smoke.cpython-313.pyc | Bin 1868 -> 0 bytes shortdeck_server/tools/run_smoke.py | 36 -- 19 files changed, 615 insertions(+), 250 deletions(-) create mode 100644 shortdeck_arena/__pycache__/hand_evaluator.cpython-313.pyc create mode 100644 shortdeck_arena/__pycache__/hand_ranking.cpython-313.pyc create mode 100644 shortdeck_arena/__pycache__/side_pot.cpython-313.pyc create mode 100644 shortdeck_arena/side_pot.py delete mode 100644 shortdeck_server/tools/__pycache__/run_smoke.cpython-313.pyc delete mode 100644 shortdeck_server/tools/run_smoke.py diff --git a/shortdeck_arena/__pycache__/card.cpython-313.pyc b/shortdeck_arena/__pycache__/card.cpython-313.pyc index bb2f87c24939850cd9d04217c79113c35c14d695..0949e2695c660e5ef445caaeae8c575ef40956fd 100644 GIT binary patch delta 1288 zcmZux%}*0S6yMq1mVVHd7Av%BlX0}GS;K73* z5RDW44@@NSVmuh*!Gl*1BwoxMj0Y31XpLTdZ?+InCfVP<_h#P5{NCGr(efZ2twkas z1=gqW@6%!5tLR;lUM9kK%W|qV2s*Q!8;rn#jL4ngFUi~XW>7@ZG?{C;#k^^`E-g+~ zU7MLE-41&+iICunYBx#p_i8evf&mt(UFToaqaD|x6OC!5*E1?K26|+J-ZT-V@tZND ziSO1vj=K`se0EXbH3=LI=w_^1I^x(}ffp$lO@fT7YD$|-~<0IzhKK)snC(J!`P zb$Ptd+Co{w>e9TTwJl_oWim>mVEi|Lh?J_cHibJ&c0^*_D$V3J+4g`NYXrEXe2q0f zn!i845=coxHl>7gUdmPwo+?+@%eky9^wyQELi=FXAX{EW_}Sd-RM`<>S%jzTh4c07 z-(rc!haS$Y1X{gzU}Hv&1Wm)6wju05*a;xi@jiI^;W;c+{zh*Zlr0WaXt`ojcaHYs z(E$X!-^M1Rnq31VnN9NFdUx+nUGtqIt0Bc7{h<j3VH&GN*U)GjQ=Uf{!JJ5ECXNmWr-vipXh; z7IJGU&Pbh{&)ZrTg8)lGn&boi+s7r*nn(0{*#FuOk*Qsw8zOt^LH@(v1*5*J;lzP@ z%PwMV|IHs5MPt1v=0czzzSPh{lCPgPJR=LzHR91Zi2lbAh7gV;oIp5qzM2A4566tm^EO@X&r)joii2uK#mD5`n?la-r(jL(~ElKPJ}0 aXTp0@+ezwP|1Skzf4bu&`BMRqet!Y;KM%hE delta 843 zcmZXS&ubGw6vy+DO|nTgsVRwR(^S$#*sP7Q6l<$33$cn;L$Llh6p>}kt~Mn7!R$uV ziyl0P)Qbap5Ih?YK|Fc#A1HX05j=X=DqekWLdD>~es zWw9*ou?!*2cY?E!;U9z9ILUFrp2@!k7u72oOWjTyDP9llHgwz&a8?ARZn#pR0@xJc zpLEGA?}H+|IX)AaP5x3s;T0IvROT-t-3UdsLVOjAn~WB#eQ5TBLSgo12fNj4`$mWz zpdT$0&d{Gih+?sbJ?TUXxx?hr5Dx>NGh;h3#M2nd@K@2hr8>G%FU$L@A1ahDs0FC- zYibqGZ@@Tzsp`k14A^;84+WDg$E7XX%&F1Lm}O1#?`jS*?_;s&a862-gVK*v{2bu~ zVVy)H^!-)SkHd3A?$O>geiB{M4ybaf1-*VVx+8df^8gC#u&3%;l zj8==ttW54`=24lB+#bFKLS5R}a(Z3YWNRn}eDErK(n=F4Sb@j$KLhmsT}ngtM*tzO F#vdlLr5*qP diff --git a/shortdeck_arena/__pycache__/game_stage.cpython-313.pyc b/shortdeck_arena/__pycache__/game_stage.cpython-313.pyc index 8653e8f2a73c73f807cc7c477e93f709e82b49e1..75546cdc56a6b3a046f315ab6676b380f8b25ee9 100644 GIT binary patch delta 2032 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 B{oMcn diff --git a/shortdeck_arena/__pycache__/hand_evaluator.cpython-313.pyc b/shortdeck_arena/__pycache__/hand_evaluator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b61c539299069d1c31305180c6b3d2ec9947d1c GIT binary patch literal 6958 zcmcf`ZBScP_P*rhB_uBd2w&kVv_RVw3xxu0sii|9g+hfkk4LefF%7RYk|uax;#Zv= zch+&!)!FH;I#Rd0rMtRY$8p46XT<96)|nlp<5wO*!6%N+tfj**Q5<*rZ_j-%A)#ih zcAdR5IrqGK&$;*9bI-l^oRbx^*@&R9y;qO_J`bUPlaEB9ADSF891ZMh>cw)Y*N~oZnn#e&0U!|vm6zo z^@!6KAvP5A3(ZX-#TL4d?!PPz< zHO6s{YQ7I4PXR)MgiHy7CM?ud6t<6s&Gz?pQTQx#F9iY;21vIxcBVl37*MGhJ${Q0`Pd)y{)QgL!{&D4(7Z=YyeC4^nUw!70Mz!PW>yKPH9WgGE z-F-0Mc|UA=NEj~PUhV}W5pO9U35LphmdJV?dRgli0*E9>nY5e~c}IKm(0xHX zDvo))!jO18h(p7IcQlE)Jy-~MhK_@UxZ{EZan0BRGMf;K7>``E#)N88`^Zj-4+D4@ zePcwdB@%wDc|7~EJqtM{QG4Sn-l?j;-90%JE!`W-X^XOLpBEHI*!etL#P|(^iZ@P# zFL*vG+8XIv$gh~oub1-cUul>!# z;*tT0nVp&dlRUhG#2%Hx(}{zXs5yOeCmd1zAV{qimo*T=qC{J=PU$U4wkEKY-m*kn zvaZ!B5^jd8a9yHxaD4)ERVJ`xoibjU_11#zG{!I0%JgGyNRE}j_>8rDJi{3{eXT~3 z?IF;f&gDqZChLR^fUO$)O`-3O3b722zN2y6ON?nR3lV=#tNZJv$es_fL+GQ=`*h3Uqt>=;d!7hK`eyq6#Z|(%# z$U@coGb^n|>eFZ>6HaQ@7U)=Or=S;eQZGM&+&8i)RqpJYw%8l#MZFo)pWKEtddcR^ zwNzTYWG3};)6(8Z8>+OHpFr<7(#!YROH&%XSkl=`T5H?!UG9OD6D(@V!5ax^jsaYQ zDK~k%_5XxBEIpV!`zaoCW2(oz#(yJ02k|mi$QAVz;*ksL>x(NM-=(&LBTw}}a7Gi4 zST+a3Cj{*Cx{0GLKyXKSMKW|bCeONTxYh4Dadg+y>%fT;9U zz%7D@2cGX_-K$M6H_dJ9nAz5Wi965-vuj6%2V^}K?iH{oJWpZrY+>TnEztxcaxakw ze~DlYEhc9`=~n4%i2C?4`zV^gCl@s zldJdk)&uT+2Y9Yi&gA-Bt@}GV`{Lg^Ur0D1%9;Cmd6&C)pS#r!&SMCvW_fVluhKz?Ij_-NaA^@RSOVhgHLoH^M}RbvI#ITt_Rtiz}&|{FRB)G3ZtBH zPuMf8L|`&A>hq2YST=^RZ%kC%GR8bUtfG3g8^j=OqL>3(Q-ZNIKruZGaWs-kzR&82 zg^DF$FA!ZZH;%4=rm$>WkQ^P+L%dYbA7MYeDw4Hw_qKjrVq-_M= z0^ofj5HT$jl#Y+im2H*Ew#Lfpr`n~0)(8vzxdl&(f6#Tg zYj$90ZonfAcxJsLbKX(OI~r~B$F}$v3f4cnW3FPmRI&Yy%71QpZPQfIrJ-1XE1Ko{ zLhWjnDw?P6nChK|<|EOpBcGdXb^+e^g zE_%py*?2Bjs%xJq7kK6|}nub(^;ZCIUs(VVAro%`9DpIr#Y ztR%!Ok5m0ktzVgc=VbLIy;R>ZT@thQN7;UIgzUtM z>Lh#JWMjcQu6>es@Y;$274d);a@SUJRkU<~w3ZxR zB~WNf2gF~%E%9pJq22VGIe9J({Z=OH(lc+BF$A`-E;I9XCIfIfhjm$*>Gcf2%No{Y zV=m_~0N*KMU3TW3%?!YIEv&1`_^zD+_+B;Zsy4p2ivjq4G3%;h-mhi=eo)W4wlh%; zL*Pc%wc8lo&H%*wK#nypIC*`@6Sr~~Jh*E!Fc5oEYw{%7>0a_=#sfSUilaCUniG8jd03u10i_kKn0*t{igPJS2H5m6T%wAB3Eg zT9Pu?LC$(qjgjEl;HW|JGAqSJUf}Br8F9oA_vxr($dM5ye&phx9$q~6+~W94S5E)= zi>ZrC=PzD)@${E(JihqkON)>H{OY+EA**@y)N6}pPeC%XbxV^<(1%IlQ}HYI^(ekc z3)#FS0x)sHFvkm6ck;`KWi#X6Kn z71t43D?Ghe7zz75IQaxqKA9ztZzA6*lKIt_Q++bG5mwvUCo{#D8G>%{9RSLEnN#?h zE>_rbQ8!7?*{Wx4)vxKK!j@@T%DyeaEHlVn0eQoP?EKUBoUIr?_PgqjvNuFp=Ph~T z)Nj0}PLB7Vxm~hsxUlX+OZEJQ>S$KQ$60w_=~2$cZz0iX|Gd~9Iq*qg>1lf2S}<=L*jho^FY<>y|T7R`Ip3cOlyzC7=7NyuHE>hArK_pVhH}s@SYGt$AiPFKW7IuDJ}>mM^xP^@HD`4 zJQ@u8v7+fbH|z~zbbCVJ?;H(>1kvrrZo)wPTy;}O;!yD`iJvO^06dE3b=k|B99_xy z$rS|EiWV_BO2hId1>2?Tq$VI~OX_$hL6;w-ly1AOc)VePKGVF60KHH(Nly+>8l}y< zR>-%)u%ZD>m4XP+*yEqD}I;j%;G1^~-iilRP4l@hA_6xlyT X>pw&5B?4>z!-OcR_-jOcK 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 literal 0 HcmV?d00001 diff --git a/shortdeck_arena/__pycache__/side_pot.cpython-313.pyc b/shortdeck_arena/__pycache__/side_pot.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6db3e822c004c18aa4686235555a72f7df5b921b GIT binary patch literal 5360 zcmb7I|8E;b7N7lMd;KkTW2cGJq;(SKi&NsHO~0mXBuKAmQxa9#8s)BqWovI8Q)iu< z-Lw>h6u|+8JD}W=(t-|BzKE2QO1VSa(ZZeJ4>-l7C1wRu(KT%&-4AI~zVv=L-J9LD z>!bm6Bm3>_&dhr=Z{B;KH#6^cI}ntc_go#E_8{~x@{%tWqe9IefXW!+5l`(#Jrt&b zq)zXqdl=00u$b+!V2l2Z*=_ByVOtRGw^g88#Iu!%x2&fPKI@GK!kpe_g*IEh4cil3 z*gi{ogelD_%5qv2)nr;$!i>h=n^aVd;U!gLw(UteduZ*3&^4yuL=l^i!wclt=QdTvqIFufd)kip(=RWvwzvh)v z$wcx%N)m=s;!z1JJ2dt{I-Sy-NkteEld>?JR$~ zv&AC_2a{M)Q%PBp)6i@agm^jz6=*548bQEd%?K|CqLm4PLaZq!h^mT{2S!v$5roqS zSCB4cgxK?hY_qZo$kXT>wkqfHJh3~seB~6oGFMSOy8D=aimm>Zr7g|ISTbyZ0h*>x z&EE?x3k}dPKQJx}?<9`#5DRAK;w^g3#=CiN2>8bFCA>|q*?A6nJ9s;GCLCdpR$?l; zSCqwsglByqVuY;Hu*j=tPF+3r)Xd3=r7bF`PpB1a>b8Bfk+*{VB@kW)ox-%n!cHq% zxnZ?}3a*%f-99WS>X0O>m z(mO$i8NDFirQssFZe2vKCmZsd(D;* z%sQ{HgZa8srVpa9b>tpk>6pp9I&=P<85eYna?HGb?((_kO`{$?Z`k$h(=X1x@kgWg z%v-O_y#Kf{#-SVh9f1TKpPDDOPHc*&h(UxPUi8h9O~64Q0L|3H3u438lsI%CE_P~b z7I{DnOQI?Xiis>rWNRuN6H`hjRE!~a0Yin@@+dk{bK>q-j9>vGm_yRhK5#=a6&*hG zU?1jm_tdNkpqLcbEJ;-wQZ&0Hj|@o|WNA!FlJ!szU_Cr6$#Gmy=Ik;2xkQYq$-@P( z)|g?_v$ax#W05~A`bf<&jHSauN;)j1G%AUkft%*I9VxX^y-A3rA!-ubfrN%63x^Fn zc0iaFjI2T93VI+B9l}U)F+z%WvBtDjKcmthuu_^}b4Jme19|<^p4ITn`N}f8Zd%dG zP%g4|I?|Jk^h`(gWFvbnMEY+c%Dv(9NdL)4PbfLBFAFHmppq52=C-qIPB-iIOzgM! zXCwVLNY8J)E3ezwm5xcS@;evu*6OS;d6s3^T-oxB<8pKR`jY&(*L0swOnP_szhBc#6!as`MBK-ItZU z?0nIAV&?^KeXhFk_m1l=X!TBNt__t10E0ZfjPus~E(%t1YrYl)DAk~`51n1jj!!Kt zp>7+{&q|zKZQN(oR-g-fq5xGLFl-F%BW|DVDc~;vjkBuq^%^1B+y3Y zAZ36&r8^H`JsoKSO9#9s%=74F-EU7AskJV$v4o_W839QLZ~JV10GWmY0?{m3 z-DGL|yMgh#mphJkyc@Ws5PdyfQAvSVMj08>edvyC!o5I(R`pZz_Yh%#HX!P43ZYc(|-ZseI3y>P6)=|W09^#^wJ55BklCMSbV8dmQ z7G(*8sI_Cj1T6qEU8=d^vJIp%{bbFHN)bHS@`l&~79ex~M}Oce+lyeYzzH7zhI>d}{W|=VsnM4*ujA6y>;pMfp%tPADD0M++HTM^JB4R5>?f3FY=H$oR*DKj*y?^5`HSdP=(~>Kj@CSs{7pF zS!LWk5&I&z_S~T1t51}cQFd<)xevfu0uE({wly2+4CiCbmmehzOIV-1gf%;s$YD7yVZ4p7wwk>O`UfAp zjgtmS`j%L}R{QfR+6&y2{{r~|z|w{M%QM_1U-`?MU)r4My42V_-MHoR#x3)Rs%V4f zIFqZa9;-SAFTN09>jLURc%r(csjo?y0KoOz)Qs5g1+G z`Nkk1YoK9V$p%(WJeCb?1r8M}P7P+dfkAmE0AgLkJI$w>KVZ&w|HYN9-8$)7m8)3s zy5pqdEq&HdSmHJM$Ps~g6Td{N!{qWu_^^981S;ap?Wv7K3eDCe!1@&+eE8?)Yx zP`Tvuk8R8OtET*+2`cLkU-XCO-GHLk>yW>CZY}awWIVTSw8GSH-SC2Z-6T#qxo!jDF$`i%R~;00}PYCxEc1{PdUvV22Ktvrj*F z^^-G;PQD~3BpID%iKQeF>*?Sf_+r5l+=FQTMJAztJpzN3S|D%^<6JM;pRvE-dB$^r z3lvUX(_C|wYre?cr9%tctdJ9-F0?Pq>Ionw=K#YsNDaPjhQ}jza*1W!3t6$Gj$ru_ zxBkhT$Nj+TK{C=DM7-!fCFHt`47w{wZj_7;BD8?7!LbQpa&W{e2&p23q%WAnSiG8$ zE<)}l)0UGA2NU2@q+{!quImU~;N4 zE?@LH1j%dRRz~Bt8rMLbm_#c8Y^0DtGsjRA^);%!ifXf{_G?r#XGOI0dzzz~=MWG` VQ)%ZMdHsHQf}&b(AVPJ%{{!<$<=g-O literal 0 HcmV?d00001 diff --git a/shortdeck_arena/__pycache__/simulation.cpython-313.pyc b/shortdeck_arena/__pycache__/simulation.cpython-313.pyc index b0ea0ebd89588da097e573c6fb3122cea4b1ee76..1747fda18748d0643a102e5d6b6c5cdb20a52d65 100644 GIT binary patch literal 26149 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⪼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 delta 4755 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 SJ6hrT$h?tfeeW}fl>h%l3)ijy diff --git a/shortdeck_arena/card.py b/shortdeck_arena/card.py index 3da1e94..eb4ba5c 100644 --- a/shortdeck_arena/card.py +++ b/shortdeck_arena/card.py @@ -30,6 +30,14 @@ class Rank(IntEnum): if self.value <= 9: return str(self.value) return {10: "T", 11: "J", 12: "Q", 13: "K", 14: "A"}[self.value] + + @property + def numeric_value(self): + return self.value + + @property + def symbol(self): + return str(self) class Card: diff --git a/shortdeck_arena/game_stage.py b/shortdeck_arena/game_stage.py index 8406d57..b99b8e5 100644 --- a/shortdeck_arena/game_stage.py +++ b/shortdeck_arena/game_stage.py @@ -41,8 +41,8 @@ class PlayerState(Enum): ALLIN = "allin" # 全下 CALLED = "called" # 已跟注 RAISE = "raised" # 已加注 - WAITING = "waiting" # 等待其他玩家 - OUT = "out" # 出局 + # WAITING = "waiting" # 等待其他玩家 + # OUT = "out" # 出局 def __str__(self) -> str: return self.value @@ -74,7 +74,7 @@ class BlindConfig: return (dealer_position + 1) % 2 # heads-up: 大盲 = 庄位 return (dealer_position + 2) % num_players # 多人: 大盲为庄位后两位 - def get_first_to_act(self, stage: GameStage, num_players) -> int: + def get_first_to_act(self, stage: GameStage, num_players, dealer_position=0) -> int: """ 获取首个行动玩家位置 区分preflop和postflop @@ -83,9 +83,9 @@ class BlindConfig: """ if stage == GameStage.PREFLOP: if num_players == 2: - return self.get_sb_position(num_players) # heads-up: 小盲先行动 + return self.get_sb_position(num_players, dealer_position) # heads-up: 小盲先行动 else: # preflop: 大盲后第一位 - return (self.get_bb_position(num_players) + 1) % num_players + return (self.get_bb_position(num_players, dealer_position) + 1) % num_players else: # flop/river/turn: 小盲位先行动 - return self.get_sb_position(num_players) \ No newline at end of file + return self.get_sb_position(num_players, dealer_position) \ No newline at end of file diff --git a/shortdeck_arena/hand_evaluator.py b/shortdeck_arena/hand_evaluator.py index 455f884..5e66846 100644 --- a/shortdeck_arena/hand_evaluator.py +++ b/shortdeck_arena/hand_evaluator.py @@ -91,9 +91,8 @@ class HandEvaluator: @staticmethod def _isStraight(ranks: List[Rank]) -> Tuple[bool, Rank]: - values = sorted([rank.numeric_value for rank in ranks], reverse=True) - + is_regular_straight = True for i in range(1, len(values)): if values[i-1] - values[i] != 1: @@ -101,7 +100,6 @@ class HandEvaluator: break if is_regular_straight: - # 返回最高牌 highest_rank = None for rank in ranks: if rank.numeric_value == values[0]: @@ -109,7 +107,6 @@ class HandEvaluator: break return True, highest_rank - if values == [14, 5, 4, 3, 2]: # A, 5, 4, 3, 2 - return True, Rank.FIVE - + if values == {14, 9, 8, 7, 6}: # A, T, 9, 8, 7, 6 + return True, Rank.R9 return False, None \ No newline at end of file diff --git a/shortdeck_arena/hand_ranking.py b/shortdeck_arena/hand_ranking.py index 9a29768..c6cd0d5 100644 --- a/shortdeck_arena/hand_ranking.py +++ b/shortdeck_arena/hand_ranking.py @@ -4,13 +4,14 @@ from .card import Card, Rank class HandType(Enum): + # 短牌规则:同花 > 葫芦 HIGH_CARD = (1, "High Card") ONE_PAIR = (2, "Pair") TWO_PAIR = (3, "Two Pair") THREE_OF_A_KIND = (4, "Three of a Kind") STRAIGHT = (5, "Straight") - FLUSH = (6, "Flush") - FULL_HOUSE = (7, "Full House") + FULL_HOUSE = (6, "Full House") + FLUSH = (7, "Flush") FOUR_OF_A_KIND = (8, "Four of a Kind") STRAIGHT_FLUSH = (9, "Straight Flush") ROYAL_FLUSH = (10, "Royal Flush") @@ -55,3 +56,26 @@ class HandRanking: return f"Pair({self.key_ranks[0].symbol})" else: return f"High Card({self.key_ranks[0].symbol})" + + def __lt__(self, other): + """比较牌力,用于排序""" + if not isinstance(other, HandRanking): + return NotImplemented + + if self.hand_type.strength != other.hand_type.strength: + return self.hand_type.strength < other.hand_type.strength + + for my_rank, other_rank in zip(self.key_ranks, other.key_ranks): + if my_rank.numeric_value != other_rank.numeric_value: + return my_rank.numeric_value < other_rank.numeric_value + + return False + def get_strength(self) -> int: + # 返回牌力 还是 牌型+点数 + # 基础强度 = 牌型强度 * 1000000 + strength = self.hand_type.strength * 1000000 + + for i, rank in enumerate(self.key_ranks): + strength += rank.numeric_value * (100 ** (4 - i)) + + return strength diff --git a/shortdeck_arena/side_pot.py b/shortdeck_arena/side_pot.py new file mode 100644 index 0000000..bad36a6 --- /dev/null +++ b/shortdeck_arena/side_pot.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from typing import List, Set, Dict +from dataclasses import dataclass + + +@dataclass +class SidePot: + amount: int + eligible_players: Set[int] + is_main_pot: bool = False + + def __post_init__(self): + if not self.eligible_players: + self.eligible_players = set() + + +class SidePotManager: + def __init__(self): + self.pots: List[SidePot] = [] + self.player_total_investment: Dict[int, int] = {} + + def add_investment(self, player_id: int, amount: int): + if player_id not in self.player_total_investment: + self.player_total_investment[player_id] = 0 + self.player_total_investment[player_id] += amount + + def create_side_pots(self, active_players: List[int]) -> List[SidePot]: + if not self.player_total_investment: + return [] + + # 按投入金额排序 + sorted_investments = sorted( + [(pid, amount) for pid, amount in self.player_total_investment.items() + if pid in active_players and amount > 0], + key=lambda x: x[1] + ) + + if not sorted_investments: + return [] + + pots = [] + prev_level = 0 + + for i, (player_id, investment) in enumerate(sorted_investments): + if investment > prev_level: + # 计算本层级的贡献 + level_contribution = investment - prev_level + + # 找到有资格竞争本层级的玩家 + eligible_players = { + pid for pid, inv in sorted_investments[i:] + if inv >= investment + } + + pot_amount = level_contribution * len(eligible_players) + + side_pot = SidePot( + amount=pot_amount, + eligible_players=eligible_players, + is_main_pot=(len(pots) == 0) + ) + + pots.append(side_pot) + prev_level = investment + + self.pots = pots + return pots + + def get_total_pot(self) -> int: + return sum(pot.amount for pot in self.pots) + + def distribute_winnings(self, hand_rankings: Dict[int, int]) -> Dict[int, int]: + winnings = {} + + for pot in self.pots: + + eligible = [pid for pid in pot.eligible_players if pid in hand_rankings] + if not eligible: + continue + + # 找到最强手牌 + best_strength = max(hand_rankings[pid] for pid in eligible) + winners = [pid for pid in eligible + if hand_rankings[pid] == best_strength] + + # 平分底池 + winnings_per_winner = pot.amount // len(winners) + remainder = pot.amount % len(winners) + + for i, winner in enumerate(winners): + if winner not in winnings: + winnings[winner] = 0 + winnings[winner] += winnings_per_winner + # 余数给前面的获胜者 + if i < remainder: + winnings[winner] += 1 + + return winnings + + def reset(self): + """重置边池管理器""" + self.pots.clear() + self.player_total_investment.clear() \ No newline at end of file diff --git a/shortdeck_arena/simulation.py b/shortdeck_arena/simulation.py index 8b6a390..ca48dc7 100644 --- a/shortdeck_arena/simulation.py +++ b/shortdeck_arena/simulation.py @@ -10,6 +10,9 @@ if TYPE_CHECKING: from .agent import Agent from .card import Card from .game_stage import GameStage, PlayerState, BlindConfig +from .side_pot import SidePotManager +from .hand_evaluator import HandEvaluator +from .hand_ranking import HandRanking class Simulation: @@ -35,6 +38,10 @@ class Simulation: self.min_raise = self.blind_config.big_blind self.dealer_position = -1 + # 边池管理和筹码 + self.side_pot_manager = SidePotManager() + self.stacks: List[int] = [1000] * len(agents) # 默认筹码 + self.new_round() def new_round(self): @@ -54,6 +61,9 @@ class Simulation: self.last_raise_amount = 0 self.min_raise = self.blind_config.big_blind + # 重置边池管理器 + self.side_pot_manager.reset() + # 设置盲注 self._setup_blinds() @@ -61,7 +71,6 @@ class Simulation: self.dealer_position = random.choice(range(len(self.agents))) def _setup_blinds(self): - """设置盲注""" num_players = len(self.agents) # 至少需要2个玩家才能设置盲注 @@ -78,25 +87,31 @@ class Simulation: return # 扣除小盲 - self.pot[sb_pos] = self.blind_config.small_blind - self.total_pot += self.blind_config.small_blind + sb_amount = min(self.blind_config.small_blind, self.stacks[sb_pos]) + self.pot[sb_pos] = sb_amount + self.stacks[sb_pos] -= sb_amount + self.total_pot += sb_amount + self.side_pot_manager.add_investment(sb_pos, sb_amount) self.history.append({ "pid": sb_pos, "action": "small_blind", - "amount": self.blind_config.small_blind + "amount": sb_amount }) # 扣除大盲 - self.pot[bb_pos] = self.blind_config.big_blind - self.total_pot += self.blind_config.big_blind + bb_amount = min(self.blind_config.big_blind, self.stacks[bb_pos]) + self.pot[bb_pos] = bb_amount + self.stacks[bb_pos] -= bb_amount + self.total_pot += bb_amount + self.side_pot_manager.add_investment(bb_pos, bb_amount) self.history.append({ "pid": bb_pos, "action": "big_blind", - "amount": self.blind_config.big_blind + "amount": bb_amount }) # 首个行动玩家 - self.current_turn = self.blind_config.get_first_to_act(self.current_stage, num_players) + self.current_turn = self.blind_config.get_first_to_act(self.current_stage, num_players, self.dealer_position) self.last_raise_amount = self.blind_config.big_blind def player_cards(self, pid) -> List[Card]: @@ -114,19 +129,113 @@ class Simulation: return [] def get_current_max_bet(self) -> int: - """ - 获取当前最高下注额, 用于计算跟注金额 - """ return max(self.pot) if self.pot else 0 def get_call_amount(self, pid) -> int: """ - 计算玩家跟注所需金额 + 跟注金额 """ if pid >= len(self.pot): return 0 max_pot = self.get_current_max_bet() return max(0, max_pot - self.pot[pid]) + + def get_min_raise_amount(self, pid) -> int: + """最小加注金额""" + call_amount = self.get_call_amount(pid) + min_raise = call_amount + max(self.last_raise_amount, self.blind_config.big_blind) + return min_raise + + def get_max_bet_amount(self, pid) -> int: + """最大下注金额(剩余筹码)""" + if pid >= len(self.stacks): + return 0 + return self.stacks[pid] + + def is_all_in_amount(self, pid, amount) -> bool: + """检查是否为allin""" + return amount >= self.stacks[pid] + + def validate_bet_amount(self, pid, action, amount) -> tuple[bool, str, int]: + """ + 验证下注金额合法性 + """ + if pid >= len(self.stacks): + return False, "无效玩家", amount + + available_stack = self.stacks[pid] + call_amount = self.get_call_amount(pid) + + if action == "fold": + return True, "", 0 + + elif action == "check": + if call_amount > 0: + return False, "不能过牌,需跟注或弃牌", 0 + return True, "", 0 + + elif action == "call": + if call_amount == 0: + return False, "不需要跟注", 0 + + # All-in call + if call_amount >= available_stack: + return True, "", available_stack + + return True, "", call_amount + + elif action in ["bet", "raise"]: + if amount <= 0: + return False, "无效下注金额", amount + + # allin + if amount >= available_stack: + return True, "", available_stack + + + if action == "raise": + min_raise = self.get_min_raise_amount(pid) + if amount < min_raise: + return False, f"最小加注金额为 {min_raise}", amount + + if action == "bet" and max(self.pot) == 0: + if amount < self.blind_config.big_blind: + return False, f"最小下注金额为 {self.blind_config.big_blind}", amount + + return True, "", amount + + return False, "无效行为", amount + + def get_available_actions(self, pid: int) -> dict: + if pid != self.current_turn: + return {"can_act": False, "reason": "不是你的回合"} + + if pid >= len(self.player_states): + return {"can_act": False, "reason": "无效玩家"} + + state = self.player_states[pid] + if state in [PlayerState.FOLDED, PlayerState.ALLIN, PlayerState.OUT]: + return {"can_act": False, "reason": f"Player state: {state}"} + + call_amount = self.get_call_amount(pid) + available_stack = self.stacks[pid] + + actions = { + "can_act": True, + "can_fold": True, + "can_check": call_amount == 0, + "can_call": call_amount > 0 and call_amount < available_stack, + "can_bet": max(self.pot) == 0 and available_stack > 0, + "can_raise": call_amount > 0 and available_stack > call_amount, + "can_allin": available_stack > 0, + "call_amount": call_amount, + "min_bet": self.blind_config.big_blind if max(self.pot) == 0 else 0, + "min_raise": self.get_min_raise_amount(pid) if call_amount > 0 else 0, + "max_bet": available_stack, + "stack": available_stack + } + + return actions def is_betting_round_complete(self) -> bool: """ @@ -138,37 +247,59 @@ class Simulation: if len(active_players) <= 1: return True - # 检查所有active玩家是否都已投入相同金额 + # 检查所有active玩家是否都已投入相同金额,且所有人都已经行动过 max_pot = self.get_current_max_bet() - for i in active_players: # allin玩家不要求跟注 - if self.pot[i] < max_pot and self.player_states[i] != PlayerState.ALLIN: - return False - return True + + # 统计还需要行动的玩家 + players_need_action = [] + for i in active_players: + # allin + if self.player_states[i] == PlayerState.ALLIN: + continue + # 投入金额不足的玩家需要行动 + if self.pot[i] < max_pot: + players_need_action.append(i) + # Active状态的玩家如果还没有在本轮行动过,也需要行动 + elif self.player_states[i] == PlayerState.ACTIVE: + # 在翻前,大盲玩家即使投入了足够金额,也有权行动一次 + if (self.current_stage == GameStage.PREFLOP and + i == self.blind_config.get_bb_position(len(self.agents), self.dealer_position)): + # 检查大盲是否已经行动过(除了盲注) + bb_actions = [h for h in self.history if h.get('pid') == i and h.get('action') not in ['big_blind']] + if not bb_actions: + players_need_action.append(i) + + return len(players_need_action) == 0 def advance_to_next_street(self): if self.current_stage == GameStage.FINISHED: return - + next_stage = GameStage.get_next_stage(self.current_stage) if next_stage is None: self.current_stage = GameStage.FINISHED + self.complete_hand() return self.current_stage = next_stage + active_players = self.get_active_players() + if len(active_players) <= 1: + self.current_stage = GameStage.FINISHED + self.complete_hand() + return + # 重置下注轮状态 self.betting_round_complete = False - #### 重置pot为累计投入 (不清零,为了计算边池) - # 重置行动状态 for i, state in enumerate(self.player_states): if state == PlayerState.CALLED: self.player_states[i] = PlayerState.ACTIVE - # 设置首个行动玩家 + # 首个行动玩家 num_players = len(self.agents) - self.current_turn = self.blind_config.get_first_to_act(self.current_stage, num_players) + self.current_turn = self.blind_config.get_first_to_act(self.current_stage, num_players, self.dealer_position) self.last_raise_amount = 0 self.min_raise = self.blind_config.big_blind @@ -179,21 +310,25 @@ class Simulation: return pos return None + def get_side_pots(self) -> List: + active_players = [ + i for i, state in enumerate(self.player_states) + if state not in [PlayerState.FOLDED, PlayerState.OUT] + ] + return self.side_pot_manager.create_side_pots(active_players) + def node_info(self) -> Dict: if self.current_turn >= len(self.pot): return {"bet_min": self.min_raise, "bet_max": 0, "call_amount": 0} - # 跟注 - call_amount = self.get_call_amount(self.current_turn) + + actions = self.get_available_actions(self.current_turn) return { - "bet_min": max(self.min_raise, call_amount + self.min_raise), - "bet_max": 100, ########## 需要从ArenaGame获取实际大小 - "call_amount": call_amount + "bet_min": actions.get("min_bet", self.min_raise), + "bet_max": actions.get("max_bet", 100), + "call_amount": actions.get("call_amount", 0) } def apply_action(self, pid, action, amount): - """ - 应用玩家动作 - """ if pid != self.current_turn: raise ValueError(f"不是玩家 {pid} 的回合") @@ -201,6 +336,14 @@ class Simulation: raise ValueError(f"玩家 {pid} 无法行动,当前状态: {self.player_states[pid]}") action = action.lower() + + # 验证动作合法性 + is_valid, error_msg, adjusted_amount = self.validate_bet_amount(pid, action, amount or 0) + if not is_valid: + raise ValueError(error_msg) + + # 使用调整后的金额 + amount = adjusted_amount self.history.append({"pid": pid, "action": action, "amount": amount}) @@ -212,10 +355,19 @@ class Simulation: if call_amount == 0: # check self.history[-1]["action"] = "check" + self.player_states[pid] = PlayerState.CALLED else: - self.pot[pid] += call_amount - self.total_pot += call_amount - self.player_states[pid] = PlayerState.CALLED + # 检查是否all-in + actual_amount = min(call_amount, self.stacks[pid]) + if actual_amount >= self.stacks[pid]: + self.player_states[pid] = PlayerState.ALLIN + else: + self.player_states[pid] = PlayerState.CALLED + + self.pot[pid] += actual_amount + self.stacks[pid] -= actual_amount + self.total_pot += actual_amount + self.side_pot_manager.add_investment(pid, actual_amount) elif action == "check": call_amount = self.get_call_amount(pid) @@ -226,19 +378,25 @@ class Simulation: elif action in ("bet", "raise"): if amount is None: raise ValueError(f"{action} 需要指定金额") - # 加注需要补齐跟注金额 - call_amount = self.get_call_amount(pid) - total_amount = call_amount + (amount or 0) - self.pot[pid] += total_amount - self.total_pot += total_amount + # 检查是否all-in + actual_amount = min(amount, self.stacks[pid]) + if actual_amount >= self.stacks[pid]: + self.player_states[pid] = PlayerState.ALLIN + else: + self.player_states[pid] = PlayerState.CALLED - if amount and amount > 0: - self.last_raise_amount = amount - self.min_raise = amount - self.player_states[pid] = PlayerState.CALLED + self.pot[pid] += actual_amount + self.stacks[pid] -= actual_amount + self.total_pot += actual_amount + self.side_pot_manager.add_investment(pid, actual_amount) - # 其他跟注的玩家 + # 更新最后加注金额 + call_amount = self.get_call_amount(pid) + raise_amount = actual_amount - call_amount + if raise_amount > 0: + self.last_raise_amount = raise_amount + self.min_raise = raise_amount for i, state in enumerate(self.player_states): if i != pid and state == PlayerState.CALLED: self.player_states[i] = PlayerState.ACTIVE @@ -285,3 +443,128 @@ class Simulation: f.write(json.dumps(self.to_save_data())) f.write("\n") self.saved = True + + + def evaluate_player_hand(self, pid: int) -> Optional[HandRanking]: + """评估玩家手牌强度""" + if pid >= len(self.agents): + return None + + if self.player_states[pid] == PlayerState.FOLDED: + return None + + try: + # 获取玩家手牌 + player_cards = self.player_cards(pid) + + # 获取公共牌 + board_cards = self.board_cards(self.current_stage.value) + + # 至少需要5张牌才能评估 + all_cards = player_cards + board_cards + if len(all_cards) < 5: + return None + + # 如果正好5张牌,直接评估 + if len(all_cards) == 5: + return HandEvaluator.evaluate5Cards(all_cards) + + # 如果超过5张牌,找最佳组合 + return HandEvaluator.evaluateHand(all_cards) + + except Exception as e: + print(f"评估玩家 {pid} 手牌时出错: {e}") + return None + + def get_active_players(self) -> List[int]: + return [i for i, state in enumerate(self.player_states) + if state not in [PlayerState.FOLDED, PlayerState.OUT]] + + def is_hand_complete(self) -> bool: + active_players = self.get_active_players() + + if len(active_players) <= 1: + return True + + # 到达河牌且所有下注完成 + if (self.current_stage == GameStage.FINISHED or + (self.current_stage == GameStage.RIVER and self.betting_round_complete)): + return True + + return False + + def determine_winners(self) -> Dict[int, HandRanking]: + active_players = self.get_active_players() + + if not active_players: + return {} + + if len(active_players) == 1: + return {active_players[0]: None} # 不需要摊牌 + + # 多人摊牌 + hand_rankings = {} + for pid in active_players: + ranking = self.evaluate_player_hand(pid) + if ranking is not None: + hand_rankings[pid] = ranking + + return hand_rankings + + def distribute_pot(self) -> Dict[int, int]: + winners = self.determine_winners() + + if not winners: + return {} + + # 只有一人获胜(其他人弃牌) + if len(winners) == 1 and list(winners.values())[0] is None: + winner_id = list(winners.keys())[0] + return {winner_id: self.total_pot} + + # 多人摊牌,使用边池分配 + if len(winners) > 1: + #转换HandRanking为数值强度 + hand_strengths = {} + for pid, ranking in winners.items(): + if ranking is not None: + hand_strengths[pid] = ranking.get_strength() + else: + hand_strengths[pid] = 0 # 弃牌玩家 + + return self.side_pot_manager.distribute_winnings(hand_strengths) + + return {} + + def complete_hand(self) -> Dict: + if not self.is_hand_complete(): + return {"complete": False, "message": "牌局未结束"} + + winners = self.determine_winners() + + winnings = self.distribute_pot() + + # 更新筹码 + for pid, amount in winnings.items(): + if pid < len(self.stacks): + self.stacks[pid] += amount + self.current_stage = GameStage.FINISHED + + result = { + "complete": True, + "winners": list(winners.keys()), + "winnings": winnings, + "final_stacks": self.stacks.copy(), + "showdown_hands": {} + } + + # 摊牌信息 + for pid, ranking in winners.items(): + if ranking is not None: + result["showdown_hands"][pid] = { + "cards": [str(card) for card in self.player_cards(pid)], + "hand_type": ranking.hand_type.type_name, + "description": str(ranking) + } + + return result \ No newline at end of file diff --git a/shortdeck_server/__pycache__/arena_adapter.cpython-313.pyc b/shortdeck_server/__pycache__/arena_adapter.cpython-313.pyc index 6f4923232fc0b02f6a80b4102a1393c050d7f1f8..210aab313a741740bf1f9b80f00db4b7cb69b68f 100644 GIT binary patch literal 10382 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 AcmMzZ literal 8238 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 Dc70u2 diff --git a/shortdeck_server/arena_adapter.py b/shortdeck_server/arena_adapter.py index c0aa5e6..4b92947 100644 --- a/shortdeck_server/arena_adapter.py +++ b/shortdeck_server/arena_adapter.py @@ -1,12 +1,8 @@ -from __future__ import annotations - -from typing import List, Dict, Optional -from pathlib import Path - -from shortdeck_arena.simulation import Simulation -from shortdeck_arena.agent import HumanAgent -from shortdeck_arena.game_stage import BlindConfig, GameStage, PlayerState import uuid +from typing import List, Optional, Dict +from shortdeck_arena.simulation import Simulation +from shortdeck_arena.agent import Agent, HumanAgent +from shortdeck_arena.game_stage import BlindConfig class ArenaGame: @@ -18,163 +14,153 @@ class ArenaGame: self.max_players = max_players self.sim: Optional[Simulation] = None - # 筹码管理 - self.stacks: List[int] = [] - - # 盲注配置 self.blind_config = BlindConfig(small_blind, big_blind, ante=0) - - # 游戏标识 self.game_id = str(uuid.uuid4()) - - def join_game(self, name: str) -> int: - if len(self.player_names) >= self.max_players: - raise ValueError("table full") - - pid = len(self.player_names) - self.player_names.append(name) - agent = HumanAgent(pid) + + def join_game(self, name) -> int: + if len(self.agents) >= self.max_players: + raise ValueError("Game is full") + + player_id = len(self.agents) + agent = HumanAgent(player_id) self.agents.append(agent) - self.stacks.append(self.starting_stack) + self.player_names.append(name) + + if len(self.agents) == 1: + self.sim = Simulation(self.agents, blind_config=self.blind_config) + # 筹码默认1000 + self.sim.stacks = [self.starting_stack] * len(self.agents) + elif self.sim: + self.sim.agents = self.agents + self.sim.player_states.append(self.sim.player_states[0].__class__.ACTIVE) + self.sim.pot.append(0) + self.sim.stacks.append(self.starting_stack) + + # 当有至少2个玩家时,触发新一轮 + if len(self.agents) >= 2 and self.sim: + self.sim.stacks = [self.starting_stack] * len(self.agents) + self.sim.new_round() + + return player_id - self.sim = Simulation(self.agents, self.blind_config) - return pid + def apply_action(self, player_id, action, amount: Optional[int] = None) -> dict: + if not self.sim: + return {"success": False, "message": "游戏未开始"} + + try: + self.sim.apply_action(player_id, action, amount) + + self.sim.dump_data() + + return {"success": True, "message": f"Applied {action}"} + except Exception as e: + return {"success": False, "message": str(e)} def info(self, player_id: Optional[int] = None) -> Dict: - """获取游戏状态信息""" + if not self.sim: - return { - "game_id": self.game_id, - "players": self.player_names, - "stacks": [], - "dealer_index": 0, - "current_turn": 0, - "pot": 0, - "stage": "preflop", - "actions": {}, - "player_cards": [], - "board_cards": [], - } + return {"error": "游戏未初始化"} - # 更新栈大小 (扣除已投入底池的金额) - updated_stacks = [] - for i, base_stack in enumerate(self.stacks): - pot_contribution = self.sim.pot[i] if i < len(self.sim.pot) else 0 - updated_stacks.append(max(0, base_stack - pot_contribution)) - - for i in range(len(self.stacks)): - if i < len(updated_stacks): - self.stacks[i] = updated_stacks[i] - - player_cards = [] - board_cards = [] - - # 获取玩家手牌 - try: - if player_id is not None and 0 <= player_id < len(self.agents): - player_cards = [str(c) for c in self.sim.player_cards(player_id)] - except Exception: - player_cards = [] - - # 获取公共牌 (根据当前阶段) - try: - current_stage = self.sim.current_stage.value - board_cards = [str(c) for c in self.sim.board_cards(current_stage)] - except Exception: - board_cards = [] - - # 获取可用动作信息 - actions = {} - if (player_id is not None and player_id == self.sim.current_turn and - self.sim.current_stage != GameStage.FINISHED): - - call_amount = self.sim.get_call_amount(player_id) - available_stack = updated_stacks[player_id] if player_id < len(updated_stacks) else 0 - - # 更新node_info以包含实际栈信息 - node_info = self.sim.node_info() - node_info["bet_max"] = available_stack - - actions = { - "call_amount": call_amount, - "bet_min": node_info["bet_min"], - "bet_max": node_info["bet_max"], - "can_check": call_amount == 0, - "can_fold": True, - "can_call": call_amount > 0 and call_amount <= available_stack, - "can_bet": available_stack > call_amount, - } - - return { + info_data = { "game_id": self.game_id, "players": self.player_names, - "stacks": updated_stacks, - "dealer_index": 0, # 简化:固定庄家位置, (优化轮询) + "dealer_index": self.sim.dealer_position, "current_turn": self.sim.current_turn, - "pot": self.sim.total_pot, "stage": self.sim.current_stage.value, - "actions": actions, - "player_cards": player_cards, - "board_cards": board_cards, - "player_states": [state.value for state in self.sim.player_states], + "total_pot": self.sim.total_pot, + "side_pots": [{"amount": pot.amount, "eligible_players": list(pot.eligible_players)} + for pot in self.sim.get_side_pots()], } - def apply_action(self, pid: int, action: str, amount: Optional[int] = None): - if not self.sim: - raise ValueError("no game") + if player_id is not None and 0 <= player_id < len(self.sim.stacks): + try: + player_cards = self.sim.player_cards(player_id) + info_data["player_cards"] = [str(card) for card in player_cards] + except Exception: + info_data["player_cards"] = [] - # 验证动作合法性 - if pid != self.sim.current_turn: - raise ValueError(f"not your turn, current turn: {self.sim.current_turn}") - - if self.sim.current_stage == GameStage.FINISHED: - raise ValueError("game already finished") - - # 获取玩家可用筹码 - pot_contribution = self.sim.pot[pid] if pid < len(self.sim.pot) else 0 - available_stack = self.stacks[pid] - pot_contribution - - # 预处理动作和金额 - action = action.lower() - - if action in ("bet", "raise") and amount is not None: - # 限制下注金额不超过可用筹码 - if amount > available_stack: - amount = available_stack - # 如果全下,可能需要标记为allin - if amount == available_stack and available_stack > 0: - self.sim.player_states[pid] = PlayerState.ALLIN - - elif action == "call": - # call动作的金额验证 - call_amount = self.sim.get_call_amount(pid) - if call_amount > available_stack: - # 不够跟注,自动allin - amount = available_stack - if available_stack > 0: - self.sim.player_states[pid] = PlayerState.ALLIN + info_data["actions"] = self.sim.get_available_actions(player_id) + else: + info_data["player_cards"] = [] + info_data["actions"] = {"can_act": False, "reason": "Invalid player"} try: - self.sim.apply_action(pid, action, amount) - except ValueError as e: - raise ValueError(f"invalid action: {e}") - - self.sim.dump_data(Path.cwd() / "shortdeck_arena_history.jsonl") + board_cards = self.sim.board_cards(self.sim.current_stage.value) + info_data["board_cards"] = [str(card) for card in board_cards] + except Exception: + info_data["board_cards"] = [] + + info_data["stacks"] = self.sim.stacks.copy() + info_data["player_states"] = [state.value for state in self.sim.player_states] + info_data["current_pot"] = self.sim.pot.copy() + + return info_data + + + def get_hand_strength(self, player_id: int) -> Dict: + ranking = self.sim.evaluate_player_hand(player_id) + if ranking is None: + return {"error": "无法评估手牌"} + return { + "hand_type": ranking.hand_type.type_name, + "description": str(ranking), + "strength": ranking.get_strength(), + "cards": [str(card) for card in ranking.cards] + } + + def check_hand_complete(self) -> bool: + return self.sim.is_hand_complete() if self.sim else False + + def get_winners(self) -> Dict: - @property - def current_turn(self) -> int: if not self.sim: - return 0 - return self.sim.current_turn + return {"error": "游戏未初始化"} + + if not self.sim.is_hand_complete(): + return {"error": "手牌未完成"} + + return self.sim.complete_hand() + + def showdown(self) -> Dict: + if not self.sim: + return {"error": "游戏未初始化"} + + winners = self.sim.determine_winners() + showdown_info = {} + + for pid, ranking in winners.items(): + if ranking is not None: + showdown_info[pid] = { + "cards": [str(card) for card in self.sim.player_cards(pid)], + "hand_type": ranking.hand_type.type_name, + "description": str(ranking), + "strength": ranking.get_strength() + } + else: + showdown_info[pid] = { + "cards": [str(card) for card in self.sim.player_cards(pid)], + "hand_type": "Winner by default", + "description": "Other players folded", + "strength": float('inf') + } + + return { + "showdown": showdown_info, + "pot_distribution": self.sim.distribute_pot() + } @property def pot(self) -> int: - if not self.sim: - return 0 - return self.sim.total_pot - + return self.sim.total_pot if self.sim else 0 + + @property + def stacks(self) -> List[int]: + return self.sim.stacks if self.sim else [] + + @property + def current_turn(self) -> int: + return self.sim.current_turn if self.sim else -1 + @property def history(self) -> List[Dict]: - if not self.sim: - return [] - return self.sim.history + return self.sim.history if self.sim else [] \ No newline at end of file diff --git a/shortdeck_server/game_stage.py b/shortdeck_server/game_stage.py index 2010fd1..a761314 100644 --- a/shortdeck_server/game_stage.py +++ b/shortdeck_server/game_stage.py @@ -12,11 +12,11 @@ class GameStage(Enum): # # 设置首个行动玩家 # pass - def advance_street(self): - # 检查下注轮是否结束 - # 发放公共牌 - # 重置行动顺序 - pass + # def advance_street(self): + # # 检查下注轮是否结束 + # # 发放公共牌 + # # 重置行动顺序 + # pass # def get_call_amount(self, pid): # # 计算跟注所需金额 @@ -31,10 +31,10 @@ class GameStage(Enum): # 标记玩家状态 pass - def evaluate_hand(self, hole_cards, board_cards): - # 短牌手牌强度排序 - # A-6 低顺特殊处理 - pass + # def evaluate_hand(self, hole_cards, board_cards): + # # 短牌手牌强度排序 + # # A-6 低顺特殊处理 + # pass def determine_winners(self, active_players): # 边池分配 diff --git a/shortdeck_server/tests/__pycache__/test_game.cpython-313-pytest-8.4.2.pyc b/shortdeck_server/tests/__pycache__/test_game.cpython-313-pytest-8.4.2.pyc index cef96d928c0c64833f3e8b5205e0345497b338df..8b018d655fa1049bae7152bb6312ce14d7d093b6 100644 GIT binary patch literal 6639 zcmeHLPfQ!x8K1GAjqNdi42D1g)L{$FlwAiKI|&JpW}9WVq)EFxjY3Pbqro1)jhV6D zn9V{JiAt3ktGf-CO03iammXHC+MKFYPkZR0haNDHl8#!bQvXEUR^rx6`@NZY<1vH< z)ZOl3cQo_n``-J#@B7~Oee=GV*>7x&aBxh&`FtURIPUM1VTaEq`2A5Je$HVI^S8K3 zp704u`)>J&-@2p8fZ`_r7ZwiX0>1Cx{&^6-@ z@ZlRo)s)wjMKwne>}UYO&vE>Iw3%w2;n@J@u@Cz(!hsQ_aS}HLsDlR%W*xl7AAx9a z5k4H!Q2XAsoZ47k&D=6t=Q|n5{go*JapI+3$z!Z=0^v zqH^r|L(Ip=BEt-c<4#6Ka69ghB2GpMr^rZw$zpdqBef7@2KHkf;&I;Y~|Qmm~AK872H{nt)0il05KiMBBKn6yOMr;$G~Ut zIVtEwtML@kYGkt5-H8^0X#E1B)pamd90&EFJL}Xctx0OaJx+f2ez9}=A_sZ=6@45h zs6X_5shL(C{g3*)ENk-T!F%Vm7Vw#c(PC0;>g-_;Ypqgit#)Z`Qj63^WodCKu7#u! zllMBG&z#^H>64=HjJz~`z4nY8n`3T`Iu_Z?G6#Rv**gT>j|U{d$;NgGP4ygJqtwpk zB{(U{R+NADB1FrYF0R4ep!wm7 z!qp5{3tVxyVsN!Ww!V1R2;oVW^tI>0=BT%Jmwe;r8Ti&^K3z=~<&a9+Zm zc+raKuV61cU2PwdXl+k@S=&FO+TM3KS~yK>7an#hdh&~1(dRiab7wCP9@(Q=zDGZE z_vo9Ch|DIVtdMd+o0oye-8Y3rLBZ){BDS6cfol$M7?BQZ9XpyaSFW-VCC z3|bZ#dpT;}EH%#&nyLxSh=wgLMQ3K}#9gw*U4m&DYCu{vB0qG9gl!X;m&`2@_<2y4 z%S%LyJY?2VTljg=Pw4k4m7OMWpz0J{wiR49(VSk)n+?WNE~gsC#1j1m|Hp^#{cHU< z&wuv8KY#sOvtbeXm3h@fR15sD@D-r7T?Bqf6g0~LV6o^UZ7`d_M?JUFK&`!EYqW30Xb@ z*HAt^w9@PeQQD5;a(dWAImiXmKdYAa=syY(;yzB}KIpjkMZPZSWpMGh?R;?Y-hbdQ zgG!G6$q`!#{l#F0f1npMc^-b|n2~RkmdmP9mK9>gWU5dmYKi3Z#gal)GYZ4<>{6i! z32T_aq7Ded?39fKeW{qYpz?fCpH+&oM#Y6?Lzz>l1heUem3N@_H9~Y^w#jokSyalh z`VM5fX8l?+Q9+y2&8Rm+%&1Z-6_;h(WHVs76jo3wsqjoRF)6dUUersFH+tD@SST1} zoh*xINjD8G{R=|q$F(U?`)%*0y-dnxY>P5p=8_qJr=qNS(L-Kl*o)43naf^SozN?0 zkf_E|vFt@;ZSUIFrH1Ob;X1A!dAV-PS(d$$5qrpHQgvK9>$r?TKPD1K9_H1X zSMNR0$$g`w2^Rm2!%fcj9W0?}JAA z%fh$~Il0RHB^29edvgAx*dLT?=)0>oo(a*%*DBZ6%G*Ns>dgaOTYMA6*HV>{T@<&^ zEfjy0aXFiKTGv;taWJr!IsgV15YIj{$$yJBKAxyd{Pd00*FFw%(b!sN<@zQXSSK6l z^#w{SRE2?EeB8@lDjb{-)Jbh2CA{oLi{((YVXm z#8c=4Cpd^|jiUn&c$-RdF)jh!ps=a=@mrO*)^Du8z1=vlI`K@1t%+5kdlU6<1gxei z^zS0)+(P}ELbuKCc<614c2OeGM~1tdbG<`@xxAvR(U< z_7)74DoNA_qyU?L*zGbKoIaAKb9&vq&)M;kP$o4+R`N=ztP-ofT3g!(9GQU$@d{!E^k}*0lculw+xj delta 1903 zcmb_c-A^M`6u)<7+D<=aT4;e5x}~KHc6RxcRv-v0t1F4RfZVMCdDv9iF6=_5&Xk9; zSy>Yk>4PrpjS0pWF>W^T#q58ekGpSt)CEUPc<_Z6mjoV+KIpyE56T+!#hb~Q^L>8j zo-^~TJJ%{634#TX^XtQ(WZ8m7N8%WUr3t>YwDSo=4q>tK4w=VBRg?^%zyF*3e2#gE(E)z7@ z>bp}n`7u5e!#Hl}fQ>jUG@s!QBL3dg&^piv5NT!DHo}c|7Pur#f&sP>B%vgY9>fW- z1G)ebgP+)a_%HjI^MHP%8ke4ncWFj76PvlE;-sTzS>uxOe0)hr$r*oEUR3;`Rhrxm{%Z1G=hPQbSY|mQ=}dM`P8DqQQj3DK1h_u~Sk* zNi88uVoX&NN=6Utl~j$jq_e%6VP-Zla|?gr80->>m&T^EFDj1b{d=f?I@_bM?)Pr}3757#@d^46#&u6d4W-@bfO0&Ejn_o;^Q50EG zJhYH)W>HZRsmyiSG)XRJ6erc2X6Mw5tYoDszU9R|SbD@=?f|NgDcc@$0r_2!bA;TJ^J>Gmz%!#Y-t+(X) zz#$I|#*aon7+vjJy^-g9dsf@M!Q9{`=Uc~zb#Zo|GvF!r^3~p4@9J=_f0OgBH|b={ z>fPmv&o<{>=`Tq66LyFCmVL?$QRBROa{2dTC$QBX14tm*eVcsfL2x7XbqirOTDI(= zUB3A2@S&CEf|NgD%Vwf`5G+eur=)`VDajbC^w6a%MDqi!Rkl{VKzVX>V>He_Qwtb5pb8s2Sa$=(+ykjhaSmGC;AOIA`~dYglPQB3vzsM TJ~61gfuDGMybHD}uM@-HLx95J diff --git a/shortdeck_server/tests/test_game.py b/shortdeck_server/tests/test_game.py index 6281683..14d5603 100644 --- a/shortdeck_server/tests/test_game.py +++ b/shortdeck_server/tests/test_game.py @@ -19,10 +19,9 @@ def test_join_and_actions(): assert g.current_turn == 0 # 测试错误的玩家尝试行动 - try: - g.apply_action(1, "fold") - except ValueError as e: - assert "not your turn" in str(e) + result = g.apply_action(1, "fold") + assert result["success"] == False + assert "不是玩家" in result["message"] or "turn" in result["message"].lower() # 小盲玩家call (跟注到大盲) g.apply_action(0, "call") diff --git a/shortdeck_server/tools/__pycache__/run_smoke.cpython-313.pyc b/shortdeck_server/tools/__pycache__/run_smoke.cpython-313.pyc deleted file mode 100644 index 605b6cfa7ab968d38656929b18e1311afbbda45b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1868 zcmb_d%}?A$6d&8OUa!qp0<;ZnRA&|1dZjFEDoBF*A(9d#kVwJ=wGk^>#_NE?u5FFI zt=JQnO3+k-OVm>(j`YT{e?pIox;bP@4prI%Hx&MXzOiF=TkWAuGl%E*X5ReXo8QcP zKJD#Q5scX%ep~xdLg)oI(g8Lf!s!~wCx{>-%%eGh3Od)td6A0nmFA^6OmRGi=QDFM zm336cc~q7s1il|3nkAVLL=_@~%#~G=o%#wPOF^id%cF_J3F&B6Ton!FnA?WHG|Th+ z$ckLw3m3pv?>k|1tLi#l1eRn4f&B_U1^_e!84IU3L3x6f&(@a3cpbzdPPO(okCxzE zL?qG;Da{u$K?Yv9eav?nBI|LyuudJ*3T@YAvg=iN4id`*)b%2UABDchGL?EQ2q81U z4|!~zO?@#{{>G>4VPM%#Ib8E;RB`O}wq}M7{o0}B$oH#ZnbtistoiFsG1y>oK%FSs zp#6ZvZzy~l#3nkzx$iIU6uv9$-rHL`C>^R3Px0g_EFqhVbYww^xmbV`baFw2_8B{P z$)p?GU(N`@;{wUt5IwZa5#zCh7IUfI{-$@>fquq6IeP~>`WG>?ZzQJDkxBdhwRXm- zMsh?Y8pQ1R-{eMBSuWS}U%0d~**hp6YtAQ>XVMd@|pLS4MI&=|OThuqi4 z9{H}PPgh;rX=Gx9Vb7{LEECTe0_Ai!`Z}C%`YTKV(YP=>0|2W|WUj&vGlVjCJ`dc= zr{j(Oe@rt8=4o7s0+fYCty7HsBV~+&3lAyuR(F?D;s)((>vC=$};h?A<|(; zipj~IM!pNqiWBLj@f4_Z%pWq7VD^|7q9Mfy^qmBz=GUPW$doTJ*^avt!+{@0ap}es zFbNLI8v{)m1MQ0uq2B2w(6|-yrWcoYQ+=<6FCHmc@T-p)U?;AI4EF@nyYa7%d$XTxXB^i>NjawN)2jb7w}x6PPb9)&+j z)BFj=VH5I|PilUpUUfdBAAu*o;qW?$6G;$+=P3UIY0r`ICmQ`dH@JK2@%x+dODqW2 dUMhpa@JRvTo~_2O(uHR@`|Zq$gz!j={1<&Tdl~=$ diff --git a/shortdeck_server/tools/run_smoke.py b/shortdeck_server/tools/run_smoke.py deleted file mode 100644 index dc46555..0000000 --- a/shortdeck_server/tools/run_smoke.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations - -import json -from fastapi.testclient import TestClient - -from shortdeck_server.main import app - -client = TestClient(app) - -def pretty(o): - print(json.dumps(o, ensure_ascii=False, indent=2)) - - -def run(): - print('POST /join aa') - r = client.post('/join', json={'name':'aa'}) - pretty(r.json()) - - print('POST /join bb') - r = client.post('/join', json={'name':'bb'}) - pretty(r.json()) - - print('GET /get_game_state?player_id=0') - r = client.get('/get_game_state', params={'player_id':0}) - pretty(r.json()) - - print('POST /apply_action check by pid=0') - r = client.post('/apply_action', json={'player_id':0, 'action':'check'}) - pretty(r.json()) - - print('POST /apply_action bet 10 by pid=1') - r = client.post('/apply_action', json={'player_id':1, 'action':'bet', 'amount':10}) - pretty(r.json()) - -if __name__ == '__main__': - run()