From ee95b8e049a91c08fd51fc022a861b3321977c92 Mon Sep 17 00:00:00 2001 From: jianghaiying Date: Tue, 30 Sep 2025 18:09:49 +0800 Subject: [PATCH] shortdeck1.0 --- .../__pycache__/actor.cpython-313.pyc | Bin 0 -> 1081 bytes .../__pycache__/agent.cpython-313.pyc | Bin 0 -> 2468 bytes .../__pycache__/card.cpython-313.pyc | Bin 0 -> 2984 bytes .../__pycache__/main.cpython-313.pyc | Bin 0 -> 1219 bytes .../__pycache__/simulation.cpython-313.pyc | Bin 0 -> 4532 bytes shortdeck_arena/agent.py | 41 +++++++ shortdeck_arena/card.py | 52 ++++++++ shortdeck_arena/main.py | 23 ++++ shortdeck_arena/simulation.py | 65 ++++++++++ shortdeck_arena_history.jsonl | 6 + .../__pycache__/arena_adapter.cpython-313.pyc | Bin 0 -> 5233 bytes .../__pycache__/game.cpython-313.pyc | Bin 0 -> 4119 bytes .../__pycache__/main.cpython-313.pyc | Bin 0 -> 3000 bytes .../__pycache__/persistence.cpython-313.pyc | Bin 0 -> 1007 bytes shortdeck_server/arena_adapter.py | 111 ++++++++++++++++++ shortdeck_server/data/[].jsonl | 4 + shortdeck_server/main.py | 50 ++++++++ shortdeck_server/persistence.py | 16 +++ shortdeck_server/random_cli.py | 98 ++++++++++++++++ shortdeck_server/requirements.txt | 4 + .../test_game.cpython-313-pytest-8.4.2.pyc | Bin 0 -> 5641 bytes shortdeck_server/tests/test_game.py | 26 ++++ .../__pycache__/run_smoke.cpython-313.pyc | Bin 0 -> 1868 bytes shortdeck_server/tools/run_smoke.py | 36 ++++++ 24 files changed, 532 insertions(+) create mode 100644 shortdeck_arena/__pycache__/actor.cpython-313.pyc create mode 100644 shortdeck_arena/__pycache__/agent.cpython-313.pyc create mode 100644 shortdeck_arena/__pycache__/card.cpython-313.pyc create mode 100644 shortdeck_arena/__pycache__/main.cpython-313.pyc create mode 100644 shortdeck_arena/__pycache__/simulation.cpython-313.pyc create mode 100644 shortdeck_arena/agent.py create mode 100644 shortdeck_arena/card.py create mode 100644 shortdeck_arena/main.py create mode 100644 shortdeck_arena/simulation.py create mode 100644 shortdeck_arena_history.jsonl create mode 100644 shortdeck_server/__pycache__/arena_adapter.cpython-313.pyc create mode 100644 shortdeck_server/__pycache__/game.cpython-313.pyc create mode 100644 shortdeck_server/__pycache__/main.cpython-313.pyc create mode 100644 shortdeck_server/__pycache__/persistence.cpython-313.pyc create mode 100644 shortdeck_server/arena_adapter.py create mode 100644 shortdeck_server/data/[].jsonl create mode 100644 shortdeck_server/main.py create mode 100644 shortdeck_server/persistence.py create mode 100644 shortdeck_server/random_cli.py create mode 100644 shortdeck_server/requirements.txt create mode 100644 shortdeck_server/tests/__pycache__/test_game.cpython-313-pytest-8.4.2.pyc create mode 100644 shortdeck_server/tests/test_game.py create mode 100644 shortdeck_server/tools/__pycache__/run_smoke.cpython-313.pyc create mode 100644 shortdeck_server/tools/run_smoke.py diff --git a/shortdeck_arena/__pycache__/actor.cpython-313.pyc b/shortdeck_arena/__pycache__/actor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c0d7b359d499b9dd4690928c1794608da9368826 GIT binary patch literal 1081 zcmZ8g&ubGw6rR~%+5Sk=($;Fd3~jL8OFW1d5uuc#rVzxfpb!|=%{E<|?8dhfBsulu zP4H6Clis8U{}69I%1S{Hh2F$ey!zhm#;S98`@Na>zWL_8x1(}-lHvOB?$h3T&e#`n z#>>b7AJq{YGMBl0o7FjRL0H??>N@CxHTCr(!?PH3b=TN7>L!?iU1TfFHJ6!dUDxsu zu-YZZJ|)ND>68~mvGPJ(+SM=3#N>gu+A7C z&1S3*XFJ(8nKr6v9Pw1|g)K)*jU?#o!ZaQvlq5uYwRNNUEQbB0=lMZ1*^5E70)PJ; zNe_a^YkIzlq1GFuwv=HMDk;;MOipd=Q%{l-ngjN6{=y4;gz#UTRGdsFHR+JGxuXs1 z8%hP;o)Rh+c!!81q4oli{Hb3}>8Ijs|7q zPIb&LUI;U^QBp9F$e)&T(S=LFiHYWbeXh*I93r1oju)!U6|W$`Y2pvq{qfmf-x-{& zYkHMAdShs17p$k;fhi1ec%~>JR#9k?GEsm9EVL9ZU7jqW!TQZDy)xd|ukDw^w{ypp z)nj`#)0Qzd@R8KPR1YJCcmeTndhLAT)*PF<6nWhMFPW6G8@Ku$!qZYd?R%Z#q$1^R z2#M;1Q4qyaLIs(C6u>3oUnF5vXXz;g;p4sv5{#Os01}(M25drPXe<-jf6mS{ZZ4f! znz``P=4LgIrkI9PB0;--)dv&=RNvH6gB~^!Xe!dl^UeeQnK|`;cc%8u{7JWvqpa|LhT3HymxUNOGf6NM?3VaY;LEoBd3nx!(msF^)noh^EPxY+}1-Pn_Gj53^nHv5Iw`Tnv{ zBP)`1nq8+{UMdw#i7SCuUSxeR5#XY9nok#wEL^Ix^TCqidJDn1Dhmsqd%i8%4)c6x z!NILFOP9G}+acOAS{=B&#&4x!wA?2vc16gFa`#l=h{vfveMaF=mlW46Ads^;xx=a2^lt1 zFaYd2*^;O_vZYaV=Tl&lku}=5@Ow^ihY-CQjf(K7v(ER0A3V}%n@J8(;R5KX(m?qvL#va zwB##!ITu57r>&(}v<0&kJ@O?X4)j-E$?2U|k!-0mn0lZz$xAtmr;GAR%feUls-+&8 z2Y(-dKPiXc(M2C4GzhY^S45|`AAwesudq~JwDdK|4MI~F0l?rj>>^9eKYv=%0C)dq zZC-6fhbv7Z%kbiHWwBZ=aK&}XWv-v~Lc8L8%4HB3Rw7oFe0VH0CMwT+{A$4i$X={+ zx#)#lbC#AozrZ#3T(#tSTt_>=Brcu^?CQ*K$?+!xBX{@S-n)9KJ}|YcH4+0W`J2ZY zgQIsdw=-W4t-bd^`Oa83zTf@BZ2iE|`uMRQcm6#3)95eeM*jW!><9J1`Q_t{Wa{_i z*ssa4)id|!@6E5(9=H#aZ$9bLUVNC$yl~;QS6}#--F1BM88U%4conXbXHrs~jvV7m z7ODogzgP=v3~-;}9pqYgd8y0n4N&hAHKUUs&rWthdUjl4vmrBQ0hPMcOw)*?%^veFH1U@3j{{VD) B-@E_- literal 0 HcmV?d00001 diff --git a/shortdeck_arena/__pycache__/card.cpython-313.pyc b/shortdeck_arena/__pycache__/card.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e09a519884cb1bb658096cce1148062ae55bf8e GIT binary patch literal 2984 zcmai0Z)g-p6rb6DxA!NxB<5mFtzP~}ylSshO`@qN(O6^B29ixMP0}vw-I-i+*-K~l zv<9U4B#JE>B*aodrGgCV8-#jkyD_AXv7fj*euzL_^O z`~JX!8H-;_zlxc+lHM?n-+9Er%d~VnVAAsdM9bwF?VYh z^Z%i_DD>yaC}9*V6Ew<1O<Ws>%a3QHMo zv_cU$hnB{$TOb^TL7EE#1MZ`Pq`x35`l6ScNJQv$Bx^h#1D)k_Mka2x( zhMj1i&gssxxN z%fZm~_`<~8#8R+6tJddWkSvnkMZr;6!p<<$5n%%`S=DvZm`cD_K3#`6WKzfnbp6AO zk;+Sgx_&mvEIXAn6J{F3RC8#TLpvRMoK?ZjP6#!C2*q^6wpsE_#!gte&O#t#)d;8+ z^ArF*Y`p?-i7cz$*^Ux;AyK)(U58871vnoAnmYoL{|T_-hE`LVPxCUr=3@cPUq@J( z7GULLWs!16>Nm{E6*OT*LEsvqb}L>4JUuIZgaASr0y=qJCg4Jz3Fm;BC!6PiM%&v7 zF~A`sm`a>BpV^YPu!(4bCTfxs6f50sxui4_<4((tfk@>d~iLYOdQ0 zGjlUHj%EW5%hg+NYIkB^S`Wmf;Nb@YkA@!(F9o}@YL^>IOorY_kZpr6t3$vHVA}y4 zp}!Sc8?=Ma4nZ4$)(x!>S`V}io@lHdmz1PcBaR`U-dsyaE3a?%*aq<9*hctQ66I6W z)e`rKL0coyX*?0XBOQm6 zyuyE${{f$|R#ng|^JJ(3i?d5Z;Z!~|2pJ=b6wk86AVyF~paw)lRsj>)afD6)3-=9& z!#t#T%#&u)hO=9Se}=fa-6oug$qj+6uKmOd@Tr%3VqK0)Fa!3ZxnW@D$;g@}kd>%& z2^Dg=;0e0Z$c+E-}$WtMd1zjoY0!gWcZcz;6 zJL4-pH zP}T{13*j&V-XM-*Oiw4wIKw+CA~|9#WjQk5-Iha{LoEvf1!psVIK4(HrSnVQDd#z} z){nwGYZ_pd{IPwpFBT!9`_AC=^I+=8+tta!INQQY1sI=^33Iq-$UE3)8&fLLSQa%Yv4}(=fl5L z?9Pd#rtWW%RK@6Xg;azt2i#(K4F233k=&w!2a8u0_a3i-F5DvM?wJ6-!$z7>_vZH_ zzRgNJJAupjVd2+{KbA5qYbs%nr{k<2W%URpr5zo}y@)@zb^%->tAZeJTUDvNe^sIK zPEc5>%n7m_0xR;It3596g(nU?mg4CcR10|FoXx=F2VOV~ju>&I1iT{|2E$S83)4w+ zjNygq8p!SC3anSkQ4hQCq$BJ-m<*@Xy>y<7l+s_xo@b=tFH-xAY+Y46wBd5wDuK^x VbvZpnv4h_i4V5(X4*}qs{s-_{C>a0% literal 0 HcmV?d00001 diff --git a/shortdeck_arena/__pycache__/main.cpython-313.pyc b/shortdeck_arena/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f00bcf22e837183c94206d6d5d77d35f40a2ddcd GIT binary patch literal 1219 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 0: + choices.append("bet") + + action = random.choice(choices) + if action == "bet": + amt = random.randint(max(1, info.get("bet_min", 1)), info.get("bet_max", 1)) + sim.apply_action(self.pid, "bet", amt) + else: + sim.apply_action(self.pid, action) diff --git a/shortdeck_arena/card.py b/shortdeck_arena/card.py new file mode 100644 index 0000000..a57bf94 --- /dev/null +++ b/shortdeck_arena/card.py @@ -0,0 +1,52 @@ +"""ShortDeck card model (6-A, 36 cards).""" +from __future__ import annotations + +from enum import IntEnum +from typing import List + + +class Suit(IntEnum): + S = 0 + H = 1 + D = 2 + C = 3 + + def __str__(self) -> str: + return "shdc"[self.value] + + +class Rank(IntEnum): + R6 = 6 + R7 = 7 + R8 = 8 + R9 = 9 + RT = 10 + RJ = 11 + RQ = 12 + RK = 13 + RA = 14 + + def __str__(self): + if self.value <= 9: + return str(self.value) + return {10: "T", 11: "J", 12: "Q", 13: "K", 14: "A"}[self.value] + + +class Card: + def __init__(self, rank: Rank, suit: Suit): + self.rank = rank + self.suit = suit + + def __repr__(self) -> str: + return f"{str(self.rank)}{str(self.suit)}" + + def __str__(self) -> str: + return self.__repr__() + + @classmethod + def all_short(cls) -> List["Card"]: + cards: List[Card] = [] + for r in [Rank.R6, Rank.R7, Rank.R8, Rank.R9, Rank.RT, Rank.RJ, Rank.RQ, Rank.RK, Rank.RA]: + for s in Suit: + cards.append(Card(r, s)) + return cards diff --git a/shortdeck_arena/main.py b/shortdeck_arena/main.py new file mode 100644 index 0000000..e89adbd --- /dev/null +++ b/shortdeck_arena/main.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from pathlib import Path +from .agent import HumanAgent, RandomAgent +from .simulation import Simulation + + +def main(): + agents = [HumanAgent(0), RandomAgent(1)] + sim = Simulation(agents) + + print("Player cards:") + for i in range(len(agents)): + print(i, sim.player_cards(i)) + + print("Random agent acting...") + agents[1].try_act(sim) + print("History:", sim.history) + sim.dump_data(Path.cwd() / "shortdeck_arena_history.jsonl") + + +if __name__ == "__main__": + main() diff --git a/shortdeck_arena/simulation.py b/shortdeck_arena/simulation.py new file mode 100644 index 0000000..ef39d8c --- /dev/null +++ b/shortdeck_arena/simulation.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import json +import random +from pathlib import Path +from typing import List, Dict, Optional + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .agent import Agent +from .card import Card + + +class Simulation: + def __init__(self, agents: List[Agent]): + self.agents = agents + self.history: List[Dict] = [] + self.cards: List[Card] = [] + self.saved = False + self.new_round() + + def new_round(self): + self.history = [] + self.cards = Card.all_short() + random.shuffle(self.cards) + self.saved = False + + def player_cards(self, pid: int) -> List[Card]: + return self.cards[pid * 2 : pid * 2 + 2] + + def board_cards(self, street: str) -> List[Card]: + nplayers = len(self.agents) + idx_start = nplayers * 2 + if street == "flop": + return self.cards[idx_start: idx_start + 3] + if street == "turn": + return self.cards[idx_start: idx_start + 4] + if street == "river": + return self.cards[idx_start: idx_start + 5] + return [] + + def node_info(self) -> Dict: + return {"bet_min": 1, "bet_max": 100} + + def apply_action(self, pid: int, action: str, amount: Optional[int] = None): + self.history.append({"pid": pid, "action": action, "amount": amount}) + + def to_save_data(self) -> Dict: + players = [f"Agent{a.pid}" for a in self.agents] + return { + "history": self.history, + "players": players, + "player_cards": ["".join(str(c) for c in self.player_cards(i)) for i in range(len(self.agents))], + "board": "".join(str(c) for c in self.board_cards("river")), + } + + def dump_data(self, path: Path | None = None): + if self.saved: + return + if path is None: + path = Path.cwd() / "shortdeck_arena_history.jsonl" + with path.open("a", encoding="utf-8") as f: + f.write(json.dumps(self.to_save_data(), ensure_ascii=False)) + f.write("\n") + self.saved = True diff --git a/shortdeck_arena_history.jsonl b/shortdeck_arena_history.jsonl new file mode 100644 index 0000000..68383b6 --- /dev/null +++ b/shortdeck_arena_history.jsonl @@ -0,0 +1,6 @@ +{"history": [{"pid": 0, "action": "check", "amount": null}], "players": ["Agent0", "Agent1"], "player_cards": ["8s7s", "8cQd"], "board": "Js9d6hAs8d"} +{"history": [{"pid": 0, "action": "check", "amount": null}], "players": ["Agent0", "Agent1"], "player_cards": ["Jc6d", "9hKc"], "board": "Th7dJd6s7h"} +{"history": [{"pid": 0, "action": "check", "amount": null}], "players": ["Agent0", "Agent1"], "player_cards": ["9c7c", "7s7d"], "board": "9sAcQc9hAh"} +{"history": [{"pid": 0, "action": "check", "amount": null}], "players": ["Agent0", "Agent1"], "player_cards": ["Ac6s", "8hQs"], "board": "9s8s9h7cJh"} +{"history": [{"pid": 0, "action": "check", "amount": null}], "players": ["Agent0", "Agent1"], "player_cards": ["Kc6c", "9hKs"], "board": "8dJs7h7dTs"} +{"history": [{"pid": 0, "action": "check", "amount": null}], "players": ["Agent0", "Agent1"], "player_cards": ["AhTd", "QcTc"], "board": "9hKdKcQh7h"} diff --git a/shortdeck_server/__pycache__/arena_adapter.cpython-313.pyc b/shortdeck_server/__pycache__/arena_adapter.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00a859a200bd846cfcd31c31e91b6d135131673d GIT binary patch literal 5233 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 G)$?D+y#PW0 literal 0 HcmV?d00001 diff --git a/shortdeck_server/__pycache__/game.cpython-313.pyc b/shortdeck_server/__pycache__/game.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ca90712f55575b4ec29ee39dd13b13637f4ac3b GIT binary patch literal 4119 zcmbUkOKcn0@hx|kT#-wWl5BlQDq4w3999LCB&_jTMNpb~r-F*N8NR-~rs@w*k08RHAZ|wMC zfCk{pPx2=PDx4Ilcrr+X3h5<7L=_T56^{fw2{g1A$OIoClHuG)&NNGQ&NfP>HI?CA z;gn(7u5jGQ+paWSMtZK8_jYzBm1x^yFcWiycXH?T2O$uhuPb8e@brLlkN?7y&U1_k z(|r9@gv``E)L6mq5IIsukJNX}lACv?-QZ0e2;ad7t#{0_tJ5sWieC@8(`xwNMydMdGp!qCb zZUJzEG~;EK%5Jxk9tTZ`V?jkdOVNeT>; z&$+3L=<-F~q;cSJgSm29Hw&}_5Ozfwp7!B_V1kF?x{X5PzGwpJZRbjcso{;d?d%eK zd=D&J$6&fjzG?4RAKu)*vEOMQSmkSyd|h6Xccf%hN^Tv$BMl-YvKHBq5>+X&HMWz? zR+HH~(t%p6b2Uu+K}Ktj55vk0^oajPflwb!pA8c_0K^POkv|h)2ckm&TX;W;;+;f5 z%iT8r5GiCpKU^iXSl9I{Yge`kPAt7Gr9F#w>%=$7Vju`q$e5fOQ1O0&{OowWPRM~< zTXO)lMciOmOru;rE0~ZmTIC?1wup@vYH1N2t#t#{&oj;Q0M*YjyQrKhWTjz}5vSf` zX(uDNl3{82v+$N(A)hN2-Oz`+t(^yNdqD0|Mxrqg!D7&hy-q~CaKbpEdtq`zxjZW# znUKfBmYe(77N5Iu&so^{^gSEqLaBbw2x4qlWtgs#`@N~!p1rl6-rDykzD$0ZT(>tb zZ(P1Pwv!&KrpNx)bL95mqc*WCyc&8KC%vi7k2gN9^(5J+JF&U6u~bVbJE_rXYP1P- z^=!Vm@n&sr-{$)p@7H=>dDPJs4?iSrZC+qc zGt2lFgyf(W%=?){4K!Z8&dtSJ#mM-@=l1Z;J+lE-7@q?Ug~e=(xS8>5PvahkQMv^F#XT6LeI$(L6*d16$!jKQ+QECq9bMn^yD*sY1?(i7ew&Hn^8O0XLMxN?J+F}IU%?q=cE+EARzCmq zy+(K0&qJ(?KX=0RCdWJ`GdAW9wtl&I&PMNMiZAk}?-B8(JR<)89+~T$g@*1ae?)dg zb*yqIt{{Y^d>O2RO$%EE$?H=w6d7Aj`JQtXm~ zX_7HO{Y4@4uqWw!sbbpDL-CM7U6JMtOLs-!HO!S*jx8!w|EOY6y|B{WFwk3AX|I?| zX6d4-c=kNN0+tC=1;w-CA3mKcR`efJTB2U+?Z%}+oLE1Axz*({Z}ZU6WrAKI_06N$ z0J{-1UGr4BVNCQMY~lStn+L!+PavL?r` zpISS$BM(&Nf#37DE>xAHU&+U6(XRCsCp!4-^2Mrh>?`?gTwZpfN-dFGox~pC^4jIC z!`0Zp%@HRydaqmA9-MHxk8a10qJYV@$t~WIQ#T`3c?7;lJMr@qTY#p2dE!|W7j7r3 z%IsbFG$`5bIp{bQ29k-qD6AKH!|y4RQ5I{n%Fr}MYE zs;?e#`rcSQ`IPE#RsPN{)qoB?r}}oHs=Rksp8cjXwcS7JbY{2Z?EU`1o&IdKKYOdA z+JEQ|`9BVRq5f&lUsHchITKUP8`IU)^r~=I>SLV`#0;yvtlqNn%i;ukd9Q%88q6XF z%eL1>28@64ggn3?$Ow$&@`h=%KLGkek2-|h*iyJbs^dHI76SA~xPZyYYvh97lfYf< zU9deY`c|Ti|Hyd|{y`>;{|MN*!aqV-)6SO)l_J8jrhQb&73(W8O22}!&h!$kMoe{IPM>0=xY-BC)rme`@RjH;JA0We-rj$ G0{#nj*9s#5 literal 0 HcmV?d00001 diff --git a/shortdeck_server/__pycache__/main.cpython-313.pyc b/shortdeck_server/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..206c5ed8ee4df751d0c8aa908b45974a4e850f0c GIT binary patch literal 3000 zcma)8-ES0C6u);qc4xoZZM&r(e6&FM7;G(3s#GevEg#athIFVgbd$-rJ1tAMv&)^S zT1-rVMCx04AV6XQh6f=&Xk(N5WW>Lq4McH-7-M|sTg~o+(HGCTv%6hLi9N}ld+s@B z@165I=iGY>kw}mL-TmsNJ>4Tr)D9p4T3gQ zr44g!7_^ZpZ6p~O-db9&YS3zuVXd}T(4xuE0YR&4CFiBeuC?%=?N!btQLFEP2$8gg z*0pb}d%+H|igz>_fjt_Z*`t1ZliIX`i>ZR^KW5lY{PYPo^3Itvr;mP=GV@L*XQ|W; z^c%K$Dwj61E4Vjk8jO72w9@*xF=^`OGPaXr)94syrez!l<;AFdF2|g-nVRUbP4yx?kR2Jp1u3wlkvL#Ecu8(Dy?PN2SY30By+75F; zYgks!F)(5qw2iuM<1D2cj>9tVPdO&2Sq5{4@)}zc|CMw*A1alE=ZwrzhB^vSoc__JR4u$RvG$k;pNUn15VWeQwyp$j(D3 z<_dnY8^~vkX_M)hG>hPFs7$B|tHntaSsk)^V6K?492UcA1F}YB=xIg;fT=1}Y`1me-G&BaxQ*QktQ>E+_R7%XmaQ|#W(VdvJ{$Z=+UmiC z&KKqG^H8l&_%+P*vVm1;W?hk)7gD7*21i1$9IzD%3&5_EZDbTikA}-FQdYn?LWZTP zX-O=%x+Ro`O(aU)pqxOjZ>bCWjXvBu_dXKXud%AUgaA z;d(dtu92NGk202H48!y459>%3*-ljO1XI&SNZQ-m85W}};WDfXM0B->fz6OrNt6fZ zlUVbO#I?koSHC>?#leTM&e=fGN5ai_)WyB?dzV9pmZU>}7iALOR3H@HYc}5#7Y4qg z-yU18IrKx_lG6Y0s)&|bfV0you%(T-Vp6~M2XImsb!{C~*tT)N!Y zLT&~7#JjQA#YMSKycp?*t0z0HdtJUh*qb2CjexygYd~vNngaGO5ondNyf&8K0_|k2 zRjL{n&&B5mQdZFn<}2)R0#a;c9%nu5Cxh?Z~k9TtC0{;VLjgt8bt!udJ&>Wlod^47_Fy$k8Z?0j}<-@qf~2$WK!ZmwgtZ%Jy}XbU&PrkCSw@m=J> zu3*0+KIo41i{g)>401wkaQ$QO05b9!hQ|vJ$yM^xX~TkN0K*5S8_Ju^hS!d1rA!Zt zh=DI2-AEgG$7BqP#1#QSj-_|!yFl|JwFa+tjZ z&QQ(7o(BeJ5v9M8eNV}@KS}3P(($`5vJ~6B?AtRV{^E~b`FKVuN|e^jsYL=qQ4v7y zEfN@tL6L5oJ5eMstcHmoUv0kBJiF&k=S=ew?OqjR+BZ8#j7grdr-JVtmu}RzPBMCRDvwNx! z;)cS8I4pv&Cz*eS|AA%V#&tu)C*OoV2tIkzt!|8+z&XEj&ll(X(n=;1fU@@CTje zyLe)fT&-Vx4AoeHKF)Lh>=8FD^0PH>)b1z55y=<#1sl1{j4x~>7-K(Ikn8toW+=8KXro8@D_{0sSz?_5rDJ~5NyHx|Ii$yaQ#tXSOsu} zIU$ExXC*J>#XKDs1aQSUF(;jk1I$PL+mcf{=)|UBj&zpqSPjnabaJNl;gOKB<2^Je z!yw#*q2tCD5>OH}7yu2wGCU9mphR?edQ~1-2t0R*6LVFnhNa=V?X$Q=&vM){Ckb8R z%4B9XV@y8Gaq9WD%cT|Ogf^#9A(yP`vLEP_$I9liZ8#Q}Y&T?h>XNf$b5aVTKS)F2 zx2=1H6^|_jzFD*jLB(UCWfvDukP+DIiOmYW%>v}bc8m6iU*p}Tk7F&PJ)nXCkJVT& zc10Q>m_b#C1GT%++lX)XeC`=)szde3{djVtZ@q7Gbh~%0uNl{OX7-i%Mr=LyhQ3X_ zPBg}Ly9U1~>Ep*u`YQQHg2eT`xc)V+{|%zn@9$*##ZUc-%#E0y;t9hjRl_Q?4TD9z zvo2IT=HuVBBRBZ^@MOFb!M$18HH{r~m)} literal 0 HcmV?d00001 diff --git a/shortdeck_server/arena_adapter.py b/shortdeck_server/arena_adapter.py new file mode 100644 index 0000000..08d18fb --- /dev/null +++ b/shortdeck_server/arena_adapter.py @@ -0,0 +1,111 @@ +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 +import uuid + + +class ArenaGame: + def __init__(self, starting_stack: int = 1000, max_players: int = 6): + self.agents = [] + self.player_names: List[str] = [] + self.starting_stack = starting_stack + self.max_players = max_players + self.sim: Optional[Simulation] = None + self.stacks: List[int] = [] + self.current_turn: int = 0 + self.pot: int = 0 + self.game_id = [str(name) for name in self.player_names] + + 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) + self.agents.append(agent) + self.stacks.append(self.starting_stack) + + self.sim = Simulation(self.agents) + return pid + + def info(self, player_id) -> 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": [], + } + + 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: + + board_cards = [str(c) for c in self.sim.board_cards("river")] + except Exception: + board_cards = [] + + return { + "game_id": self.game_id, + "players": self.player_names, + "stacks": list(self.stacks), + "dealer_index": 0, + "current_turn": self.current_turn, + "pot": self.pot, + "stage": "preflop", + "actions": {"bet_min": 1, "bet_max": 100}, + "player_cards": player_cards, + "board_cards": board_cards, + } + + def apply_action(self, pid: int, action: str, amount: Optional[int] = None): + if not self.sim: + raise ValueError("no game") + if pid != self.current_turn: + raise ValueError("not your turn") + + action = action.lower() + if action == "check": + pass + elif action == "bet": + if amount is None: + raise ValueError("bet requires amount") + if amount < 0: + raise ValueError("invalid amount") + if amount > self.stacks[pid]: + + amount = self.stacks[pid] + self.stacks[pid] -= amount + self.pot += amount + elif action == "fold": + self.stacks[pid] = 0 + else: + raise ValueError(f"unknown action: {action}") + + self.sim.apply_action(pid, action, amount) + self.sim.dump_data(Path.cwd() / "shortdeck_arena_history.jsonl") + if len(self.agents) > 0: + self.current_turn = (self.current_turn + 1) % len(self.agents) + + @property + def history(self) -> List[Dict]: + if not self.sim: + return [] + return self.sim.history diff --git a/shortdeck_server/data/[].jsonl b/shortdeck_server/data/[].jsonl new file mode 100644 index 0000000..64020f2 --- /dev/null +++ b/shortdeck_server/data/[].jsonl @@ -0,0 +1,4 @@ +{"history": [{"pid": 0, "action": "check", "amount": null}]} +{"history": [{"pid": 0, "action": "check", "amount": null}, {"pid": 1, "action": "bet", "amount": 10}]} +{"history": [{"pid": 0, "action": "check", "amount": null}]} +{"history": [{"pid": 0, "action": "check", "amount": null}, {"pid": 1, "action": "bet", "amount": 10}]} diff --git a/shortdeck_server/main.py b/shortdeck_server/main.py new file mode 100644 index 0000000..9bb5a6f --- /dev/null +++ b/shortdeck_server/main.py @@ -0,0 +1,50 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +from .persistence import append_game_history +from .arena_adapter import ArenaGame + +app = FastAPI(title="shortdeck-server") +GAME = ArenaGame() + +class JoinRequest(BaseModel): + name: str + + +class JoinResponse(BaseModel): + player_id: int + name: str + + +class ActionRequest(BaseModel): + player_id: int + action: str + amount: int | None = None + + +@app.post("/join", response_model=JoinResponse) +def join(req: JoinRequest): + try: + player_id = GAME.join_game(req.name) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + return JoinResponse(player_id=player_id, name=req.name) + + +@app.get("/get_game_state") +def get_game_state(player_id): + try: + state = GAME.info(player_id) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) from e + return state + + +@app.post("/apply_action") +def apply_action(req: ActionRequest): + try: + GAME.apply_action(req.player_id, req.action, req.amount) + append_game_history(GAME.game_id, {"history": GAME.history}) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + return {"ok": True} diff --git a/shortdeck_server/persistence.py b/shortdeck_server/persistence.py new file mode 100644 index 0000000..ecc2202 --- /dev/null +++ b/shortdeck_server/persistence.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict + +DATA_DIR = Path(__file__).parent.joinpath("data") +DATA_DIR.mkdir(exist_ok=True) + + +def append_game_history(game_id, entry): + file = DATA_DIR / f"{game_id}.jsonl" + with file.open("a", encoding="utf-8") as f: + f.write(json.dumps(entry)) + f.write("\n") + diff --git a/shortdeck_server/random_cli.py b/shortdeck_server/random_cli.py new file mode 100644 index 0000000..a083cbf --- /dev/null +++ b/shortdeck_server/random_cli.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import argparse +import random +import time +from typing import Any, Dict, Optional + +import requests + + +BASE_PATH = "/get_game_state" +APPLY_PATH = "/apply_action" + + +def fetch_game_state(base_url: str, player_id: int) -> Dict[str, Any]: + resp = requests.get(f"{base_url.rstrip('/')}{BASE_PATH}", params={"player_id": player_id}, timeout=5) + resp.raise_for_status() + return resp.json() + + +def post_action(base_url: str, player_id: int, action: str, amount: Optional[int]): + payload = {"player_id": player_id, "action": action} + if amount is not None: + payload["amount"] = amount + resp = requests.post(f"{base_url.rstrip('/')}{APPLY_PATH}", json=payload, timeout=5) + resp.raise_for_status() + return resp.json() + + +def choose_random_action(info: Dict[str, Any]) -> Optional[tuple[str, Optional[int]]]: + actions = info.get("actions") + if not actions: + return None + + is_check = actions.get("is_check") or False + call_amount = int(actions.get("call", 0)) + bet_min = int(actions.get("bet_min", 0)) + bet_max = int(actions.get("bet_max", 0)) + + choices = [("fold", None)] + + if call_amount > 0: + choices.append(("call", call_amount)) + if bet_max >= bet_min and bet_max > 0: + choices.append(("bet", random.randint(bet_min, bet_max))) + choices.append(("allin", bet_max)) + + return random.choice(choices) + + +def run_loop(base_url: str, player_id: int, interval: float = 2.0, seed: Optional[int] = None): + if seed is not None: + random.seed(seed) + + print(f"RandomAgent connecting to {base_url} as player_id={player_id}") + + while True: + try: + info = fetch_game_state(base_url, player_id) + except Exception as e: + print(f"failed to fetch game state: {e}") + time.sleep(interval) + continue + + action = choose_random_action(info) + if action is None: + time.sleep(interval) + continue + + try: + act, amt = action + print(f"posting action {act} {amt}") + post_action(base_url, player_id, act, amt) + except Exception as e: + print(f"failed to post action: {e}") + + time.sleep(interval) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--server", default="http://localhost:8000") + parser.add_argument("--player_id", type=int, default=1) + parser.add_argument("--interval", type=float, default=2.0) + parser.add_argument("--seed", type=int, default=None) + args = parser.parse_args(argv) + + try: + run_loop(args.server, args.player_id, args.interval, args.seed) + except KeyboardInterrupt: + print("stopped") + return 0 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/shortdeck_server/requirements.txt b/shortdeck_server/requirements.txt new file mode 100644 index 0000000..9827ad4 --- /dev/null +++ b/shortdeck_server/requirements.txt @@ -0,0 +1,4 @@ +fastapi>=0.95.0 +uvicorn>=0.22.0 +pydantic>=1.10.0 +requests>=2.30.0 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 new file mode 100644 index 0000000000000000000000000000000000000000..cef96d928c0c64833f3e8b5205e0345497b338df GIT binary patch literal 5641 zcmeGfU2hx5aqolFkw;RWR%Az^Xem+2x{)QDv?VLDtHg-|8BkDdSvZw}!-+h}w5dCG z@6<#tg1SI~iYN;6rGSDyR6x@Led}|7K_5kls?=)>1ZaUmFNFd7p-3Ov*}L1jqZ(Hd zlAsUi;;=h2J3BKwGdH{YG?|PeXn+0HAMgE)N9ZpUVGVHFxTmi{|5Jn!=5C`oj&K z^_IG-l_&>m5�#{PzUayvVT$%wZl2Q@oCfXaP8mHY^rAUY&amN5tpganzV73fJC7 z$hevHM3sMu(4xrXE<_m=jKc_b6vaG;r8~WOzTolV*dn#sX$y`|8Rm||sNfN%VJyGc zrUiC+v<;&o$1G*Gsr_xMQb*Sy6khcX2Q~#e`E+i9>A>+Z{#Dqm9>FqB6eHuDgGCp+ znU;{O9!3jB&>Us5*r;(B&2Y6haMHN+nfyEw@^k8Q_cPDzJ(ZvRRp94T$T$B(KSRc! z2_nKEmUntbu>*HTfUHaKA8{9XW3w1@vi~yMdD&>ou;*pJ0NLMlryfM**!@e)$H#KU z7$@%bGBS>PaBng0Wu$zHjFg!yHo6(9OOTaQX<9>GU|tGFoWy+vul8l0#}mabwebuF z>lvhrkoV7uosNe;^gaCB6L~lW9!}qRC-5*H`rPv#F|RP&PE=p~Rgm{X$41ky$H#JZ zGHY;VOmJ%tK97fsQ7`UYr-*wOlf_0a?jpqf=g*D$2!6TXoheS-(=3v~2r+&6GsX0k z|Ht$TifJZ@5`%Ekt9GMb?n(z4Kg@g*jQA_mJ2>n4XynW7qZM=>EePK|ZGLHq(A!Fm`-GpF$#D;RUdh(h$8%<8 zW+p?y%Q@2)YnAdiNE2X&c9$QfRMXUmr* zP-XECY{9gM&1pH2^nr<_lXmo~OCSqSn+21u1(SB9bWbZi0Bw0H=Mqx^fTsdnG*((8 z@BpbdIbxoQhbnsrDYe4y&We^FI4tTqjY--b1Z z|AJ;m)e?OX=XwZzuGz8sMnzXv;8ASHzg}CnG}BU4Vs|SgwOUn(RwE^2wWboyPQbLX zv{tD?f|z!+Y5<364=Cn6W35_tSe2Ekv7}ZNor;Sorn;hzNZ-+A=lU&A&@wWWq&UL-JwdiXDM`w6rkyco?}H<}QShtAtj#FmK!R zRCuOM=cGxWzL}%VzIOB4-S-Ugz^thy?Jl^QSY^nx08-$``mRRq(!iNc?*wzKwr-zs zg6YIp)ytksP4YS@qHitpL-^|-p+8C8TfM(I-<0x?W)I}Vrn&XT=6wp>Z_3$y(ffA9?8XfjYX#sgW%GG_0LV{xA(A_gVvkexRO36Fqb=$D z6A4A4Kbrr+d}E@q(h}3uf!FKTx5f14H=M31r}stg+Y!?n*L}<-vW?`*y^^4KRRWtpMDmygrW)0GSG$s!uf* z>NA@IO&J7U*wUP?DPPzZy>Canuq}fCinc=VF6H*Qd;nPf@vIjm`kx^V!q=JJma-p> zY^lE(1z>Bm*_qv!yl+>^ZhYUxS^?-!1MQ;`A8WQ@F8TK$Q4Mo2Y>%q)C&B=T`R5Y> zEc?Wy6)}(V7#(228CZ&6}7C^ER8rPhEqR&jh+HO vBC~Xr^xCRXUaM-?$!$RCv2N1i686Dye@4T9L+FQS59Oam`?#->Ihgr34r<6{ literal 0 HcmV?d00001 diff --git a/shortdeck_server/tests/test_game.py b/shortdeck_server/tests/test_game.py new file mode 100644 index 0000000..73a4e6a --- /dev/null +++ b/shortdeck_server/tests/test_game.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from shortdeck_server.arena_adapter import ArenaGame + + +def test_join_and_actions(): + g = ArenaGame(starting_stack=100, max_players=3) + pid0 = g.join_game("aa") + pid1 = g.join_game("bb") + assert pid0 == 0 + assert pid1 == 1 + + state = g.info() + assert state["stacks"] == [100, 100] + try: + g.apply_action(1, "fold") + except ValueError as e: + assert "not your turn" in str(e) + + g.apply_action(0, "check") + assert g.current_turn == 1 + + g.apply_action(1, "bet", 10) + assert g.pot == 10 + assert g.stacks[1] == 90 + assert g.history[-1]["action"] == "bet" diff --git a/shortdeck_server/tools/__pycache__/run_smoke.cpython-313.pyc b/shortdeck_server/tools/__pycache__/run_smoke.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..605b6cfa7ab968d38656929b18e1311afbbda45b GIT binary patch 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~=$ literal 0 HcmV?d00001 diff --git a/shortdeck_server/tools/run_smoke.py b/shortdeck_server/tools/run_smoke.py new file mode 100644 index 0000000..dc46555 --- /dev/null +++ b/shortdeck_server/tools/run_smoke.py @@ -0,0 +1,36 @@ +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()