From 035b2f7af93705683a1c29297d80c70130f4318f Mon Sep 17 00:00:00 2001 From: hefanyang Date: Sun, 24 May 2026 08:10:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=B8=E6=88=8F=E5=8F=AF=E4=BB=A5=E8=BF=90?= =?UTF-8?q?=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 36 + card_game/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 158 bytes card_game/__pycache__/ai.cpython-312.pyc | Bin 0 -> 10014 bytes .../__pycache__/battlefield.cpython-312.pyc | Bin 0 -> 14955 bytes card_game/__pycache__/card.cpython-312.pyc | Bin 0 -> 5026 bytes card_game/__pycache__/config.cpython-312.pyc | Bin 0 -> 19926 bytes card_game/__pycache__/deck.cpython-312.pyc | Bin 0 -> 1593 bytes card_game/__pycache__/effects.cpython-312.pyc | Bin 0 -> 8710 bytes .../__pycache__/factions.cpython-312.pyc | Bin 0 -> 1209 bytes .../__pycache__/ink_style.cpython-312.pyc | Bin 0 -> 20447 bytes card_game/__pycache__/main.cpython-312.pyc | Bin 0 -> 31320 bytes card_game/__pycache__/player.cpython-312.pyc | Bin 0 -> 9866 bytes card_game/__pycache__/ui.cpython-312.pyc | Bin 0 -> 52075 bytes card_game/__pycache__/utils.cpython-312.pyc | Bin 0 -> 589 bytes card_game/ai.py | 148 ++ card_game/battlefield.py | 337 ++++ card_game/card.py | 87 + card_game/config.py | 1588 +++++++++++++++++ card_game/deck.py | 26 + card_game/effects.py | 116 ++ card_game/factions.py | 20 + card_game/ink_style.py | 340 ++++ card_game/main.py | 549 ++++++ card_game/player.py | 187 ++ card_game/ui.py | 776 ++++++++ card_game/utils.py | 11 + requirements.txt | 1 + 28 files changed, 4222 insertions(+) create mode 100644 CLAUDE.md create mode 100644 card_game/__init__.py create mode 100644 card_game/__pycache__/__init__.cpython-312.pyc create mode 100644 card_game/__pycache__/ai.cpython-312.pyc create mode 100644 card_game/__pycache__/battlefield.cpython-312.pyc create mode 100644 card_game/__pycache__/card.cpython-312.pyc create mode 100644 card_game/__pycache__/config.cpython-312.pyc create mode 100644 card_game/__pycache__/deck.cpython-312.pyc create mode 100644 card_game/__pycache__/effects.cpython-312.pyc create mode 100644 card_game/__pycache__/factions.cpython-312.pyc create mode 100644 card_game/__pycache__/ink_style.cpython-312.pyc create mode 100644 card_game/__pycache__/main.cpython-312.pyc create mode 100644 card_game/__pycache__/player.cpython-312.pyc create mode 100644 card_game/__pycache__/ui.cpython-312.pyc create mode 100644 card_game/__pycache__/utils.cpython-312.pyc create mode 100644 card_game/ai.py create mode 100644 card_game/battlefield.py create mode 100644 card_game/card.py create mode 100644 card_game/config.py create mode 100644 card_game/deck.py create mode 100644 card_game/effects.py create mode 100644 card_game/factions.py create mode 100644 card_game/ink_style.py create mode 100644 card_game/main.py create mode 100644 card_game/player.py create mode 100644 card_game/ui.py create mode 100644 card_game/utils.py create mode 100644 requirements.txt diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5d407ff --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,36 @@ +# 战国卡牌 - 水墨风云 + +春秋战国主题卡牌对战游戏,基于 pygame 构建,国风水墨视觉风格。 + +## 运行方式 + +```bash +pip install -r requirements.txt +python -m card_game.main +``` + +## 架构 + +``` +card_game/ + config.py — 常量、水墨色板、卡牌数据库(80+张)、阵营定义、预设卡组 + card.py — Card 类(单位/指令卡) + deck.py — Deck 类(组牌、洗牌、抽牌) + factions.py — 阵营被动技能系统 + utils.py — 工具函数 + player.py — Player 类(手牌、营地、前线、HP、粮草) + ink_style.py — 水墨风格绘制原语(宣纸纹理、毛笔笔触、印章、卷轴等) + effects.py — 视觉特效(浮动文字、攻击线、墨花飞溅) + battlefield.py — 核心游戏逻辑(战斗、指令、回合管理) + ai.py — AI 决策(规则驱动) + ui.py — 所有 UI 渲染(使用 ink_style 水墨风格) + main.py — 主循环、状态机 +``` + +## 游戏规则 + +- 7 个阵营(战国七雄):秦、齐、楚、燕、韩、赵、魏 +- 30 张牌组,起始 4 张手牌,最多 8 张 +- 粮草系统:每回合获得 turn+1 粮草(上限 10) +- 战场:营地(5槽+都城) → 前线(5槽) → 对手 +- 胜利条件:将对方都城 HP 降为 0 diff --git a/card_game/__init__.py b/card_game/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/card_game/__pycache__/__init__.cpython-312.pyc b/card_game/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..601a34a02710afd0e5d6ae7dd650516c5f87eced GIT binary patch literal 158 zcmX@j%ge<81P(_8vq1D?5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!vQ4&%2`x@7D%KCl z$jr+&bWF+3%+q(tPcF?(%_}L^ch4-*PfjdKi2)Ju>4~|iG4b)4d6^~g@p=W7zc_4i d^HWN5QtgUZfu=D6aWRPTk(rT^v4|PS0sy->CRYFe literal 0 HcmV?d00001 diff --git a/card_game/__pycache__/ai.cpython-312.pyc b/card_game/__pycache__/ai.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abd2ed98ad507361db246b0f2d5e7f126ce0d556 GIT binary patch literal 10014 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 literal 0 HcmV?d00001 diff --git a/card_game/__pycache__/battlefield.cpython-312.pyc b/card_game/__pycache__/battlefield.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f9e9abda664362b1e0f3e14b98e9b05c7780590 GIT binary patch literal 14955 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 literal 0 HcmV?d00001 diff --git a/card_game/__pycache__/card.cpython-312.pyc b/card_game/__pycache__/card.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ab4b0d2172ea4e88fb48b0c655ac648fbc99619 GIT binary patch literal 5026 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 literal 0 HcmV?d00001 diff --git a/card_game/__pycache__/effects.cpython-312.pyc b/card_game/__pycache__/effects.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7820abf7830e035423e0490091067273c201cb0d GIT binary patch literal 8710 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 P^w6#J&UFd^Va5Lf?~hz= literal 0 HcmV?d00001 diff --git a/card_game/__pycache__/factions.cpython-312.pyc b/card_game/__pycache__/factions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..469bb8b6bce011dccbd25ebf5cf32fcb1c031956 GIT binary patch literal 1209 zcmZ`&%}*0S6rbH~cl$*P1&zc?MiPk2p%o7%v_d2?aw8^w#2AvzvOA?)+TF6VMYgF4 z2V)`$&>I{)loJ{+-2D^0wB;b~$;1=4YHysJS+*+>eNEra%zN+mKIS)H2L>X5$d|cf z@f{1mPrB&~<1Kn4WU&WS@CvBRTkwieS)y5+69cuok7dQu3?0hLT1m5Ph;3}4au&*! zN=ZW)R%DE|bp)3Uy^0Y|D@PU29*4bje3O@*dI?%vp z59gnDMh*7uPl3qSCKr%(xe~Z8m#PTn4HL>Rf8Imdi7PTWH*HC_ELmASrMaC!P0!1^ zMNU<9#>jY{A*>JemNIRu?*XR}In z={ZIw&O9q<`s(z&TGsT;BSWc{k#6D4f@WnD*;JRPl2+t0TJoTAx?(#+B%S2VR&ue; zI6Bj-xZJz)X*XgG<*$Yj&@RB?H+5bw*`Y?*L#<)IpItUlEmct=H>dgDGCTZqHBU zY6CQbY4>*3#zD6GHhsRWt28)A&%hD%cOv&K#xLwaxIEMCrm@n z`NE$w&=EY&Ieb~(kO~ziqEtzivaH=_SICe00lAb|h? literal 0 HcmV?d00001 diff --git a/card_game/__pycache__/ink_style.cpython-312.pyc b/card_game/__pycache__/ink_style.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..791035980dad5e29956f6ab13a92327086ab2852 GIT binary patch literal 20447 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 QggeT8`FIgmF9$9BKb3BOk^lez literal 0 HcmV?d00001 diff --git a/card_game/__pycache__/main.cpython-312.pyc b/card_game/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c5c71e16e4a31d5b8a315d704bbbd9b373c1d5a GIT binary patch literal 31320 zcmdUY32+-%c3|Va8U#Rq_bpN+!J86wP^U?eqG(CfMeDL`7-B;*XdapdD2W`@J%C2XLFSquqG2QEj$LBYV~pPwi&k z`;G1fz@`q*q_W$hes}->`~Tm+|GVFR_-m`xOu=z)XZGNs0~GbQcq1OB`gh#t0{w32JxP}Z>Rq>WtbhwQ_SlMZri7|I@Yo^+CH;eYFs_8c3USl^mvDTpF1EbF8 z-tFn`?b*}MYjz(z!khcM`+7Wkp6cl9;my5$dp*ZGj_mQQcdK|yr#BcJ@}2Sfh8Vb2 zKh+DVv7`6kkayh2!gY@K+_|A~&l&GP&_6ojIR|6(pZ7t5$v^UfClDMT^6{2{_q>ns zFus8o0s|>8N+qY0`YZyu*C-zai;%(v_%a2{PYtP-(LkzWw2ATj0@6SCI`|y zCKu9tCJ)jACLhv5rU23+rV!HN@gjFIuf-2CAdVj2NDKcp@c%PCEQD)RP%0ZtE)1+B z9`XkcidobkwvcfQ8Zt8AHfR*{f&|F%?u=SokRi&Wm&(Np<+g27v2?+x#CB9v%E;_e z1?ghYDP2kz;#%;l#Nxqx@k&06Pe5-)Vs3~Uq{VCbERGA>&JuGO3ht!0VAOCY zjg*u*(rRX>-AUsoHC9rXR^qDKxBX`pKj#F-J-x$8u--Ty?_S#?m z?A6!*`_z^4#`3?L|I?rSQRMC)fAiDt&+yt|-^dt?OT?Ye>(7m!g>}Vi{UiP$Z(#g^ za{y($G2jb&hDRA6iwlg!b;9cd1FX+C0+0n|1Kx88d%QO2ho^PBhDHZo;B^CdVsS2c zGd2j-2A=n^i2r%@n4dR!{T^)J$MQOYNO;R>8P@qMp-cp9-VD`~K+anR#sb07VFJ*+ z!!tDMWjv`_MyQKpVR-{w1%keFH&whg;2SyaTl}DbKZoQcYT1}| z)iPzdVqMNHzBV#563=bqavR?Y{lzm2O?#uc`@-5Q7Oaysl@&Lx;f!k*DxZiMJC<{b zu01{TbUde;%c+j$)Iv4$=X%PN9XA$p#^RW<^wVtDbU)Vly=EvhBnnF4lyxVk2&*QF z%EH>1F+Wkd2G2!_!cxe#N!dm^ZY<@Dr7>f!2X;;zAhAH*sdM?f`B1tm1|_=rTg)z^Tg|Djr>VUwzd^kB&t6x@a+biy7pQ9-L% zBLw^gqj^jFCcYUQ8>mTjPy*yZ35bPKOhXcao76DCvoMr6E8SB&ng zR9i$NjGj(P^+jDK&4dFm9~qT6VwG&B^8tqSrx^a zfs;vl`UE_CffDMv4Siqgng#;^nkx27-B|EU<7NN^qiT237a!+TeE0elc2MgZH}uKrY>9!O@+R5U|t(<*~PW&T4?UN-M!HGWYo4dtX;NP zuWp*!baltnj@hc1rR<(sZ>?R)dJh zV5I zoJGkkg;W5HniOEv060kt78bFV(Y>Wl0Vk~tY=aWLl$#EzLTbkF9d$^{7#Y)B<~NgL z#>JcgToMhHK@fmN8m$7L6iR?1QB?pG4FXaMXjuCCfk!lw#q1e}Ye2&)+0TdBHVYfrBTSq{IH;a6%pCP>pE$o0A0Uaa=F9N^-*+Zr_ zmF$V&lbMfQ2d+Ybl*x>g>Dvb)%#v|^Islm?p<5dk8atx4&an2LT4!=BJ94I(*GUd z!yMtkUjZSr7XVzC93Rtm011;L2@=|@P8aolc1fpE^L}$aTz_EHbvg_mSPgjXGIf?~ zJ}6T`nnss!0T7V*16a)0C=>h(LY8nSvAlygeHq7_$@?y9TJ=m9MNO(gszC+dNNHJO ze&wbJCU>U*rNn&7b;OF?D^EUjEDCgrjO7?SDKVS!7#KAvEe2|vPsIL4Gw2P!H<9KMhKY$t!p7X+tCgE0klaiHX5AE@bhxUjB zG2XBFI5Oj4baJmL?~AxH;~4D7$bef$&lp4=Zc+=Q+O@KdII6+! zm1T@k?0r%f>=DaDIv`)BLP20=WqdWhBz*oh%O$nKhRAO$$ZJl22qd63>oiG{SV|3H85fj;@E z=SR<@ti8AU1Zh?Cf)C{6y`JtPyE+bb^I2j;z5r+ne7wOkb`B%}&=hzW)_Z})cj2{X zhWzJPMD?gf0L1~)Yr-1wrXcGb@SXO;z)a^@|47i|yEwoiwSuSoLqm{`j)1=6$ar8k zUYO5}vcq0d3p5MohTz3bpydE{0CuWpeZaA?y!JE{xXprOats?&`Pd=sbQEuNpu+S8 zc$FVHwGo(6;Q7w*h6`SQ5G1Dnegwjdk%WjayoMbcnJ^L&2Ch1xMqnSn%1cxtEBl^$ zt+ipfr1Hk8>!;!+ja*4%w50hn%Ia7*-JQrQzBVy4@x7yOJpKC9i+L@}`K323*Dc?% zF6S5BFkUzQp*c}ddZX}q;rGtRYqoPW+ZPMACyL5%Y`eZK(z;mGkSH#X7dLUmO_8y; z$8U}=6}Kl!)?i1GL(!5ZQrgB9x6MEG?umC!EEVreX|0{BXsqCT6etxtp>%fBAVGNKBtm4r{|F z_VR?iAg#J(#uB%ebN2GNf|$L2Ij`i}*Jr*S&#U9|>K5v^-Z~u3dtzFf$gha!H*on4 z(fp=(ejAtHHs2r3-!iRRE~|~?L;~~bNMW?B?LMV;?3n49?wAgO0_4Wl>s#l7kzJ9) zbK~<>x2(~k-P2tOSN^rVGkfE%D$Z3k*B|MeSI-~1mHp1qcPtC_+m>A0@9Cfo;AI_o z>2Fq8I&Jtkw`94Z=8fXli{lloTt#cNV*Pzem(wt-!`a(%eal<9Zx`JxS}bZ?E?#qE z@AbXk*}q&|c4N==J>Pj!u2FikGz#-uU4Omjo&}!1uc7iv|LGs^<-r7f89*W9cRF|W z7^x369qZuqqfI4{{`=gH7C8Nt(Y)WN`K#>i%KaM6-)z)F`X4nq$Qnq+*@%t^?|d09 zR@3PWVvUSL)~8IWLu$4{Y6?Mcs)S+|d9|Aw0gRu+`kFI-CV=Ar&=deOTSPzv zhj~!tIgux*8dA|7DHw(a1IRBBu)TVA>TEo#ip#2+I}*)uhczEtvJz|7hPz^PS)!~G z&&3Ja9;erE^qLr5l_;nVcfI-~Dg|sT(pu$-7j*PE1%ps0^H36B&Qom5qt|9s$cZZv zW%(1bMxwEnKG--&_>bNLjOWy&*Jd=2FrK!vtEd+}T5*P;M?}0Chw?aNI|R{-r@W_f z{XrzT8HYSuWWm?sf>#>B3!7je<`Zy`1VpIm%xDg7WdOh>PjvJ^iGZ}RsOaF$Wc{BW z^MkR=EG&N-m#ash(*d3y7#$k&Grn`6n+uY)&u7ce_*hV~gGP_Q2=7WO5Oo=spdYpZ zJF|dh+{*@udYgR-a?+uuARTH_AakNr0eDi%FJM~>kN^x>9H6m2HghbV7~{H9Z+`I>t2}TR8r7tm#yupe)>VNBpeutP- zn?HLRLc5346N!1YtE`IOe)QUm=@H)WWDqNnW0jT_v`f<4 z37Vu$@T6om6!d!UXRm~`A@bcp23Lkfg|{6n zlix|a3hAYGji56?G@5aMG;b#BLGr!;`PmpUy#Q~;_WYW&j%}=cj5nyv7#!TnZvfsM z_^l+Ulj|yJs8UEF&geJ)ADPkK-@uGkuQCftYvQ3Znk4IVUsB-bO=vpv9O*sT&07z5 z9PaHu;n~-_zqkJgZ{5{#7%XcY{T-bhN4nXU;RCQQLV~7qpO*!5JnIAVI+!VF(#FBV z-AB6nj}W^6Zx|RIIpb%CL7w%p{@^&<2T!`4yd4<;LeiT^jkD~l*zPwmK^p*JD%gL8 zSFd1#>@yq2b2gq^$HAYaE)snE!p#fu=3QL# zuG>9a^S*fV39k9X!pUc1&CjOsD9GVB!NhoB=0ZHLo`XM2{k-;F!#jp}OBdJDb^AEi z(id-enrnG_;gl!V^4w484a@Gv$k45}_|`rS{+bRf90bnkc+`DDWOS+t65nDEfnUch0AS;=WgV3H_iv5xm&|MAT7lU>bQcsNc;S8;MZ=e zq6MAdClj{Z*}_Fz^ z$AhIiBCqTq@`}>43I-QoqYx23Vr?biUbfICNsCfj7U>0~ixkp=YK5KulA}&4BG})B z(SZVjU`NzKuz!FF;z?8>4x_&FI#x+hJh1;7GSWmS)(bfk`DxfFMVWdN>%RsGib=Ms zTc)}13Fc5!ffr%Jkgu>@mH1>O z$2~={Ua=>Uvr|z-LV3hKWu9L41Cres!TwWCY#)#Q4wAGXPnoPVxKc}OgGytQ9RHM_ zOXHFho)+I7zK0|k!Mx+j&p4F0VtIV&PLQD9(DdEK&(9msn-3J{ZU>7WSzs}ceP)r% z79583NpC!agOnh~Sq< zpxCVz94u6x0HP()?dURGPuK+rm0SE%$PN4m67ZNPN+p2@-a8O20{=a9v$NRZmZC*V zk)$Vx)0G@u8K-MGx;9QXadcB87;oOmHSdhkPbBIa!(DM2^$9|~7!96Cg|n@{cO1Qi z+NM>6biTRmBF z+u{|YCEppposz;raua>P2o#kJA&p4Lz=c3d-cU(tpcT>U1KLMim~o(&oY+2bA<%&` zL&xTY^dVvP@qLs8(iF#tRln&&R)SmC$J3)M7)m{$BL>lq*Mrh|XmlJr#)0|a_28)u zT5ETEMVUVj^sU5m-R4=$Q?ryhUi1_?7ck!Qum3G*AxY z3q{E;bpS~Nv`Z`#hLg;d8yG{^b;;m^W6gX}7WKM?^dpSNK`ceK3Xy;9aII6bJWCwWeN|$I+Y{7kzk!l z63U6fG`2CV>;q^ba2c2~U>vQExV40{mW0&_hx1y&OhH(G2bf9ldW~DkI7?aF(f}f0 z%`U^jIlpDV=Ltw6x6kzkBSRWAP17E^YvoJXEgd z==#X67~Qg>dhMbt{B|uicM(6`TFzD*amH+o3489!&NDi!UbNIk`rkfw^H{vGbFs0L zbciFJgAQv`J1k$cl+X3QaqRVD@wJ<|wVQA4SzOyis;}i}kUC>@9agtF(K|S9tKn=l z5l!T1)YcZ(er&OknrN?^YfCmP3aHIoZu7im{%|yR^OPm54f~+>bocDhZwa=53XTSn z8>4Hm^;BBBWUP1qk^Ls&lL1uWt7RUF1Qv}WTjb^$G%F>ilkOX z%&W92NiqZh%O6a-8dTudQxU!j_8chtQ_93dxo@RSTk<*~0*(&F{Vm5aY$>45*ql=cxL{+V*SgS~^sS~fu5>=o8 zM7etj@TS#q_1URsXD=*Ts|n64<7{PdTLWini1f#7>k_pM(^}M5q5Dg9n|NIb`SIG# zTovUL$G3 zP+VBDyYD%uteOWPqGb@ZSL61T+&$z)v&uqK5F|lrPz19FBPVnV4XIP~T?!~<&;p+{ zkB`#Fj`owktN>buCCH@)s zQ}tlg%C1BD7ft~jS3bh5A(~#4irt9)%8;qxa}`P##)eC_}5zh=_tr0?Smfw6Pv zM%f_y4oo5#iM;3hLGKVdk9VdsEVxIaH>2ArV|N+j^EMAMx`f3gyed!Q{U|gz0$W^o z?F5{8GZF)Vhvw<$y@BMl4~#hefQRU581@$QN9Y1EDDh6hK#RST9s=wv*7*nrmV-?a zRy{cCkp{`rWH_Vezz7sT;=qoc_Xj|q6A*cALc@?au*ebfMrou5LAVI;hUC*LxNi0b z@Wez;Qt=~D5RsMtKTs5SRba1E**sLKtRN^do^IjnE%UCZePdXcu-K<}U*3uI#F-a& zR&vhDxU+?Gw#?^7otwi(WVPF;+GkCRmI|O6S1(<@gm#wAi#3~X_5bkM_nwVy-=9u7 znk34xTv$4LFOfLi8tb$DYAK?tSx5Wkf?1I>x0u}ZVF-Az}Ys; zpNZMFCko32hnENwEnE+6I1A#=D$ZFIF)TWp6AdlXUDtYMdSWhjqAq#86NPEm|eN4RI9kR$4`VR2PzBPmnoRQn2Xcw;^>Dq!1;- zCK5nUn`p;1ppp=5>T2G8a9_uX?!%rVPaQmX;Bdd^1aCQcps(8_gp}Cb@3yhO4>QC5 z6q7rcWU#f!w+dJ(i7!2Yp-~j<*#MT%VlE;}7G{!SpTvA{PemOFL14UOe@iMw+p(h^1ED$Z5~GO^8>aOU4}xTf@Bb+|K8R3Bk~Ty$h% zL-$7(Zo`pS(UGw2zgh|t&OE_R+{8JXz^Qt7)VVc`g8sDk^2O!iwR30U)$Lq$d$f4V zeM;jfo#_Hk>#xDi@|p2OS^2E?r0lNguU#Hrwy&LIBD>=a+qs7AG5Zd} z-LB#6Aecv7F?$nHh&OWf#t1XNJ7!0lv|#x~em7=!C-RCCHrGnLfL_JG>QbN0{iY;u zB}Nw*)ebB)kh*D&L527=?1ecin&(MFj~6)#=z{1$IGn zBHE=TO=I?*3?*BCd_y<4p*xzpTUNDo&34`Bx!w~mZsp+5g$hg2V%E&j&GXv%Gq;!+ zy$krNI9snnUM0j=RIsQc@WH*V(|x5pcw;2NKZSvr&&_Q&Ww4`_IaUVG0# z(VeQ5_y*S@!8hCfdGU>g;2Wh?ub}z+iugtn_I{1Nlt!pvvI7{F%o}JvN%AD8L4;J| zh%_Q^gB)10ZtE%-PeG)AX*FD}gk7XrAec<)eI7pJU*Rd(0V8lY=1hKt$WsItBw(@e zIqC9Ll6T=9D`hB`Bu;@81ulEAdjR&nfrZ@bAC^U}G@S80=s{2q{yp|(0Vl+O`~K80Kj@=1_Cra%5{}M zS0&p?3BzIlamc{#0k>#DU$ZM@2FQ|$kM${>vp?n1^R zGK=v)p{17Upk(R?OfINTLZV(MXPpm#)`0I z`qamk+}UlB&5K3rZ&_}Sahvx+8-hj!_K|LlIyZr=g!~LKX?8{GV>Gy|Cf)Z^*rW{3 z<#^1pF;Q4KcYL9s2|Z&AW}o69_>nz(H9G*xMfO}}^JJ2ph3TUx#218VIWFKSm6Aw8 z0;7NugM@8JeVQ}@n_eW%FS{csK^Lrl1ydy>t$$(Ll0?q6L5Y}xkOAKf4^Uu$dIK<$ zT1hODq2h{5OI+hDSx7- zLWV&F(FIg1$@xI+qKH+b4Ji=nA->8yNOhpEyI*-GEFtwpl~NSNkYxhcy8*RF5;P$k zLWvf4HyZjTY{X{|B6A7~iY;HNwy^u*3r{#0@38l*&m$KR=4Zlo79#DXnyZ7yJ;ROoMwMS!j!mU~&@@+zv6qj1CJ7)FS3sky-K%4GC&zUO({ssDA)rf(9W{ zlSdc>i(HDRvAGS`g7=0&=ysgFhFNbx!fTk}vn(j|C>C)VyNr2y(L=^NjN2ksQQ3S7 zRWo)bWGap>Lc0NM92lm&qH8bCy!aVqGc|ytye&6wtKe)E3v1TLY;6!1V7BMR{_FcA zl~LIB|22@7qWSA!yL&<$yNPxh-p@H$B`d*RC12Wg{n<4 z$L8hSg4voIjn^9^nrLA|v@ub* zCSKUW6}ChR*G(IEXCZ6?m_2&qnd{F)HbqO;Pwyr@R&y@c2C(F+PsrVESaNJ!E-0Qo zMk=p|&t_1xSr}ctOa}jKsM>%^vptYZu!8;3#RVq-U&X6Fi`!GSTP_NLhSYOF{ z#rBH*6~`;tuQ*?E$r|9x*~99~LcCwikY-qW**dI)-I)5z_F=LV&Eo=`v!r5VG*cGPEm>J#6#z_N2nv!&! z5J^xkF#v5-M!uWx;$i$l@NH?k&+{5iivKRw{`b*?>~{M zOx}I?Kwtm9UeG9?5OlY=@ObC`j^iG==*YeU!X~bRz5N~ggy6<(7gS6ORHlUtH*a1Dx7^f z(gDF{I9KDe3QTscoN4cLTX^r!4OC9`9P@_%bw8LaT^kqLdp|nFwfAwZK4_q}@p{qx z-rH4N+isze{<%ZfPXJyACv~SwXn`PTmdyK6coDY?LK8?k56ApuB84gaeVkk};aNDt zckq^c1bO}uyjVck6q}9nZveA0aP%yZS$KmWbOd-?%G8pWJZ8)gD0osIO8kf*{C84b z5kf2GkdMe41tG!5qKS@T$zakuW(%=*wHFLC5cXIdgfncceE5Q4^RdAKNCG+VCeH~= zg``i2hakHAFDMrEB`I$sydipY22iy6r+-Ad&8{!hYE#P>6ePdE1cLS2i)OvxUj)hx z8^rkiLTfSQ|AK<#UZD-5HQI8gFV5x0Y_%U3teM*!Eohk0PpfA-?-ZL!7#^qdmO4gnO^Px?8SX}!VH+m0;J{l8qcNAL@Kn%UGO$Ba+!}J*Y<3zGUGz1FR@j(U( z=u?t#$Tk;AY9aAOkUb#>A!HIK41{d$B!(NNh74p!g@WNk9u>Mz%Ds@_VyYKaS2jav zF*v7$7Sn>KN79p;FwHE1L5r|o4RIs5R0CHEe6QecO?JmXQ}A5{MZI~U_Jf{EQ_X!F9WD|nQHHFQ13fT9XmDe7Z!DF(Y(J!2iIvU3cn;- zQJsSHtEkHYTG<7!of`tPz7ov_ItBfK+yjjX2l1iQZokeizrFe;C2CFJMejt+eq%WK>jP^fcV44)LJW` zR)g_^ue>P+^8@BI6{bmPN;F}Uz?>4nmN%V+xbPm_BFg?Tbju!w1fmIBl1eI6e6e3( z0jw@cF#eedvgxW>`)3bvbm^=6lOD3$ zrnY@&{cP~YrR$fXMa_$r=6h<5HHX;i1)tRh&fWkPd?Z;eQH2lN%H4^QTHH1c`Bo=* z^^`({Y_f^`4#bWtn<<-fVe~Z4(Ij~4(wFI36=y7XRlo#SthiIZVyb;1zn!zRC-O>W zy))zCJs8zHXBh%^pZ~qBbB=HABpg*e=coEyAz|yZYi`|AcWC3G^IcxxmC$a_TN=B9=jr|vdttluJ zU<-D7FO;n3Eb9TBfTafjL4y=rukbDuEn8`OSP~@_VJ+_Tj2R21;0npTE170>2tqH` zEl(5!=ZMcO84Cpc3Jgm+@N1EkiVC&WR;rSiDzh)}rEtAx9I(29;U_GUv?9`D1(H1p zAmFWo@sK!Z@Ph&5lVQ^--?AERg&*(`xaqTSFR~xW=@vqa{sYeUdMJhY&h+2}+=laC zfFZ@iaAO$B4+55hVnt5}W5?C=;}R zRD>!fn883VWJ^?iwqVf$zf1#@U(V6xF?x+skO1VOgz2hn$|n3S(~_}#B~?bw zhtQtqA$`b4LJueWs2PV6Y^P2VJeBbT-X?LwY9WD?pk48^iK6WTu#kX5R_n>Ka&XEE zU)3Y*5{epZ<|0Ur(FFANO9uT)N|z|sU(y;b_n&FmF?a8ZWM zIPiyZf`wwHXg49jIFMD!IFzEa0DrGy%&5`_GjT=618^&@384*wZuAmWwHAPgC+=4)U3XgXP zmZRRj-3O9;55~~{>1S`h_jgm@_}OdU{n>Q5si~=-2>N$7K?#xFVfPi0-toI7L)u`B zU*1kEbGmkh4ih9Pr;0;@}`RBN#%6iXG7e`vjse1IEMm3^QyHvk+8Cw0w39 zn=_#~{DPkW`*{{=Sz?NK{RRIB>~|1CSL0wSWq51{b+B;R?8Emf+ zc0?gTPm*Byy+u*JMMxJn4I7vHvz+qQ3s-Vc&Ul4SR}OYkQL zM^L5Y;msZlm@zhl*R;p;wK4CIP{O+LD=FmnD>Q0I$wsLbxGhepn=nEUtx{ptQp4T1*fMNP^n^ z0QUJZbzhyUE4bfc(be8BH0m7pT{XIf`$tuJUCn(Ltt-1g4PbI%(|lc`NdG@3Ail3 zU#!(t-M8x?$u{en?&s@t6`y6HZBAblnfhMO40z?-K*)0h0oS|Qq15h4x~R82LKdHt*zv$Ev} zlQFsjWXn-9C?MhhBf#kZ`LRBD6Zk0%SQf(Xj*KB27e_fzVrCF-f4P>nyK zihe>>{Df-z2~~C{+jXsGrsi71Ov5aDF{hd$O({#nlCnmuspd#?$`-M)*x#76r&=N{G|wd+ zDQCpVG6v=ZBbYv51oNi`d6kGO+Z3`agwG_S**FQtl2K7S985*i(doDtycA6f13__q zZf=%jg2_ZWJ`lV}X44rtd-BXcaE{DgO^Auvv^e-BP8n*JypvNS=cdNSPw>Y^&Wufs zoaRrSksNQ1yv?6EH}So($+3y?NvQ?PCr8G^{Nz~Vxa7v-<4VbC>TCqBAj3Z zY7#i0X5?$6NiYMo2tJ_+dRYYvl$r%AP@B*U)GpY7wg`5h4xt68Q*Z!v2~MDH!3ESK zxPf}Jo=~e~md_<-tlJvy49m|fEbc6$HRaeXkY6KX3H4` zItJ=gsblKuOzIp0jxuj5HL0a(2HGrYNnobgj7KdkzzonzWgY8iYgNimYvo106gr~)(&_yKk4XRA1HS0rY(V2)|17+zC8E9 z(pl~3lnS`)s8@+?Lp6h;+6x@?D%(||D#dbFCY)A zc5cmc4=w&@uoot#i7gb61ys;4LS~boGc%e%Lz9xI@Pp{jXripPFwBf5uC=-xD+~Xp z%9<<#Vq_SXOvp$v#1Rxs`B=6>PBOhWpGXQJ1L?w&d5*-zct*sH1cM}qblJ%x@$yqQLuHAAzwCo@~&~XztkQ0;} zNyw~$TFun+3hFSaio-G)P02GFKh!r3GZ#Sa7m-&we5uW`?BjqZ_t&Zw+bYB4YY=6EMfW0+z>NSlA#i*aidm+j1YGLd7r`ehj#~AdF*a`&>kwgcWyAO zbz6R3#H0)Am1r#$}-hsNBW3IC^8Y4e8Eg6oJnMtPeMCt zTSHQJ0zKY&*=o?9*u38I6%GYjkJo0pdpERd)}gknx+1DBC~wpFm#7nx;YxyF?fQ z7G~2bhEt>!(DJ1DXeJW{u(yI?p=u&XUOqZEm(22);$W1b?%u?)u(U{+H>p`vAa7M^J`m zz;rFCnYf>5&cpk_^cI8nG#vo5CuY%nJ4 z_IwyRSM1$Id-nr-Z`J9&nO({j-Y7Y@-wjs=hKd72pZ{TL;8eAze}((lskV=mov+;O z`_+o0Od8$NPks5 zZO^glUQk*oc4)p$Z9&1HV21|Gw5R6j)Oy0oI^!2Wqyc#}e~fxY1eKNS1`@JL#$-G# zSKvz!YT7u&a&i#Lk{Luc4j#@nY(zM@rx-krnnh%>BB2D;Wx=J`In5f3K&^-t8_b-| zkM`puB{T*OtE_ElT4NinZ`%S5cGP^vhRb+XMI54U|4G1FTkWIZ~`;W{FGf| zS!THzt?^$OudxKR{~kCil3{R<#ESK1_gm}GbUYouK1YTZwkv0Y=!8f`t@YqgayAxC zio=6i$1^aUh{oYRnE!sCDYXr)pIy092!EU{whetlJ<&gQxY((XN#bI&Vur$|WTpOQ zCOa3GjL~$KbijD>U7V>-Mx>TaYcKfpc61sC9RUJZW^>*=v~*}CvvyN!`mzx7r7kwN9?jSXIwO}OqvCArOYS#bB4@sfCE(oH0VLk3%S&gat4$Q zxahETt^=0vU1)-FIz|$DXi)YdU;&ZV0TBf*G=YZsbzF!Uyp}YidN|7&Ch-DBFI{9W zbPU-fYaH%3aT3kMWyv@S)iT0xVsbV^?H_p^E2$Aag*k*N$xPy@*{ku8Nw$;6umJ^B zViV*fJVj(SSV`4R+JyLAayCoN^@2x5qgvf0j=2MbO4myFhu?YZ?_Te#^z1A4>?`~C zZ!pG|j#X}jUD;dp`c@CYePkv4$hYNb6VyMofO*0(E192(>-#Ic`-{E%%UuUb?FXvf zb}(dlu436zv}`H#6)jy=-!^))@%ibav#Zj%tJt}VT4(Cye&{UhzkTS|p`xYlhfc-q zuUPy=i@)G|U zLoo-Om(8a|&cYneYkf0Cn^_{niRQc1s$wuk><6N_{x9RoLrdT#4EedN+PYVv+V;W6 ze}TGD-o3|bd(rXAMY=E zUP;bKfdL{rb&8H94S2wxX8Afb1`*K_hMYkWhnm-<3k&)%K;Nq!cCP~ms*gl(5^ViV zVv@OM8vpWtAj$YZ9rKV~vh(O@&(86hNg)%!0_wQSkbzn+Lx%NIROAsA1Q{}@8}CKa zf|Wc(WEAh4d?mYL{hqwLot#sMmTZFnG8O0cqI3HLXQ<}0zZ7@csG5P2 z7=CZIbF-4@AQFg~juL80qAT2*l4$e6<-X3AQYFz;23EibPeTIH*Kw=0OEOz?tcf3 zpPs}8dceV-c!71pv!d4RZ5hmUZ5#M*JAhlG=o{$)Tg-6Xa2Y&M;{R=7oSx{mG;!U+NeQ?7Uv~=Leq&M%&T~7}>8sBbb*FF%0=D&VAK^JuBRrW7Sc}+|J(0mRunmydQ|8U-la`O9$K1MD5R@u*OWw zAyCLSCb04Fl>%df@3YHhU@i)1L&_P{1A?oWpT@6n{)Y5B&BOw*;1w@qlnvnsK;UFtp1i5*Zmqa`ite5b#$XLXpcQP< zV%62UW?!`z&O&U)1l14=^0ci5Rs+AGldCmve+jckVObilhG%06GdJ0oVH!df7r=SJ z`>xFJRl~BCgW5%4MGSNlbJrB6u#^=_Sb_*JFl~ad~5g%=aH4@-Lv;D{Fp5{kKDH$`34@(Q0s0s1-LaXz-_v|;bh8u9c>7$(W6% zrKVW)YBWi*GJ?@q7Dvv*yfkzxS&scgXTWdR{&H=i7a zl7)_mue1q-PbPN zE52&2)F)UbuooaXy1kX(637;?HD`F)heE2Pp(*WLY5D5b&>i_JJoS!)E%nN zx}ECu{+)KJJCC|kHP~SyJ7MocbB-hn{$?PVxEilF#j?@~?3WT4wwp-1f0p*e~OAx&76$dk}Am17Mk5KE*u(AE{5V)az&OL_Uf9`FXe` zYTi?K$<>Fq$btW~FrIdt4Q*MueP`Ld3+D>axu~yn1-z}snfi7pj?|TstNSaMCqzy9 zc!(Vhu>@U*kPm-OkcJ-^@VwN_!_`}uPh#HA^H=7hNx6k|Ls$B>n7oR)KR~hz$(u+f zk-UZE0umlc6bbq+1TWTP1_>fGRaSBhbE8OlkOYxnB$E=nz)`>E1Lm2*ZQ8ccZ!m>6 zEJnyRx0$wY?6I2mZM2z8zGn`H$+r>MF6B;9XRH-OVic)-ojXf0*2WE{OZ|Yx+|o k%d(G{Q(to&YxtT`$$vF7Z17*0uCL9r1~#<8Af^2LUx3hyR{#J2 literal 0 HcmV?d00001 diff --git a/card_game/__pycache__/ui.cpython-312.pyc b/card_game/__pycache__/ui.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26f88f72323f332b80f18672b640d56de0a7852a GIT binary patch literal 52075 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 Dw_ zk;WBvWlHE0B?W(gUqC~YCRx!@(M54b#q41#Bh9=wyF0V{W^Z%3R|M^*G ai.provisions: + continue + if not self.battlefield.needs_target(card): + self.battlefield.apply_order_effect(card, ai) + ai.play_order(card) + actions.append(("order", card)) + return actions + + def _deploy_units(self, ai): + actions = [] + units = [c for c in ai.hand if c.card_type == "unit" and ai.can_play_card(c)] + units.sort(key=lambda c: c.cost) + for card in units: + if not ai.can_play_card(card): + continue + from card_game.factions import apply_faction_passive + apply_faction_passive(card, ai.faction_id) + slot = ai.deploy_unit(card) + if slot >= 0: + actions.append(("deploy", card, slot)) + self._handle_deploy(card, ai) + return actions + + def _play_targeted_orders(self, ai): + actions = [] + for card in ai.hand[:]: + if card.card_type != "order": + continue + if card.cost > ai.provisions: + continue + if not self.battlefield.needs_target(card): + continue + targets = self.battlefield.get_valid_targets(card, ai) + if targets: + target = self._pick_best_target(card, targets, ai) + if target: + self.battlefield.apply_order_effect(card, ai, target) + ai.play_order(card) + actions.append(("order_targeted", card, target)) + return actions + + def _move_units(self, ai): + actions = [] + if not self.battlefield.can_move_to_frontline(ai): + return actions + for unit in ai.get_support_units(): + op_cost = ai._get_op_cost(unit) + if ai.provisions >= op_cost: + slot = ai.move_to_frontline(unit) + if slot >= 0: + if self.battlefield.frontline_controller is None: + self.battlefield.claim_frontline(ai) + actions.append(("move", unit, slot)) + return actions + + def _attack(self, ai): + actions = [] + player = self.battlefield.player + for unit in ai.get_frontline_units(): + if not unit.can_attack or unit.has_attacked: + continue + enemy_units = player.get_frontline_units() + if enemy_units: + killable = [u for u in enemy_units if u.current_hp <= unit.get_effective_attack()] + if killable: + target = min(killable, key=lambda u: u.current_hp) + else: + target = max(enemy_units, key=lambda u: u.get_effective_attack()) + dead = self.battlefield.resolve_attack(unit, target) + actions.append(("attack_unit", unit, target)) + else: + self.battlefield.attack_capital(unit) + actions.append(("attack_capital", unit)) + + # Ranged units in support attack + for unit in ai.get_support_units(): + if not unit.can_attack or unit.has_attacked or not unit.is_ranged(): + continue + enemy_units = player.get_frontline_units() + if enemy_units: + killable = [u for u in enemy_units if u.current_hp <= unit.get_effective_attack()] + target = min(killable, key=lambda u: u.current_hp) if killable else min(enemy_units, key=lambda u: u.current_hp) + self.battlefield.resolve_attack(unit, target) + actions.append(("attack_unit", unit, target)) + else: + self.battlefield.attack_capital(unit) + actions.append(("attack_capital", unit)) + + return actions + + def _handle_deploy(self, card, ai): + for ability in card.abilities: + if ability.startswith("draw_on_deploy:"): + count = int(ability.split(":")[1]) + for _ in range(count): + ai.draw_card() + elif ability.startswith("damage_on_deploy:"): + dmg = int(ability.split(":")[1]) + opponent = self.battlefield.get_opponent(ai) + targets = opponent.get_all_units() + if targets: + import random + target = random.choice(targets) + target.take_damage(dmg) + + def _pick_best_target(self, card, targets, ai): + if not targets: + return None + if card.effect_type == "destroy_damaged": + dmg_targets = [t for t in targets if hasattr(t, 'current_hp') and t.current_hp < t.max_hp] + if dmg_targets: + return min(dmg_targets, key=lambda u: u.current_hp) + return None + if card.effect_type == "bounce": + return max(targets, key=lambda u: u.get_effective_attack() if hasattr(u, 'get_effective_attack') else 0) + if card.effect_type in ("buff_single", "move_to_front"): + return max(targets, key=lambda u: u.get_effective_attack() if hasattr(u, 'get_effective_attack') else 0) + if card.effect_type == "damage": + unit_targets = [t for t in targets if hasattr(t, 'get_effective_attack')] + if unit_targets: + return max(unit_targets, key=lambda u: u.get_effective_attack()) + return targets[0] if targets else None diff --git a/card_game/battlefield.py b/card_game/battlefield.py new file mode 100644 index 0000000..2e16a46 --- /dev/null +++ b/card_game/battlefield.py @@ -0,0 +1,337 @@ +"""Battlefield: core game logic — combat, orders, turn management.""" + +from card_game.player import Player +from card_game.factions import apply_faction_passive + + +class Battlefield: + def __init__(self, player_faction, ai_faction): + self.player = Player(player_faction, is_ai=False) + self.ai = Player(ai_faction, is_ai=True) + self.turn_number = 0 + self.current_turn = "player" + self.game_over = False + self.winner = None + self.log = [] + self.pending_order = None + self.effects = [] + self.frontline_controller = None + + def start_game(self): + self.player.start_game() + self.ai.start_game() + self.turn_number = 1 + self.current_turn = "player" + self.frontline_controller = None + self.player.start_turn(self.turn_number) + + def get_active_player(self): + return self.player if self.current_turn == "player" else self.ai + + def get_opponent(self, player): + return self.ai if player == self.player else self.player + + # --- Frontline Control --- + + def can_move_to_frontline(self, player): + if self.frontline_controller is None: + return True + if player == self.player and self.frontline_controller == "player": + return True + if player == self.ai and self.frontline_controller == "ai": + return True + return False + + def claim_frontline(self, player): + if player == self.player: + self.frontline_controller = "player" + else: + self.frontline_controller = "ai" + + def _update_frontline_control(self): + if self.frontline_controller == "player": + if not self.player.get_frontline_units(): + self.frontline_controller = None + elif self.frontline_controller == "ai": + if not self.ai.get_frontline_units(): + self.frontline_controller = None + + # --- Turn Management --- + + def end_player_turn(self): + if self.current_turn != "player" or self.game_over: + return + self.current_turn = "ai" + + def start_ai_turn(self): + self.turn_number += 1 + self.ai.start_turn(self.turn_number) + self.current_turn = "ai" + + def end_ai_turn(self): + if self.game_over: + return + self.current_turn = "player" + self.turn_number += 1 + self.player.start_turn(self.turn_number) + + # --- Combat --- + + def resolve_attack(self, attacker, defender): + dead = [] + atk = attacker.get_effective_attack() + + if "siege" in attacker.abilities and defender == "capital": + atk *= 2 + + owner = self._get_unit_owner(attacker) + if (owner and owner.faction_id == "han" + and attacker.unit_type == "archer" and defender == "capital"): + atk += 1 + + if isinstance(defender, str) and defender == "capital": + target_player = self.get_opponent(owner) + target_player.capital_hp -= atk + self._add_effect("damage", target_player, "capital", atk) + self._check_game_over() + attacker.has_attacked = True + attacker.can_attack = False + return dead + + defender.take_damage(atk) + self._add_effect("damage", self._get_unit_owner(defender), defender, atk) + + if not defender.is_alive(): + dead.append(defender) + if owner and owner.faction_id == "qin": + owner.provisions += 1 + + if "no_retaliation" not in attacker.abilities: + if not (attacker.is_ranged() and attacker.zone == "support"): + retal = defender.get_effective_defense() if defender.is_alive() else 0 + if retal > 0: + attacker.take_damage(retal) + self._add_effect("damage", self._get_unit_owner(attacker), attacker, retal) + if not attacker.is_alive(): + dead.append(attacker) + + attacker.has_attacked = True + attacker.can_attack = False + self._cleanup_dead() + self._check_game_over() + return dead + + def attack_capital(self, attacker): + owner = self._get_unit_owner(attacker) + if not owner: + return + self.resolve_attack(attacker, "capital") + + # --- Order Effects --- + + def apply_order_effect(self, card, caster, target=None): + etype = card.effect_type + params = card.effect_params + opponent = self.get_opponent(caster) + + if etype == "damage": + dmg = params["damage"] + if target and hasattr(target, 'take_damage'): + target.take_damage(dmg) + self._add_effect("damage", self._get_unit_owner(target), target, dmg) + self._cleanup_dead() + return True + + elif etype == "damage_hq": + dmg = params["damage"] + opponent.capital_hp -= dmg + self._add_effect("damage", opponent, "capital", dmg) + self._check_game_over() + return True + + elif etype == "damage_all_front": + dmg = params["damage"] + tgt = params.get("target", "enemy") + if tgt == "enemy": + for u in opponent.get_frontline_units(): + u.take_damage(dmg) + self._add_effect("damage", opponent, u, dmg) + else: + for u in caster.get_frontline_units(): + u.take_damage(dmg) + self._add_effect("damage", caster, u, dmg) + self._cleanup_dead() + return True + + elif etype == "draw": + count = params["count"] + for _ in range(count): + caster.draw_card() + return True + + elif etype == "gain_provisions": + caster.provisions += params["amount"] + return True + + elif etype == "buff_all": + atk_b = params.get("attack_bonus", 0) + def_b = params.get("defense_bonus", 0) + dur = params.get("duration", 1) + for u in caster.get_all_units(): + u.buffs.append((atk_b, def_b, dur)) + return True + + elif etype == "buff_type": + unit_type = params["unit_type"] + atk_b = params.get("attack_bonus", 0) + def_b = params.get("defense_bonus", 0) + dur = params.get("duration", 1) + for u in caster.get_all_units(): + if u.unit_type == unit_type: + u.buffs.append((atk_b, def_b, dur)) + return True + + elif etype == "buff_single": + if target and hasattr(target, 'buffs'): + atk_b = params.get("attack_bonus", 0) + def_b = params.get("defense_bonus", 0) + dur = params.get("duration", 1) + target.buffs.append((atk_b, def_b, dur)) + return True + + elif etype == "heal_hq": + amount = params["amount"] + caster.capital_hp = min(caster.max_capital_hp, caster.capital_hp + amount) + return True + + elif etype == "bounce": + if target and hasattr(target, 'zone'): + owner = self._get_unit_owner(target) + if owner: + owner.remove_unit(target) + target.zone = "hand" + if len(owner.hand) < 8: + owner.hand.append(target) + return True + + elif etype == "destroy_damaged": + if target and hasattr(target, 'is_alive'): + if target.current_hp < target.max_hp: + owner = self._get_unit_owner(target) + if owner: + owner.remove_unit(target) + return True + + elif etype == "move_to_front": + if target and hasattr(target, 'zone') and target.zone == "support": + caster.move_to_frontline_free(target) + return True + + elif etype == "summon": + from card_game.card import Card + count = params.get("count", 1) + unit_id = params["unit_id"] + for _ in range(count): + new_card = Card(unit_id) + new_card.turn_played = caster.turn_number + placed = False + for i, s in enumerate(caster.support_line): + if s is None: + caster.support_line[i] = new_card + new_card.zone = "support" + new_card.slot = i + placed = True + break + if not placed: + break + return True + + elif etype == "heal_all": + amount = params.get("amount", 1) + for u in caster.get_all_units(): + u.current_hp = min(u.max_hp, u.current_hp + amount) + return True + + elif etype == "draw_self_damage": + count = params.get("count", 1) + self_damage = params.get("self_damage", 0) + for _ in range(count): + caster.draw_card() + if self_damage > 0: + caster.capital_hp -= self_damage + return True + + return False + + def needs_target(self, card): + etype = card.effect_type + if etype in ("damage", "bounce", "destroy_damaged", "move_to_front", "buff_single"): + return True + return False + + def get_valid_targets(self, card, caster): + etype = card.effect_type + opponent = self.get_opponent(caster) + params = card.effect_params + + if etype == "damage": + tt = params.get("target_type", "any") + targets = [] + if tt in ("any", "enemy_unit"): + targets.extend(opponent.get_all_units()) + if tt == "any": + targets.append(("capital", opponent)) + return targets + + elif etype == "bounce": + return opponent.get_frontline_units() + + elif etype == "destroy_damaged": + return [u for u in opponent.get_all_units() if u.current_hp < u.max_hp] + + elif etype == "move_to_front": + return [u for u in caster.get_support_units()] + + elif etype == "buff_single": + return caster.get_all_units() + + return [] + + # --- Helpers --- + + def _get_unit_owner(self, unit): + for u in self.player.get_all_units(): + if u is unit: + return self.player + for u in self.ai.get_all_units(): + if u is unit: + return self.ai + return None + + def _cleanup_dead(self): + self.player.cleanup_dead() + self.ai.cleanup_dead() + self._update_frontline_control() + + def _check_game_over(self): + if self.player.capital_hp <= 0: + self.game_over = True + self.winner = "ai" + elif self.ai.capital_hp <= 0: + self.game_over = True + self.winner = "player" + + def _add_effect(self, etype, target_player, target, value): + self.effects.append({ + "type": etype, + "target_player": target_player, + "target": target, + "value": value, + "timer": 60, + }) + + def update_effects(self): + for eff in self.effects[:]: + eff["timer"] -= 1 + if eff["timer"] <= 0: + self.effects.remove(eff) diff --git a/card_game/card.py b/card_game/card.py new file mode 100644 index 0000000..95fc47c --- /dev/null +++ b/card_game/card.py @@ -0,0 +1,87 @@ +"""Card class: represents a single card (unit or order).""" + +from card_game.config import CARD_DATABASE, FACTION_COLORS, KEYWORDS + + +class Card: + def __init__(self, card_id): + data = CARD_DATABASE[card_id] + self.id = data["id"] + self.name = data["name"] + self.faction = data["faction"] + self.card_type = data["type"] # "unit" or "order" + self.cost = data["cost"] + self.op_cost = data.get("op_cost", 0) + self.description = data["description"] + self.rarity = data["rarity"] + + # Unit-specific + self.unit_type = data.get("unit_type", None) + self.attack = data.get("attack", 0) + self.defense = data.get("defense", 0) + self.max_hp = data.get("max_hp", 0) + self.abilities = data.get("abilities", []) + + # Order-specific + self.effect_type = data.get("effect_type", None) + self.effect_params = data.get("effect_params", {}) + + # Runtime state + self.current_hp = self.max_hp + self.zone = "hand" # "hand", "support", "frontline" + self.slot = -1 + self.can_attack = False + self.has_moved = False + self.has_attacked = False + self.turn_played = 0 + + # Buff tracking: list of (attack_bonus, defense_bonus, turns_remaining) + self.buffs = [] + + def take_damage(self, amount): + self.current_hp -= amount + + def is_alive(self): + return self.current_hp > 0 + + def get_effective_attack(self): + bonus = sum(b[0] for b in self.buffs) + return self.attack + bonus + + def get_effective_defense(self): + bonus = sum(b[1] for b in self.buffs) + return self.defense + bonus + + def reset_turn_flags(self): + self.can_attack = False + self.has_moved = False + self.has_attacked = False + # Tick buffs + self.buffs = [(a, d, t - 1) for a, d, t in self.buffs if t > 1] + + def can_move_and_attack(self): + return "charge" in self.abilities + + def is_ranged(self): + return "ranged" in self.abilities + + def get_keywords(self): + result = [] + for ability in self.abilities: + base = ability.split(":")[0] + if base in KEYWORDS: + kw = KEYWORDS[base] + desc = kw["desc"] + if ":" in ability: + param = ability.split(":")[1] + desc = desc.replace("X", param) + result.append((kw["icon"], kw["name"], desc, kw["color"])) + return result + + def get_color(self): + if self.faction == "neutral": + return (110, 105, 95) + return FACTION_COLORS.get(self.faction, (128, 128, 128)) + + def __repr__(self): + return f"Card({self.name})" diff --git a/card_game/config.py b/card_game/config.py new file mode 100644 index 0000000..ec7d9c2 --- /dev/null +++ b/card_game/config.py @@ -0,0 +1,1588 @@ +"""All constants, colors, card data, faction data, and deck presets.""" + +# --- Window --- +WINDOW_WIDTH = 1280 +WINDOW_HEIGHT = 720 +FPS = 60 + +# --- 国风水墨色板 (Chinese Ink Painting Palette) --- +# 基础色 +INK_BLACK = (20, 15, 10) # 墨黑 +PAPER_WHITE = (245, 235, 220) # 宣纸白 +GRAY = (128, 128, 128) +DARK_GRAY = (64, 60, 55) +LIGHT_GRAY = (192, 185, 175) + +# 传统色 +ZHU_HONG = (190, 50, 40) # 朱红 +SONGHUA_GREEN = (70, 140, 80) # 松花绿 +DIAN_BLUE = (50, 80, 140) # 靛蓝 +TENG_HUANG = (210, 170, 50) # 藤黄 +JIANG_BROWN = (140, 80, 40) # 酱褐 +QING_CYAN = (55, 120, 150) # 青色 +ZI_PURPLE = (100, 55, 140) # 紫色 +ORANGE = (200, 130, 45) # 橙 +GOLD = (210, 175, 55) # 金 +SILVER = (185, 180, 170) # 银 + +# 兼容旧名 +BLACK = INK_BLACK +WHITE = PAPER_WHITE +RED = ZHU_HONG +GREEN = SONGHUA_GREEN +BLUE = DIAN_BLUE +YELLOW = TENG_HUANG +BROWN = JIANG_BROWN +CYAN = QING_CYAN +PURPLE = ZI_PURPLE +DARK_RED = (139, 30, 30) +DARK_GREEN = (30, 90, 30) + +# 水墨渐变色阶 (从淡到浓) +INK_WASH_1 = (210, 200, 185) # 淡墨 +INK_WASH_2 = (175, 165, 150) # 轻墨 +INK_WASH_3 = (130, 120, 108) # 中墨 +INK_WASH_4 = (80, 72, 62) # 浓墨 +INK_WASH_5 = (35, 28, 20) # 焦墨 + +# 背景色 +BG_COLOR = (235, 225, 205) # 宣纸底色 +FIELD_COLOR = (225, 218, 198) # 淡宣纸 +FRONTLINE_COLOR = (215, 205, 185) # 前线区 +HIGHLIGHT_COLOR = (220, 200, 120, 128) +VALID_TARGET = (100, 200, 100, 100) + +# --- 阵营色 (Faction Colors - muted ink-wash tones) --- +FACTION_COLORS = { + "qin": (160, 50, 45), # 秦 - 朱砂 (Vermillion) + "qi": (65, 125, 70), # 齐 - 松绿 (Pine Green) + "chu": (100, 55, 130), # 楚 - 葡紫 (Grape Purple) + "yan": (55, 115, 145), # 燕 - 青瓷 (Celadon) + "han": (170, 135, 50), # 韩 - 古金 (Antique Gold) + "zhao": (165, 85, 40), # 赵 - 赭石 (Sienna) + "wei": (50, 70, 145), # 魏 - 靛蓝 (Indigo) + "neutral": (110, 105, 95), # 中立 - 墨灰 (Ink Gray) + "ally": (160, 140, 75), # 盟国 - 古铜 (Bronze) +} + +# --- Layout --- +ENEMY_INFO_HEIGHT = 40 +ENEMY_HAND_HEIGHT = 50 +ACTION_BAR_HEIGHT = 50 +HAND_HEIGHT = 120 + +BATTLEFIELD_AVAILABLE = WINDOW_HEIGHT - ENEMY_INFO_HEIGHT - ENEMY_HAND_HEIGHT - HAND_HEIGHT - ACTION_BAR_HEIGHT +ZONE_HEIGHT = BATTLEFIELD_AVAILABLE // 3 + +CARD_WIDTH = 80 +CARD_HEIGHT = 110 +FIELD_CARD_WIDTH = 70 +FIELD_CARD_HEIGHT = 95 +CAPITAL_WIDTH = 80 +CAPITAL_HEIGHT = 60 +HAND_CARD_SPACING = 85 +SLOT_SPACING = 85 + +ENEMY_SUPPORT_Y = ENEMY_INFO_HEIGHT + ENEMY_HAND_HEIGHT +FRONTLINE_Y = ENEMY_SUPPORT_Y + ZONE_HEIGHT +PLAYER_SUPPORT_Y = FRONTLINE_Y + ZONE_HEIGHT +PLAYER_HAND_Y = PLAYER_SUPPORT_Y + ZONE_HEIGHT +ACTION_BAR_Y = WINDOW_HEIGHT - ACTION_BAR_HEIGHT + +MAX_SUPPORT_SLOTS = 5 +MAX_FRONTLINE_SLOTS = 5 +MAX_HAND_SIZE = 8 + +# --- Game Rules --- +STARTING_CAPITAL_HP = 20 +MAX_PROVISIONS = 10 +DECK_SIZE = 30 +STARTING_HAND_SIZE = 4 +FATIGUE_START_DAMAGE = 1 + +# --- Rarity --- +RARITY_LIMITS = { + "common": 3, + "rare": 2, + "legendary": 1, +} + +# --- Unit Types --- +UNIT_TYPES = ["infantry", "cavalry", "chariot", "archer", "siege"] + +# ============================================================ +# CARD DATABASE +# ============================================================ +CARD_DATABASE = { + # ==================== 秦 (Qin) ==================== + "qin_tiesying": { + "id": "qin_tiesying", + "name": "铁鹰剑士", + "faction": "qin", + "type": "unit", + "unit_type": "infantry", + "cost": 3, + "op_cost": 1, + "attack": 3, + "defense": 3, + "max_hp": 3, + "description": "步兵", + "abilities": [], + "rarity": "common", + }, + "qin_qiangnu": { + "id": "qin_qiangnu", + "name": "强弩手", + "faction": "qin", + "type": "unit", + "unit_type": "archer", + "cost": 2, + "op_cost": 1, + "attack": 2, + "defense": 2, + "max_hp": 2, + "description": "弓手·可从营地射击", + "abilities": ["ranged"], + "rarity": "common", + }, + "qin_bubing": { + "id": "qin_bubing", + "name": "秦锐士", + "faction": "qin", + "type": "unit", + "unit_type": "infantry", + "cost": 2, + "op_cost": 1, + "attack": 2, + "defense": 2, + "max_hp": 3, + "description": "步兵", + "abilities": [], + "rarity": "common", + }, + "qin_qibing": { + "id": "qin_qibing", + "name": "秦骑兵", + "faction": "qin", + "type": "unit", + "unit_type": "cavalry", + "cost": 4, + "op_cost": 1, + "attack": 4, + "defense": 2, + "max_hp": 2, + "description": "骑兵·冲锋", + "abilities": ["charge"], + "rarity": "common", + }, + "qin_gongcheng": { + "id": "qin_gongcheng", + "name": "攻城弩", + "faction": "qin", + "type": "unit", + "unit_type": "siege", + "cost": 5, + "op_cost": 2, + "attack": 3, + "defense": 1, + "max_hp": 3, + "description": "攻城·对都城双倍伤害", + "abilities": ["siege", "ranged"], + "rarity": "rare", + }, + "qin_shangyang": { + "id": "qin_shangyang", + "name": "商鞅变法", + "faction": "qin", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "buff_all", + "effect_params": {"attack_bonus": 1, "defense_bonus": 0, "duration": 1}, + "description": "所有友方单位+1攻击", + "rarity": "rare", + }, + "qin_lianheng": { + "id": "qin_lianheng", + "name": "连横", + "faction": "qin", + "type": "order", + "cost": 3, + "op_cost": 0, + "effect_type": "damage_all_front", + "effect_params": {"damage": 2, "target": "enemy"}, + "description": "对所有敌方前线单位造成2伤害", + "rarity": "rare", + }, + "qin_jiancu": { + "id": "qin_jiancu", + "name": "剑卒突击", + "faction": "qin", + "type": "order", + "cost": 1, + "op_cost": 0, + "effect_type": "damage", + "effect_params": {"damage": 2, "target_type": "any"}, + "description": "对一个单位造成2伤害", + "rarity": "common", + }, + "qin_shihuang": { + "id": "qin_shihuang", + "name": "始皇帝令", + "faction": "qin", + "type": "order", + "cost": 5, + "op_cost": 0, + "effect_type": "damage_hq", + "effect_params": {"damage": 5}, + "description": "对敌方都城造成5伤害", + "rarity": "legendary", + }, + + # ==================== 齐 (Qi) ==================== + "qi_jiji": { + "id": "qi_jiji", + "name": "齐技击", + "faction": "qi", + "type": "unit", + "unit_type": "infantry", + "cost": 2, + "op_cost": 1, + "attack": 2, + "defense": 3, + "max_hp": 3, + "description": "步兵", + "abilities": [], + "rarity": "common", + }, + "qi_shangren": { + "id": "qi_shangren", + "name": "临淄商人", + "faction": "qi", + "type": "unit", + "unit_type": "infantry", + "cost": 2, + "op_cost": 1, + "attack": 1, + "defense": 2, + "max_hp": 3, + "description": "部署时:抽1牌", + "abilities": ["draw_on_deploy:1"], + "rarity": "common", + }, + "qi_gongshou": { + "id": "qi_gongshou", + "name": "齐弓手", + "faction": "qi", + "type": "unit", + "unit_type": "archer", + "cost": 3, + "op_cost": 1, + "attack": 3, + "defense": 2, + "max_hp": 2, + "description": "弓手·可从营地射击", + "abilities": ["ranged"], + "rarity": "common", + }, + "qi_tianqi": { + "id": "qi_tianqi", + "name": "天齐战车", + "faction": "qi", + "type": "unit", + "unit_type": "chariot", + "cost": 5, + "op_cost": 2, + "attack": 4, + "defense": 4, + "max_hp": 4, + "description": "战车·无视报复", + "abilities": ["no_retaliation"], + "rarity": "rare", + }, + "qi_tongshang": { + "id": "qi_tongshang", + "name": "通商宽农", + "faction": "qi", + "type": "order", + "cost": 1, + "op_cost": 0, + "effect_type": "gain_provisions", + "effect_params": {"amount": 3}, + "description": "获得3粮草", + "rarity": "common", + }, + "qi_jixia": { + "id": "qi_jixia", + "name": "稷下学宫", + "faction": "qi", + "type": "order", + "cost": 3, + "op_cost": 0, + "effect_type": "draw", + "effect_params": {"count": 2}, + "description": "抽2牌", + "rarity": "rare", + }, + "qi_sunbin": { + "id": "qi_sunbin", + "name": "孙膑兵法", + "faction": "qi", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "damage", + "effect_params": {"damage": 3, "target_type": "enemy_unit"}, + "description": "对一个敌方单位造成3伤害", + "rarity": "rare", + }, + "qi_fuguo": { + "id": "qi_fuguo", + "name": "富国强兵", + "faction": "qi", + "type": "order", + "cost": 4, + "op_cost": 0, + "effect_type": "buff_all", + "effect_params": {"attack_bonus": 1, "defense_bonus": 1, "duration": 1}, + "description": "所有友方单位+1/+1", + "rarity": "legendary", + }, + + # ==================== 楚 (Chu) ==================== + "chu_manbing": { + "id": "chu_manbing", + "name": "楚蛮兵", + "faction": "chu", + "type": "unit", + "unit_type": "infantry", + "cost": 3, + "op_cost": 1, + "attack": 3, + "defense": 4, + "max_hp": 4, + "description": "步兵·高防御", + "abilities": [], + "rarity": "common", + }, + "chu_wuyi": { + "id": "chu_wuyi", + "name": "巫医", + "faction": "chu", + "type": "unit", + "unit_type": "infantry", + "cost": 2, + "op_cost": 1, + "attack": 1, + "defense": 3, + "max_hp": 3, + "description": "回合开始时恢复友方单位1HP", + "abilities": ["heal_all:1"], + "rarity": "common", + }, + "chu_gongshou": { + "id": "chu_gongshou", + "name": "楚弓手", + "faction": "chu", + "type": "unit", + "unit_type": "archer", + "cost": 3, + "op_cost": 1, + "attack": 2, + "defense": 3, + "max_hp": 3, + "description": "弓手·可从营地射击", + "abilities": ["ranged"], + "rarity": "common", + }, + "chu_zhancha": { + "id": "chu_zhancha", + "name": "楚战车", + "faction": "chu", + "type": "unit", + "unit_type": "chariot", + "cost": 5, + "op_cost": 2, + "attack": 4, + "defense": 5, + "max_hp": 5, + "description": "战车·无视报复", + "abilities": ["no_retaliation"], + "rarity": "rare", + }, + "chu_gushou": { + "id": "chu_gushou", + "name": "固守", + "faction": "chu", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "buff_all", + "effect_params": {"attack_bonus": 0, "defense_bonus": 2, "duration": 1}, + "description": "所有友方单位+2防御", + "rarity": "common", + }, + "chu_cici": { + "id": "chu_cici", + "name": "楚辞鼓舞", + "faction": "chu", + "type": "order", + "cost": 4, + "op_cost": 0, + "effect_type": "heal_hq", + "effect_params": {"amount": 5}, + "description": "恢复都城5HP", + "rarity": "rare", + }, + "chu_dazhao": { + "id": "chu_dazhao", + "name": "楚国之怒", + "faction": "chu", + "type": "order", + "cost": 3, + "op_cost": 0, + "effect_type": "damage_all_front", + "effect_params": {"damage": 1, "target": "enemy"}, + "description": "对所有敌方前线单位造成1伤害", + "rarity": "rare", + }, + "chu_xiangyu": { + "id": "chu_xiangyu", + "name": "霸王降世", + "faction": "chu", + "type": "order", + "cost": 6, + "op_cost": 0, + "effect_type": "summon", + "effect_params": {"unit_id": "chu_manbing", "count": 2, "zone": "support"}, + "description": "召唤2个楚蛮兵到营地", + "rarity": "legendary", + }, + + # ==================== 燕 (Yan) ==================== + "yan_qibing": { + "id": "yan_qibing", + "name": "燕骑", + "faction": "yan", + "type": "unit", + "unit_type": "cavalry", + "cost": 2, + "op_cost": 0, + "attack": 3, + "defense": 1, + "max_hp": 2, + "description": "骑兵·行动费用-1", + "abilities": ["charge"], + "rarity": "common", + }, + "yan_cike": { + "id": "yan_cike", + "name": "刺客", + "faction": "yan", + "type": "unit", + "unit_type": "infantry", + "cost": 3, + "op_cost": 1, + "attack": 4, + "defense": 1, + "max_hp": 1, + "description": "可攻击敌方营地单位", + "abilities": ["can_attack_support"], + "rarity": "rare", + }, + "yan_bubing": { + "id": "yan_bubing", + "name": "燕步兵", + "faction": "yan", + "type": "unit", + "unit_type": "infantry", + "cost": 2, + "op_cost": 1, + "attack": 2, + "defense": 2, + "max_hp": 2, + "description": "步兵", + "abilities": [], + "rarity": "common", + }, + "yan_gongshou": { + "id": "yan_gongshou", + "name": "燕弓手", + "faction": "yan", + "type": "unit", + "unit_type": "archer", + "cost": 3, + "op_cost": 1, + "attack": 2, + "defense": 2, + "max_hp": 2, + "description": "弓手·可从营地射击", + "abilities": ["ranged"], + "rarity": "common", + }, + "yan_jingke": { + "id": "yan_jingke", + "name": "荆轲刺秦", + "faction": "yan", + "type": "order", + "cost": 4, + "op_cost": 0, + "effect_type": "damage_hq", + "effect_params": {"damage": 4}, + "description": "对敌方都城造成4伤害", + "rarity": "rare", + }, + "yan_jixing": { + "id": "yan_jixing", + "name": "急行军", + "faction": "yan", + "type": "order", + "cost": 1, + "op_cost": 0, + "effect_type": "move_to_front", + "effect_params": {}, + "description": "将一个友方单位移至前线", + "rarity": "common", + }, + "yan_tuxi": { + "id": "yan_tuxi", + "name": "突袭", + "faction": "yan", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "buff_type", + "effect_params": {"unit_type": "cavalry", "attack_bonus": 2, "defense_bonus": 0, "duration": 1}, + "description": "所有骑兵+2攻击", + "rarity": "common", + }, + "yan_yanzhao": { + "id": "yan_yanzhao", + "name": "燕昭王求贤", + "faction": "yan", + "type": "order", + "cost": 3, + "op_cost": 0, + "effect_type": "draw", + "effect_params": {"count": 2}, + "description": "抽2牌", + "rarity": "rare", + }, + + # ==================== 韩 (Han) ==================== + "han_nubing": { + "id": "han_nubing", + "name": "韩弩兵", + "faction": "han", + "type": "unit", + "unit_type": "archer", + "cost": 2, + "op_cost": 1, + "attack": 1, + "defense": 3, + "max_hp": 3, + "description": "弓手·可从营地射击", + "abilities": ["ranged"], + "rarity": "common", + }, + "han_shushi": { + "id": "han_shushi", + "name": "术士", + "faction": "han", + "type": "unit", + "unit_type": "infantry", + "cost": 2, + "op_cost": 1, + "attack": 2, + "defense": 2, + "max_hp": 2, + "description": "部署时:对敌单位造成2伤害", + "abilities": ["damage_on_deploy:2"], + "rarity": "common", + }, + "han_jianbing": { + "id": "han_jianbing", + "name": "韩剑兵", + "faction": "han", + "type": "unit", + "unit_type": "infantry", + "cost": 3, + "op_cost": 1, + "attack": 3, + "defense": 3, + "max_hp": 3, + "description": "步兵", + "abilities": [], + "rarity": "common", + }, + "han_qibing": { + "id": "han_qibing", + "name": "韩骑兵", + "faction": "han", + "type": "unit", + "unit_type": "cavalry", + "cost": 4, + "op_cost": 1, + "attack": 3, + "defense": 2, + "max_hp": 3, + "description": "骑兵·冲锋", + "abilities": ["charge"], + "rarity": "common", + }, + "han_weiwei": { + "id": "han_weiwei", + "name": "围魏救赵", + "faction": "han", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "bounce", + "effect_params": {"target_type": "enemy_front"}, + "description": "将一个敌方前线单位返回手牌", + "rarity": "rare", + }, + "han_fubing": { + "id": "han_fubing", + "name": "伏兵", + "faction": "han", + "type": "order", + "cost": 3, + "op_cost": 0, + "effect_type": "destroy_damaged", + "effect_params": {}, + "description": "消灭一个受伤的敌方单位", + "rarity": "rare", + }, + "han_liannu": { + "id": "han_liannu", + "name": "连弩齐射", + "faction": "han", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "damage_all_front", + "effect_params": {"damage": 1, "target": "enemy"}, + "description": "对所有敌方前线单位造成1伤害", + "rarity": "common", + }, + "han_shenjian": { + "id": "han_shenjian", + "name": "神臂弓", + "faction": "han", + "type": "order", + "cost": 4, + "op_cost": 0, + "effect_type": "damage_hq", + "effect_params": {"damage": 4}, + "description": "对敌方都城造成4伤害", + "rarity": "legendary", + }, + + # ==================== 赵 (Zhao) ==================== + "zhao_bianqi": { + "id": "zhao_bianqi", + "name": "赵边骑", + "faction": "zhao", + "type": "unit", + "unit_type": "cavalry", + "cost": 3, + "op_cost": 1, + "attack": 4, + "defense": 2, + "max_hp": 2, + "description": "骑兵·冲锋", + "abilities": ["charge"], + "rarity": "common", + }, + "zhao_tieqi": { + "id": "zhao_tieqi", + "name": "铁骑", + "faction": "zhao", + "type": "unit", + "unit_type": "cavalry", + "cost": 5, + "op_cost": 2, + "attack": 5, + "defense": 3, + "max_hp": 4, + "description": "骑兵·无视报复", + "abilities": ["charge", "no_retaliation"], + "rarity": "rare", + }, + "zhao_bubing": { + "id": "zhao_bubing", + "name": "赵步兵", + "faction": "zhao", + "type": "unit", + "unit_type": "infantry", + "cost": 2, + "op_cost": 1, + "attack": 2, + "defense": 2, + "max_hp": 3, + "description": "步兵", + "abilities": [], + "rarity": "common", + }, + "zhao_gongshou": { + "id": "zhao_gongshou", + "name": "赵弓骑", + "faction": "zhao", + "type": "unit", + "unit_type": "archer", + "cost": 3, + "op_cost": 1, + "attack": 3, + "defense": 2, + "max_hp": 2, + "description": "弓手·可从营地射击", + "abilities": ["ranged"], + "rarity": "common", + }, + "zhao_qixi": { + "id": "zhao_qixi", + "name": "奇袭", + "faction": "zhao", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "buff_type", + "effect_params": {"unit_type": "cavalry", "attack_bonus": 2, "defense_bonus": 0, "duration": 1}, + "description": "所有骑兵+2攻击", + "rarity": "common", + }, + "zhao_wuling": { + "id": "zhao_wuling", + "name": "武灵王令", + "faction": "zhao", + "type": "order", + "cost": 3, + "op_cost": 0, + "effect_type": "buff_all", + "effect_params": {"attack_bonus": 2, "defense_bonus": 0, "duration": 1}, + "description": "所有友方骑兵+2攻击", + "rarity": "rare", + }, + "zhao_chongfeng": { + "id": "zhao_chongfeng", + "name": "冲锋号令", + "faction": "zhao", + "type": "order", + "cost": 1, + "op_cost": 0, + "effect_type": "move_to_front", + "effect_params": {}, + "description": "将一个友方单位移至前线", + "rarity": "common", + }, + "zhao_lianpo": { + "id": "zhao_lianpo", + "name": "廉颇之勇", + "faction": "zhao", + "type": "order", + "cost": 4, + "op_cost": 0, + "effect_type": "buff_single", + "effect_params": {"attack_bonus": 3, "defense_bonus": 3, "duration": 1}, + "description": "一个友方单位+3/+3", + "rarity": "legendary", + }, + + # ==================== 魏 (Wei) ==================== + "wei_wuzu": { + "id": "wei_wuzu", + "name": "魏武卒", + "faction": "wei", + "type": "unit", + "unit_type": "infantry", + "cost": 3, + "op_cost": 1, + "attack": 2, + "defense": 4, + "max_hp": 4, + "description": "步兵·高防御", + "abilities": [], + "rarity": "common", + }, + "wei_zhongzhuang": { + "id": "wei_zhongzhuang", + "name": "重装步兵", + "faction": "wei", + "type": "unit", + "unit_type": "infantry", + "cost": 3, + "op_cost": 1, + "attack": 3, + "defense": 3, + "max_hp": 3, + "description": "步兵", + "abilities": [], + "rarity": "common", + }, + "wei_gongshou": { + "id": "wei_gongshou", + "name": "魏弓手", + "faction": "wei", + "type": "unit", + "unit_type": "archer", + "cost": 2, + "op_cost": 1, + "attack": 2, + "defense": 2, + "max_hp": 2, + "description": "弓手·可从营地射击", + "abilities": ["ranged"], + "rarity": "common", + }, + "wei_qibing": { + "id": "wei_qibing", + "name": "魏骑兵", + "faction": "wei", + "type": "unit", + "unit_type": "cavalry", + "cost": 4, + "op_cost": 1, + "attack": 4, + "defense": 2, + "max_hp": 3, + "description": "骑兵·冲锋", + "abilities": ["charge"], + "rarity": "common", + }, + "wei_zhengjun": { + "id": "wei_zhengjun", + "name": "整军备战", + "faction": "wei", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "buff_type", + "effect_params": {"unit_type": "infantry", "attack_bonus": 1, "defense_bonus": 1, "duration": 1}, + "description": "所有步兵+1/+1", + "rarity": "common", + }, + "wei_diaodu": { + "id": "wei_diaodu", + "name": "调度", + "faction": "wei", + "type": "order", + "cost": 1, + "op_cost": 0, + "effect_type": "move_to_front", + "effect_params": {}, + "description": "将一个友方单位移至前线", + "rarity": "common", + }, + "wei_lianbao": { + "id": "wei_lianbao", + "name": "连环堡", + "faction": "wei", + "type": "order", + "cost": 3, + "op_cost": 0, + "effect_type": "buff_all", + "effect_params": {"attack_bonus": 0, "defense_bonus": 2, "duration": 2}, + "description": "所有友方单位+2防御持续2回合", + "rarity": "rare", + }, + "wei_weiliaozi": { + "id": "wei_weiliaozi", + "name": "尉缭子兵法", + "faction": "wei", + "type": "order", + "cost": 5, + "op_cost": 0, + "effect_type": "damage_all_front", + "effect_params": {"damage": 3, "target": "enemy"}, + "description": "对所有敌方前线单位造成3伤害", + "rarity": "legendary", + }, + + # ==================== 中立牌 (Neutral) ==================== + "neutral_miliao": { + "id": "neutral_miliao", + "name": "密探", + "faction": "neutral", + "type": "unit", + "unit_type": "infantry", + "cost": 1, + "op_cost": 1, + "attack": 1, + "defense": 1, + "max_hp": 2, + "description": "部署时:抽1牌", + "abilities": ["draw_on_deploy:1"], + "rarity": "common", + }, + "neutral_yimin": { + "id": "neutral_yimin", + "name": "义民", + "faction": "neutral", + "type": "unit", + "unit_type": "infantry", + "cost": 1, + "op_cost": 1, + "attack": 1, + "defense": 2, + "max_hp": 2, + "description": "步兵", + "abilities": [], + "rarity": "common", + }, + "neutral_liangcao": { + "id": "neutral_liangcao", + "name": "粮草补给", + "faction": "neutral", + "type": "order", + "cost": 0, + "op_cost": 0, + "effect_type": "gain_provisions", + "effect_params": {"amount": 2}, + "description": "获得2粮草", + "rarity": "common", + }, + + # ==================== 盟国 (Ally) ==================== + # --- 鲁 (Lu) --- + "ally_lu_dizi": { + "id": "ally_lu_dizi", + "name": "孔门弟子", + "faction": "ally", + "ally_state": "鲁", + "type": "unit", + "unit_type": "infantry", + "cost": 1, + "op_cost": 1, + "attack": 1, + "defense": 2, + "max_hp": 2, + "description": "部署时:抽1牌", + "abilities": ["draw_on_deploy:1"], + "rarity": "common", + }, + "ally_lu_liyue": { + "id": "ally_lu_liyue", + "name": "礼乐教化", + "faction": "ally", + "ally_state": "鲁", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "buff_all", + "effect_params": {"attack_bonus": 0, "defense_bonus": 1, "duration": 1}, + "description": "所有友方+1防御", + "rarity": "common", + }, + "ally_lu_gongjiang": { + "id": "ally_lu_gongjiang", + "name": "鲁国工匠", + "faction": "ally", + "ally_state": "鲁", + "type": "unit", + "unit_type": "infantry", + "cost": 2, + "op_cost": 1, + "attack": 2, + "defense": 2, + "max_hp": 2, + "description": "部署时:获得2粮草", + "abilities": ["gain_on_deploy:2"], + "rarity": "common", + }, + "ally_lu_xingtan": { + "id": "ally_lu_xingtan", + "name": "杏坛讲学", + "faction": "ally", + "ally_state": "鲁", + "type": "order", + "cost": 3, + "op_cost": 0, + "effect_type": "draw", + "effect_params": {"count": 2}, + "description": "抽2牌", + "rarity": "rare", + }, + + # --- 宋 (Song) --- + "ally_song_shanggu": { + "id": "ally_song_shanggu", + "name": "商贾", + "faction": "ally", + "ally_state": "宋", + "type": "unit", + "unit_type": "infantry", + "cost": 1, + "op_cost": 1, + "attack": 1, + "defense": 2, + "max_hp": 2, + "description": "部署时:获得2粮草", + "abilities": ["gain_on_deploy:2"], + "rarity": "common", + }, + "ally_song_ren": { + "id": "ally_song_ren", + "name": "宋襄之仁", + "faction": "ally", + "ally_state": "宋", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "heal_hq", + "effect_params": {"amount": 3}, + "description": "恢复都城3HP", + "rarity": "common", + }, + "ally_song_chongji": { + "id": "ally_song_chongji", + "name": "宋国重骑", + "faction": "ally", + "ally_state": "宋", + "type": "unit", + "unit_type": "cavalry", + "cost": 4, + "op_cost": 1, + "attack": 4, + "defense": 2, + "max_hp": 2, + "description": "骑兵·冲锋", + "abilities": ["charge"], + "rarity": "rare", + }, + "ally_song_yishang": { + "id": "ally_song_yishang", + "name": "殷商遗礼", + "faction": "ally", + "ally_state": "宋", + "type": "order", + "cost": 3, + "op_cost": 0, + "effect_type": "heal_all", + "effect_params": {"amount": 2}, + "description": "所有友方单位恢复2HP", + "rarity": "rare", + }, + + # --- 吴 (Wu) --- + "ally_wu_wugou": { + "id": "ally_wu_wugou", + "name": "吴钩武士", + "faction": "ally", + "ally_state": "吴", + "type": "unit", + "unit_type": "infantry", + "cost": 2, + "op_cost": 1, + "attack": 3, + "defense": 1, + "max_hp": 2, + "description": "步兵·高攻", + "abilities": [], + "rarity": "common", + }, + "ally_wu_sunwu": { + "id": "ally_wu_sunwu", + "name": "孙武兵法", + "faction": "ally", + "ally_state": "吴", + "type": "order", + "cost": 3, + "op_cost": 0, + "effect_type": "damage_all_front", + "effect_params": {"damage": 2, "target": "enemy"}, + "description": "对所有敌方前线单位造成2伤害", + "rarity": "rare", + }, + "ally_wu_shuijun": { + "id": "ally_wu_shuijun", + "name": "吴国水军", + "faction": "ally", + "ally_state": "吴", + "type": "unit", + "unit_type": "archer", + "cost": 3, + "op_cost": 1, + "attack": 2, + "defense": 3, + "max_hp": 3, + "description": "弓手·可从营地射击", + "abilities": ["ranged"], + "rarity": "common", + }, + "ally_wu_wuzixu": { + "id": "ally_wu_wuzixu", + "name": "伍子胥复仇", + "faction": "ally", + "ally_state": "吴", + "type": "order", + "cost": 5, + "op_cost": 0, + "effect_type": "damage_hq", + "effect_params": {"damage": 4}, + "description": "对敌方都城造成4伤害", + "rarity": "legendary", + }, + + # --- 越 (Yue) --- + "ally_yue_nvjian": { + "id": "ally_yue_nvjian", + "name": "越女剑客", + "faction": "ally", + "ally_state": "越", + "type": "unit", + "unit_type": "infantry", + "cost": 2, + "op_cost": 1, + "attack": 2, + "defense": 2, + "max_hp": 2, + "description": "步兵·冲锋", + "abilities": ["charge"], + "rarity": "common", + }, + "ally_yue_woxin": { + "id": "ally_yue_woxin", + "name": "卧薪尝胆", + "faction": "ally", + "ally_state": "越", + "type": "order", + "cost": 1, + "op_cost": 0, + "effect_type": "draw_self_damage", + "effect_params": {"count": 1, "self_damage": 2}, + "description": "抽1牌,都城-2HP", + "rarity": "common", + }, + "ally_yue_sishi": { + "id": "ally_yue_sishi", + "name": "越国死士", + "faction": "ally", + "ally_state": "越", + "type": "unit", + "unit_type": "infantry", + "cost": 3, + "op_cost": 1, + "attack": 4, + "defense": 1, + "max_hp": 1, + "description": "步兵·无视报复", + "abilities": ["no_retaliation"], + "rarity": "rare", + }, + "ally_yue_jinggang": { + "id": "ally_yue_jinggang", + "name": "精钢剑", + "faction": "ally", + "ally_state": "越", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "buff_single", + "effect_params": {"attack_bonus": 2, "defense_bonus": 0, "duration": 1}, + "description": "一个友方单位+2攻击", + "rarity": "common", + }, + + # --- 郑 (Zheng) --- + "ally_zheng_xiangao": { + "id": "ally_zheng_xiangao", + "name": "弦高犒师", + "faction": "ally", + "ally_state": "郑", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "bounce", + "effect_params": {"target_type": "enemy_front"}, + "description": "将一个敌方前线单位返回手牌", + "rarity": "common", + }, + "ally_zheng_jianshi": { + "id": "ally_zheng_jianshi", + "name": "郑国剑士", + "faction": "ally", + "ally_state": "郑", + "type": "unit", + "unit_type": "infantry", + "cost": 2, + "op_cost": 1, + "attack": 2, + "defense": 3, + "max_hp": 3, + "description": "步兵", + "abilities": [], + "rarity": "common", + }, + "ally_zheng_zichan": { + "id": "ally_zheng_zichan", + "name": "子产治郑", + "faction": "ally", + "ally_state": "郑", + "type": "order", + "cost": 3, + "op_cost": 0, + "effect_type": "destroy_damaged", + "effect_params": {}, + "description": "消灭一个受伤的敌方单位", + "rarity": "rare", + }, + "ally_zheng_shangdui": { + "id": "ally_zheng_shangdui", + "name": "郑国商队", + "faction": "ally", + "ally_state": "郑", + "type": "unit", + "unit_type": "infantry", + "cost": 1, + "op_cost": 1, + "attack": 1, + "defense": 1, + "max_hp": 2, + "description": "部署时:获得3粮草", + "abilities": ["gain_on_deploy:3"], + "rarity": "common", + }, + + # --- 陈 (Chen) --- + "ally_chen_wuzhu": { + "id": "ally_chen_wuzhu", + "name": "宛丘巫祝", + "faction": "ally", + "ally_state": "陈", + "type": "unit", + "unit_type": "infantry", + "cost": 2, + "op_cost": 1, + "attack": 1, + "defense": 3, + "max_hp": 3, + "description": "回合开始时恢复友方1HP", + "abilities": ["heal_all:1"], + "rarity": "common", + }, + "ally_chen_maobing": { + "id": "ally_chen_maobing", + "name": "陈国矛兵", + "faction": "ally", + "ally_state": "陈", + "type": "unit", + "unit_type": "infantry", + "cost": 3, + "op_cost": 1, + "attack": 2, + "defense": 4, + "max_hp": 4, + "description": "步兵·高防御", + "abilities": [], + "rarity": "common", + }, + "ally_chen_wuyu": { + "id": "ally_chen_wuyu", + "name": "舞雩祭", + "faction": "ally", + "ally_state": "陈", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "heal_hq", + "effect_params": {"amount": 4}, + "description": "恢复都城4HP", + "rarity": "common", + }, + "ally_chen_wugu": { + "id": "ally_chen_wugu", + "name": "巫蛊之术", + "faction": "ally", + "ally_state": "陈", + "type": "order", + "cost": 3, + "op_cost": 0, + "effect_type": "damage", + "effect_params": {"damage": 3, "target_type": "enemy_unit"}, + "description": "对一个敌方单位造成3伤害", + "rarity": "rare", + }, + + # --- 蔡 (Cai) --- + "ally_cai_tongjian": { + "id": "ally_cai_tongjian", + "name": "蔡侯铜剑", + "faction": "ally", + "ally_state": "蔡", + "type": "order", + "cost": 2, + "op_cost": 0, + "effect_type": "damage", + "effect_params": {"damage": 3, "target_type": "enemy_unit"}, + "description": "对一个敌方单位造成3伤害", + "rarity": "common", + }, + "ally_cai_jingbing": { + "id": "ally_cai_jingbing", + "name": "蔡国精兵", + "faction": "ally", + "ally_state": "蔡", + "type": "unit", + "unit_type": "infantry", + "cost": 3, + "op_cost": 1, + "attack": 3, + "defense": 3, + "max_hp": 3, + "description": "步兵", + "abilities": [], + "rarity": "common", + }, + "ally_cai_shoushou": { + "id": "ally_cai_shoushou", + "name": "蔡国射手", + "faction": "ally", + "ally_state": "蔡", + "type": "unit", + "unit_type": "archer", + "cost": 3, + "op_cost": 1, + "attack": 2, + "defense": 2, + "max_hp": 2, + "description": "弓手·可从营地射击", + "abilities": ["ranged"], + "rarity": "common", + }, + "ally_cai_qingtong": { + "id": "ally_cai_qingtong", + "name": "青铜铸造", + "faction": "ally", + "ally_state": "蔡", + "type": "order", + "cost": 1, + "op_cost": 0, + "effect_type": "buff_single", + "effect_params": {"attack_bonus": 1, "defense_bonus": 1, "duration": 1}, + "description": "一个友方单位+1/+1", + "rarity": "common", + }, +} + +# ============================================================ +# KEYWORDS SYSTEM +# ============================================================ +KEYWORDS = { + "charge": {"name": "冲锋", "icon": "冲", "desc": "部署当回合可移动并攻击", "color": (200, 130, 45)}, + "ranged": {"name": "射击", "icon": "射", "desc": "可从营地攻击,不受报复", "color": (55, 115, 140)}, + "no_retaliation": {"name": "强袭", "icon": "强", "desc": "攻击时不受报复反击", "color": (170, 50, 40)}, + "siege": {"name": "攻城", "icon": "城", "desc": "对都城造成双倍伤害", "color": (160, 80, 40)}, + "draw_on_deploy": {"name": "部署抽牌", "icon": "抽", "desc": "部署时抽X张牌", "color": (50, 70, 140)}, + "damage_on_deploy": {"name": "部署伤害", "icon": "伤", "desc": "部署时对随机敌造成X伤害", "color": (180, 60, 50)}, + "gain_on_deploy": {"name": "部署获利", "icon": "获", "desc": "部署时获得X粮草", "color": (190, 160, 45)}, + "heal_all": {"name": "治疗", "icon": "愈", "desc": "回合开始恢复友方X HP", "color": (65, 130, 70)}, + "can_attack_support": {"name": "渗透", "icon": "渗", "desc": "可攻击敌方营地单位", "color": (90, 50, 130)}, +} + +# ============================================================ +# FACTION DEFINITIONS +# ============================================================ +FACTIONS = { + "qin": { + "id": "qin", + "name": "秦国", + "leader": "秦始皇", + "passive_id": "military_reward", + "passive_name": "军功爵制", + "passive_desc": "每消灭一个敌方单位,获得1粮草", + "capital_hp": 20, + }, + "qi": { + "id": "qi", + "name": "齐国", + "leader": "齐桓公", + "passive_id": "extra_provision", + "passive_name": "管仲之策", + "passive_desc": "每回合额外获得1粮草", + "capital_hp": 20, + }, + "chu": { + "id": "chu", + "name": "楚国", + "leader": "楚庄王", + "passive_id": "fortified_capital", + "passive_name": "楚国天险", + "passive_desc": "都城拥有25HP(而非20HP)", + "capital_hp": 25, + }, + "yan": { + "id": "yan", + "name": "燕国", + "leader": "燕昭王", + "passive_id": "cavalry_discount", + "passive_name": "突袭战术", + "passive_desc": "骑兵行动费用-1", + "capital_hp": 20, + }, + "han": { + "id": "han", + "name": "韩国", + "leader": "韩昭侯", + "passive_id": "archer_bonus", + "passive_name": "劲弩之术", + "passive_desc": "弓手对敌方都城额外造成1伤害", + "capital_hp": 20, + }, + "zhao": { + "id": "zhao", + "name": "赵国", + "leader": "赵武灵王", + "passive_id": "cavalry_power", + "passive_name": "胡服骑射", + "passive_desc": "骑兵+1攻击", + "capital_hp": 20, + }, + "wei": { + "id": "wei", + "name": "魏国", + "leader": "魏文侯", + "passive_id": "infantry_armor", + "passive_name": "魏武卒", + "passive_desc": "步兵+1防御", + "capital_hp": 20, + }, +} + +# ============================================================ +# DECK PRESETS (30 cards each) +# ============================================================ +DECK_PRESETS = { + "qin": { + "faction": "qin", + "cards": [ + "qin_tiesying", "qin_tiesying", "qin_tiesying", + "qin_qiangnu", "qin_qiangnu", "qin_qiangnu", + "qin_bubing", "qin_bubing", "qin_bubing", + "qin_qibing", "qin_qibing", + "qin_gongcheng", "qin_gongcheng", + "qin_shangyang", "qin_shangyang", + "qin_lianheng", + "qin_jiancu", "qin_jiancu", "qin_jiancu", + "qin_shihuang", + "neutral_miliao", "neutral_miliao", + "neutral_yimin", "neutral_yimin", "neutral_yimin", + "neutral_liangcao", "neutral_liangcao", "neutral_liangcao", + "neutral_yimin", + "neutral_liangcao", + ], + }, + "qi": { + "faction": "qi", + "cards": [ + "qi_jiji", "qi_jiji", "qi_jiji", + "qi_shangren", "qi_shangren", "qi_shangren", + "qi_gongshou", "qi_gongshou", "qi_gongshou", + "qi_tianqi", "qi_tianqi", + "qi_tongshang", "qi_tongshang", "qi_tongshang", + "qi_jixia", + "qi_sunbin", "qi_sunbin", + "qi_fuguo", + "neutral_miliao", "neutral_miliao", + "neutral_yimin", "neutral_yimin", "neutral_yimin", + "neutral_liangcao", "neutral_liangcao", "neutral_liangcao", + "neutral_yimin", "neutral_yimin", + "neutral_miliao", + "neutral_yimin", + ], + }, + "chu": { + "faction": "chu", + "cards": [ + "chu_manbing", "chu_manbing", "chu_manbing", + "chu_wuyi", "chu_wuyi", "chu_wuyi", + "chu_gongshou", "chu_gongshou", "chu_gongshou", + "chu_zhancha", "chu_zhancha", + "chu_gushou", "chu_gushou", "chu_gushou", + "chu_cici", + "chu_dazhao", "chu_dazhao", + "chu_xiangyu", + "neutral_miliao", "neutral_miliao", + "neutral_yimin", "neutral_yimin", "neutral_yimin", + "neutral_liangcao", "neutral_liangcao", "neutral_liangcao", + "neutral_yimin", "neutral_yimin", + "neutral_miliao", + "neutral_yimin", + ], + }, + "yan": { + "faction": "yan", + "cards": [ + "yan_qibing", "yan_qibing", "yan_qibing", + "yan_cike", "yan_cike", + "yan_bubing", "yan_bubing", "yan_bubing", + "yan_gongshou", "yan_gongshou", "yan_gongshou", + "yan_jingke", + "yan_jixing", "yan_jixing", "yan_jixing", + "yan_tuxi", "yan_tuxi", + "yan_yanzhao", "yan_yanzhao", + "neutral_yimin", "neutral_yimin", "neutral_yimin", + "neutral_liangcao", "neutral_liangcao", "neutral_liangcao", + "neutral_miliao", "neutral_miliao", + "neutral_miliao", + "neutral_yimin", "neutral_yimin", + ], + }, + "han": { + "faction": "han", + "cards": [ + "han_nubing", "han_nubing", "han_nubing", + "han_shushi", "han_shushi", "han_shushi", + "han_jianbing", "han_jianbing", "han_jianbing", + "han_qibing", "han_qibing", + "han_weiwei", "han_weiwei", + "han_fubing", + "han_liannu", "han_liannu", "han_liannu", + "han_shenjian", + "neutral_miliao", "neutral_miliao", + "neutral_yimin", "neutral_yimin", "neutral_yimin", + "neutral_liangcao", "neutral_liangcao", "neutral_liangcao", + "neutral_yimin", + "neutral_miliao", + "neutral_yimin", "neutral_yimin", + ], + }, + "zhao": { + "faction": "zhao", + "cards": [ + "zhao_bianqi", "zhao_bianqi", "zhao_bianqi", + "zhao_tieqi", "zhao_tieqi", + "zhao_bubing", "zhao_bubing", "zhao_bubing", + "zhao_gongshou", "zhao_gongshou", "zhao_gongshou", + "zhao_qixi", "zhao_qixi", "zhao_qixi", + "zhao_wuling", + "zhao_chongfeng", "zhao_chongfeng", + "zhao_lianpo", + "neutral_miliao", "neutral_miliao", + "neutral_yimin", "neutral_yimin", "neutral_yimin", + "neutral_liangcao", "neutral_liangcao", "neutral_liangcao", + "neutral_yimin", "neutral_yimin", + "neutral_miliao", + "neutral_yimin", + ], + }, + "wei": { + "faction": "wei", + "cards": [ + "wei_wuzu", "wei_wuzu", "wei_wuzu", + "wei_zhongzhuang", "wei_zhongzhuang", "wei_zhongzhuang", + "wei_gongshou", "wei_gongshou", "wei_gongshou", + "wei_qibing", "wei_qibing", + "wei_zhengjun", "wei_zhengjun", "wei_zhengjun", + "wei_diaodu", "wei_diaodu", "wei_diaodu", + "wei_lianbao", + "wei_weiliaozi", + "neutral_miliao", "neutral_miliao", + "neutral_liangcao", "neutral_liangcao", "neutral_liangcao", + "neutral_yimin", "neutral_yimin", "neutral_yimin", + "neutral_yimin", + "neutral_miliao", + "neutral_yimin", + ], + }, +} diff --git a/card_game/deck.py b/card_game/deck.py new file mode 100644 index 0000000..0ec42a3 --- /dev/null +++ b/card_game/deck.py @@ -0,0 +1,26 @@ +"""Deck class: build, shuffle, draw.""" + +import random +from card_game.card import Card + + +class Deck: + def __init__(self): + self.cards = [] + self.draw_pile = [] + + def build(self, card_id_list): + self.cards = [Card(cid) for cid in card_id_list] + self.draw_pile = list(self.cards) + random.shuffle(self.draw_pile) + + def draw(self): + if not self.draw_pile: + return None + return self.draw_pile.pop() + + def is_empty(self): + return len(self.draw_pile) == 0 + + def remaining(self): + return len(self.draw_pile) diff --git a/card_game/effects.py b/card_game/effects.py new file mode 100644 index 0000000..20452b3 --- /dev/null +++ b/card_game/effects.py @@ -0,0 +1,116 @@ +"""Visual effects: floating damage numbers, attack lines, ink splash.""" + +import math +import random + +import pygame + +from card_game.config import INK_BLACK, ZHU_HONG, TENG_HUANG + + +class FloatingText: + def __init__(self, x, y, text, color=ZHU_HONG, duration=60): + self.x = x + self.y = y + self.text = text + self.color = color + self.timer = duration + self.max_timer = duration + + def update(self): + self.y -= 0.5 + self.timer -= 1 + return self.timer > 0 + + def draw(self, surface, font): + alpha = min(255, int(255 * self.timer / self.max_timer)) + text_surf = font.render(self.text, True, self.color) + alpha_surf = pygame.Surface(text_surf.get_size(), pygame.SRCALPHA) + alpha_surf.fill((255, 255, 255, alpha)) + text_surf.blit(alpha_surf, (0, 0), special_flags=pygame.BLEND_RGBA_MULT) + surface.blit(text_surf, (self.x - text_surf.get_width() // 2, int(self.y))) + + +class AttackLine: + def __init__(self, x1, y1, x2, y2, duration=20): + self.x1, self.y1 = x1, y1 + self.x2, self.y2 = x2, y2 + self.timer = duration + self.max_timer = duration + + def update(self): + self.timer -= 1 + return self.timer > 0 + + def draw(self, surface): + progress = 1 - self.timer / self.max_timer + cx = self.x1 + (self.x2 - self.x1) * min(progress * 2, 1) + cy = self.y1 + (self.y2 - self.y1) * min(progress * 2, 1) + from card_game.ink_style import draw_brush_stroke + draw_brush_stroke(surface, (int(self.x1), int(self.y1)), + (int(cx), int(cy)), 3, TENG_HUANG, alpha=200) + + +class InkSplash: + def __init__(self, x, y, duration=30): + self.x = x + self.y = y + self.timer = duration + self.max_timer = duration + rng = random.Random(int(x * 100 + y)) + self.particles = [] + for _ in range(8): + angle = rng.uniform(0, 2 * math.pi) + speed = rng.uniform(1, 4) + size = rng.randint(3, 8) + self.particles.append((angle, speed, size)) + + def update(self): + self.timer -= 1 + return self.timer > 0 + + def draw(self, surface): + progress = 1 - self.timer / self.max_timer + alpha = max(0, int(200 * (1 - progress))) + + for angle, speed, size in self.particles: + dist = speed * progress * 30 + px = int(self.x + dist * math.cos(angle)) + py = int(self.y + dist * math.sin(angle)) + current_size = max(1, int(size * (1 - progress * 0.5))) + s = pygame.Surface((current_size * 2, current_size * 2), pygame.SRCALPHA) + pygame.draw.circle(s, (*INK_BLACK[:3], alpha), + (current_size, current_size), current_size) + surface.blit(s, (px - current_size, py - current_size)) + + +class EffectManager: + def __init__(self): + self.texts = [] + self.lines = [] + self.splashes = [] + + def add_damage(self, x, y, amount): + self.texts.append(FloatingText(x, y, f"-{amount}", ZHU_HONG)) + + def add_heal(self, x, y, amount): + self.texts.append(FloatingText(x, y, f"+{amount}", (70, 140, 80))) + + def add_attack_line(self, x1, y1, x2, y2): + self.lines.append(AttackLine(x1, y1, x2, y2)) + + def add_ink_splash(self, x, y): + self.splashes.append(InkSplash(x, y)) + + def update(self): + self.texts = [t for t in self.texts if t.update()] + self.lines = [l for l in self.lines if l.update()] + self.splashes = [s for s in self.splashes if s.update()] + + def draw(self, surface, font): + for line in self.lines: + line.draw(surface) + for splash in self.splashes: + splash.draw(surface) + for text in self.texts: + text.draw(surface, font) diff --git a/card_game/factions.py b/card_game/factions.py new file mode 100644 index 0000000..7a62bf7 --- /dev/null +++ b/card_game/factions.py @@ -0,0 +1,20 @@ +"""Faction ability system: applies passive bonuses.""" + + +def get_passive_bonus(faction_id, bonus_type): + """Get passive bonus value for a faction.""" + bonuses = { + "zhao": {"cavalry_attack": 1}, + "wei": {"infantry_defense": 1}, + } + return bonuses.get(faction_id, {}).get(bonus_type, 0) + + +def apply_faction_passive(unit, faction_id): + """Apply faction passive to a unit at deploy time.""" + if faction_id == "zhao" and unit.unit_type == "cavalry": + unit.attack += 1 + elif faction_id == "wei" and unit.unit_type == "infantry": + unit.defense += 1 + unit.max_hp += 1 + unit.current_hp += 1 diff --git a/card_game/ink_style.py b/card_game/ink_style.py new file mode 100644 index 0000000..bbc74bf --- /dev/null +++ b/card_game/ink_style.py @@ -0,0 +1,340 @@ +"""Ink painting style rendering primitives for Chinese aesthetic.""" + +import random +import math +import pygame + +from card_game.config import ( + WINDOW_WIDTH, WINDOW_HEIGHT, + INK_BLACK, PAPER_WHITE, ZHU_HONG, SONGHUA_GREEN, TENG_HUANG, GOLD, + BG_COLOR, INK_WASH_1, INK_WASH_2, INK_WASH_3, INK_WASH_4, INK_WASH_5, +) + +# Cached surfaces +_paper_texture = None +_mountain_layers = None + + +def init_cache(): + """Pre-generate cached surfaces. Call once after pygame.init().""" + global _paper_texture, _mountain_layers + _paper_texture = _generate_paper_texture(WINDOW_WIDTH, WINDOW_HEIGHT) + _mountain_layers = _generate_mountain_layers(WINDOW_WIDTH, WINDOW_HEIGHT) + + +def get_paper_texture(): + return _paper_texture + + +def get_mountain_layers(): + return _mountain_layers + + +def _generate_paper_texture(w, h): + """Generate a rice paper (宣纸) texture surface.""" + surf = pygame.Surface((w, h)) + surf.fill(BG_COLOR) + + # Add subtle noise + rng = random.Random(42) + for y in range(0, h, 2): + for x in range(0, w, 2): + noise = rng.gauss(0, 6) + r = min(255, max(0, int(BG_COLOR[0] + noise))) + g = min(255, max(0, int(BG_COLOR[1] + noise))) + b = min(255, max(0, int(BG_COLOR[2] + noise))) + surf.set_at((x, y), (r, g, b)) + if x + 1 < w: + surf.set_at((x + 1, y), (r, g, b)) + if y + 1 < h: + surf.set_at((x, y + 1), (r, g, b)) + if x + 1 < w and y + 1 < h: + surf.set_at((x + 1, y + 1), (r, g, b)) + + # Add fiber lines + for _ in range(40): + x1 = rng.randint(0, w) + y1 = rng.randint(0, h) + length = rng.randint(30, 150) + angle = rng.uniform(0, math.pi) + color = (rng.randint(200, 220), rng.randint(185, 205), rng.randint(160, 180)) + points = [] + for i in range(10): + t = i / 9 + px = int(x1 + length * t * math.cos(angle) + rng.gauss(0, 2)) + py = int(y1 + length * t * math.sin(angle) + rng.gauss(0, 2)) + points.append((px, py)) + if len(points) >= 2: + pygame.draw.lines(surf, color, False, points, 1) + + return surf + + +def _generate_mountain_layers(w, h): + """Generate 3 layers of misty mountain silhouettes.""" + rng = random.Random(123) + layers = [] + layer_colors = [ + (INK_WASH_1[0], INK_WASH_1[1], INK_WASH_1[2], 60), + (INK_WASH_2[0], INK_WASH_2[1], INK_WASH_2[2], 45), + (INK_WASH_3[0], INK_WASH_3[1], INK_WASH_3[2], 30), + ] + base_heights = [h * 0.55, h * 0.62, h * 0.70] + + for i, (color, base_y) in enumerate(zip(layer_colors, base_heights)): + surf = pygame.Surface((w, h), pygame.SRCALPHA) + points = [(0, h)] + x = 0 + while x <= w: + freq1 = rng.uniform(0.003, 0.008) + freq2 = rng.uniform(0.01, 0.02) + amp1 = rng.uniform(30, 60) + amp2 = rng.uniform(10, 25) + y = base_y + amp1 * math.sin(x * freq1 + i) + amp2 * math.sin(x * freq2 + i * 2) + points.append((x, int(y))) + x += rng.randint(8, 20) + points.append((w, h)) + + if len(points) >= 3: + pygame.draw.polygon(surf, color, points) + layers.append(surf) + + return layers + + +def blit_paper_background(surface): + """Blit the cached paper texture onto the surface.""" + if _paper_texture: + surface.blit(_paper_texture, (0, 0)) + else: + surface.fill(BG_COLOR) + + +def blit_mountains(surface): + """Blit mountain layers onto the surface.""" + if _mountain_layers: + for layer in _mountain_layers: + surface.blit(layer, (0, 0)) + + +def draw_ink_rect(surface, rect, color, alpha=255, border_radius=0): + """Draw a rectangle with slightly wobbly brush-stroke edges.""" + x, y, w, h = rect if isinstance(rect, (list, tuple)) else (rect.x, rect.y, rect.w, rect.h) + + s = pygame.Surface((w, h), pygame.SRCALPHA) + base_color = (*color[:3], alpha) + + # Draw base rect with slight gradient + for col in range(w): + gradient = 1.0 - 0.1 * (col / max(w, 1)) + r = min(255, max(0, int(color[0] * gradient))) + g = min(255, max(0, int(color[1] * gradient))) + b = min(255, max(0, int(color[2] * gradient))) + pygame.draw.line(s, (r, g, b, alpha), (col, 0), (col, h - 1)) + + # Feather edges + rng = random.Random(x * 1000 + y) + for edge_x in range(min(3, w)): + for edge_y in range(h): + if rng.random() < 0.3: + s.set_at((edge_x, edge_y), (0, 0, 0, 0)) + if rng.random() < 0.3: + s.set_at((w - 1 - edge_x, edge_y), (0, 0, 0, 0)) + for edge_y in range(min(3, h)): + for edge_x in range(w): + if rng.random() < 0.3: + s.set_at((edge_x, edge_y), (0, 0, 0, 0)) + if rng.random() < 0.3: + s.set_at((edge_x, h - 1 - edge_y), (0, 0, 0, 0)) + + surface.blit(s, (x, y)) + + +def draw_ink_circle(surface, center, radius, color, alpha=200): + """Draw a circle with irregular ink-wash edges.""" + cx, cy = center + size = radius * 2 + 4 + s = pygame.Surface((size, size), pygame.SRCALPHA) + scx, scy = size // 2, size // 2 + + rng = random.Random(cx * 100 + cy) + n = max(12, radius) + for i in range(n): + angle = 2 * math.pi * i / n + r = radius + rng.gauss(0, max(1, radius * 0.08)) + px = int(scx + r * math.cos(angle)) + py = int(scy + r * math.sin(angle)) + pygame.draw.circle(s, (*color[:3], alpha), (px, py), max(2, int(radius * 0.4))) + + # Fill center + pygame.draw.circle(s, (*color[:3], alpha), (scx, scy), max(1, radius - 2)) + + surface.blit(s, (cx - size // 2, cy - size // 2)) + + +def draw_brush_stroke(surface, start, end, width, color, alpha=180): + """Draw a thick-to-thin brush stroke line.""" + x1, y1 = start + x2, y2 = end + dx, dy = x2 - x1, y2 - y1 + length = math.sqrt(dx * dx + dy * dy) + if length < 1: + return + + steps = max(int(length / 2), 4) + rng = random.Random(int(x1 * 100 + y1)) + + # Perpendicular direction for jitter + nx, ny = -dy / length, dx / length + + for i in range(steps): + t = i / (steps - 1) + # Width tapers: starts thin, peaks middle, ends thin + w = width * (1 - abs(2 * t - 1) * 0.6) * (0.8 + 0.2 * rng.random()) + px = x1 + dx * t + nx * rng.gauss(0, 1.5) + py = y1 + dy * t + ny * rng.gauss(0, 1.5) + a = max(50, int(alpha * (0.7 + 0.3 * rng.random()))) + circle_s = pygame.Surface((int(w * 2 + 4), int(w * 2 + 4)), pygame.SRCALPHA) + pygame.draw.circle(circle_s, (*color[:3], a), + (int(w + 2), int(w + 2)), max(1, int(w))) + surface.blit(circle_s, (int(px - w - 2), int(py - w - 2))) + + +def draw_seal_stamp(surface, rect, text, font, color=None): + """Draw a traditional Chinese seal (印章): red square with white text.""" + if color is None: + color = ZHU_HONG + x, y, w, h = rect if isinstance(rect, (list, tuple)) else (rect.x, rect.y, rect.w, rect.h) + + s = pygame.Surface((w, h), pygame.SRCALPHA) + + # Red background with slight irregularity + rng = random.Random(x * 100 + y + w) + pygame.draw.rect(s, color, (0, 0, w, h)) + # Feather edges slightly + for edge in range(2): + for i in range(w): + if rng.random() < 0.25: + s.set_at((i, edge), (0, 0, 0, 0)) + s.set_at((i, h - 1 - edge), (0, 0, 0, 0)) + for i in range(h): + if rng.random() < 0.25: + s.set_at((edge, i), (0, 0, 0, 0)) + s.set_at((w - 1 - edge, i), (0, 0, 0, 0)) + + # White border + pygame.draw.rect(s, (255, 255, 255), (0, 0, w, h), 2) + + # Text in white + text_surf = font.render(text, True, (255, 255, 255)) + tx = (w - text_surf.get_width()) // 2 + ty = (h - text_surf.get_height()) // 2 + s.blit(text_surf, (tx, ty)) + + surface.blit(s, (x, y)) + + +def draw_scroll(surface, rect, scroll_color=None): + """Draw a scroll/卷轴 shape with wooden rollers.""" + if scroll_color is None: + scroll_color = (210, 195, 170) + x, y, w, h = rect if isinstance(rect, (list, tuple)) else (rect.x, rect.y, rect.w, rect.h) + + roller_h = 6 + roller_color = (140, 90, 50) + + # Top roller + pygame.draw.rect(surface, roller_color, (x - 4, y, w + 8, roller_h), border_radius=3) + pygame.draw.rect(surface, (100, 65, 35), (x - 4, y, w + 8, roller_h), 1, border_radius=3) + + # Paper body + body_rect = pygame.Rect(x, y + roller_h, w, h - roller_h * 2) + draw_ink_rect(surface, body_rect, scroll_color, alpha=240) + + # Bottom roller + by = y + h - roller_h + pygame.draw.rect(surface, roller_color, (x - 4, by, w + 8, roller_h), border_radius=3) + pygame.draw.rect(surface, (100, 65, 35), (x - 4, by, w + 8, roller_h), 1, border_radius=3) + + +def draw_ink_text(surface, text, pos, font, color, shadow=True): + """Render text with a subtle ink bleed shadow.""" + if shadow: + shadow_color = (max(0, color[0] - 40), max(0, color[1] - 40), max(0, color[2] - 40)) + shadow_surf = font.render(text, True, shadow_color) + surface.blit(shadow_surf, (pos[0] + 1, pos[1] + 1)) + text_surf = font.render(text, True, color) + surface.blit(text_surf, pos) + return text_surf + + +def draw_cloud_pattern(surface, rect): + """Draw traditional Chinese cloud motifs (祥云) as decoration.""" + x, y, w, h = rect if isinstance(rect, (list, tuple)) else (rect.x, rect.y, rect.w, rect.h) + color = (*INK_WASH_2[:3], 80) + + s = pygame.Surface((w, h), pygame.SRCALPHA) + rng = random.Random(x * 7 + y) + n_clouds = max(1, w // 80) + + for i in range(n_clouds): + cx = rng.randint(10, w - 10) + cy = rng.randint(5, h - 5) + # Simple cloud: overlapping arcs + for j in range(3): + r = rng.randint(6, 12) + ox = j * 8 - 8 + oy = rng.randint(-3, 3) + pygame.draw.circle(s, color, (cx + ox, cy + oy), r) + + surface.blit(s, (x, y)) + + +def draw_zone_bg(surface, rect, base_color, accent_color=None): + """Draw a battlefield zone background with ink wash effect.""" + x, y, w, h = rect if isinstance(rect, (list, tuple)) else (rect.x, rect.y, rect.w, rect.h) + + s = pygame.Surface((w, h), pygame.SRCALPHA) + + # Base fill + s.fill((*base_color[:3], 120)) + + # Subtle horizontal brush strokes + rng = random.Random(y) + for _ in range(5): + by = rng.randint(0, h) + bx = rng.randint(0, w // 4) + bw = rng.randint(w // 3, w) + bh = rng.randint(2, 6) + bc = (*INK_WASH_2[:3], rng.randint(15, 35)) + pygame.draw.rect(s, bc, (bx, by, bw, bh)) + + surface.blit(s, (x, y)) + + # Border line + if accent_color: + pygame.draw.line(surface, accent_color, (x, y + h - 1), (x + w, y + h - 1), 1) + + +def draw_ink_hp_bar(surface, x, y, w, h, ratio, bg_color=None): + """Draw an HP bar with ink brush style.""" + if bg_color is None: + bg_color = INK_WASH_4 + pygame.draw.rect(surface, bg_color, (x, y, w, h)) + + if ratio > 0: + bar_w = max(1, int(w * ratio)) + if ratio > 0.5: + bar_color = SONGHUA_GREEN + elif ratio > 0.25: + bar_color = TENG_HUANG + else: + bar_color = ZHU_HONG + + # Brush-stroke bar + rng = random.Random(x * 100 + y) + for px in range(bar_w): + thickness = h - rng.randint(0, 1) + pygame.draw.line(surface, bar_color, + (x + px, y + (h - thickness) // 2), + (x + px, y + (h + thickness) // 2)) diff --git a/card_game/main.py b/card_game/main.py new file mode 100644 index 0000000..c9a455e --- /dev/null +++ b/card_game/main.py @@ -0,0 +1,549 @@ +"""Game class: main loop, state machine, event handling.""" + +import sys +import os +import json +import random +import pygame +from card_game.config import WINDOW_WIDTH, WINDOW_HEIGHT, FPS, TENG_HUANG, INK_WASH_3 +from card_game.battlefield import Battlefield +from card_game.ui import UI +from card_game.ai import AIPlayer +from card_game.factions import apply_faction_passive +from card_game import ink_style + +SAVED_DECKS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "saved_decks") + + +class Game: + def __init__(self): + pygame.init() + self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) + pygame.display.set_caption("战国卡牌 - 水墨风云") + self.clock = pygame.time.Clock() + + # Initialize ink style cache + ink_style.init_cache() + + self.ui = UI(self.screen) + self.ai_player = None + + self.state = "menu" + self.battlefield = None + self.player_faction = None + self.ai_faction = None + self.custom_deck = None + self._load_custom_deck() + self.custom_deck = None + + # AI turn timing + self.ai_timer = 0 + self.ai_step = 0 + + def _load_custom_deck(self): + if not self.player_faction: + return + path = os.path.join(SAVED_DECKS_DIR, f"{self.player_faction}.json") + if os.path.exists(path): + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + if len(data.get("cards", [])) == 30: + self.custom_deck = data["cards"] + except Exception: + self.custom_deck = None + + def _save_custom_deck(self, faction_id, cards): + os.makedirs(SAVED_DECKS_DIR, exist_ok=True) + path = os.path.join(SAVED_DECKS_DIR, f"{faction_id}.json") + try: + with open(path, "w", encoding="utf-8") as f: + json.dump({"faction": faction_id, "cards": cards}, f, ensure_ascii=False, indent=2) + self.custom_deck = list(cards) + except Exception: + pass + + def run(self): + while True: + try: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + elif event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + self._handle_click(event.pos) + elif event.button == 3: + self._handle_right_click(event.pos) + elif event.type == pygame.MOUSEMOTION: + self._handle_hover(event.pos) + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + self._handle_escape() + + self._update() + self._draw() + pygame.display.flip() + self.clock.tick(FPS) + except Exception as e: + import traceback + traceback.print_exc() + self.screen.fill((0, 0, 0)) + font = pygame.font.SysFont("microsoftyahei", 16) + lines = traceback.format_exc().split('\n') + for i, line in enumerate(lines[:15]): + surf = font.render(line[:80], True, (255, 80, 80)) + self.screen.blit(surf, (10, 10 + i * 20)) + pygame.display.flip() + waiting = True + while waiting: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + if event.type == pygame.KEYDOWN: + waiting = False + + # --- State: Menu --- + + def _handle_click_menu(self, pos): + fid = self.ui.get_faction_at(pos) + if fid: + self.player_faction = fid + self.custom_deck = None + self._load_custom_deck() + self.state = "deck_select" + + # --- State: Deck Select --- + + def _handle_click_deck_select(self, pos): + if "back" in self.ui.menu_buttons and self.ui.menu_buttons["back"].collidepoint(pos): + self.state = "menu" + return + if "deck_build" in self.ui.menu_buttons and self.ui.menu_buttons["deck_build"].collidepoint(pos): + self.ui.deck_builder_cards = [] + self.ui.deck_builder_faction = self.player_faction + self.state = "deck_build" + return + fid = self.ui.get_faction_at(pos) + if fid and fid != self.player_faction: + self.ai_faction = fid + self._start_game() + + # --- State: Deck Build --- + + def _handle_click_deck_build(self, pos): + from card_game.config import DECK_SIZE, RARITY_LIMITS, CARD_DATABASE + + if "back" in self.ui.menu_buttons and self.ui.menu_buttons["back"].collidepoint(pos): + self.state = "deck_select" + return + if "clear" in self.ui.menu_buttons and self.ui.menu_buttons["clear"].collidepoint(pos): + self.ui.deck_builder_cards = [] + return + if "preset" in self.ui.menu_buttons and self.ui.menu_buttons["preset"].collidepoint(pos): + from card_game.config import DECK_PRESETS + self.ui.deck_builder_cards = list(DECK_PRESETS[self.player_faction]["cards"]) + return + if "confirm" in self.ui.menu_buttons and self.ui.menu_buttons["confirm"].collidepoint(pos): + if len(self.ui.deck_builder_cards) == DECK_SIZE: + self._save_custom_deck(self.player_faction, self.ui.deck_builder_cards) + self.state = "deck_select" + return + cid = self.ui.get_deck_card_at(pos) + if cid: + card_data = CARD_DATABASE[cid] + in_deck = self.ui.deck_builder_cards.count(cid) + max_copies = RARITY_LIMITS.get(card_data["rarity"], 3) + if in_deck < max_copies and len(self.ui.deck_builder_cards) < DECK_SIZE: + self.ui.deck_builder_cards.append(cid) + return + cid = self.ui.get_deck_build_card_at(pos) + if cid: + card_data = CARD_DATABASE[cid] + in_deck = self.ui.deck_builder_cards.count(cid) + max_copies = RARITY_LIMITS.get(card_data["rarity"], 3) + if in_deck < max_copies and len(self.ui.deck_builder_cards) < DECK_SIZE: + self.ui.deck_builder_cards.append(cid) + + def _handle_right_click(self, pos): + if self.state != "deck_build": + return + cid = self.ui.get_deck_card_at(pos) + if cid and cid in self.ui.deck_builder_cards: + self.ui.deck_builder_cards.remove(cid) + return + cid = self.ui.get_deck_build_card_at(pos) + if cid and cid in self.ui.deck_builder_cards: + self.ui.deck_builder_cards.remove(cid) + + def _start_game(self): + self.battlefield = Battlefield(self.player_faction, self.ai_faction) + if self.custom_deck: + self.battlefield.player.deck.build(self.custom_deck) + self.battlefield.player.hand = [] + for _ in range(4): + self.battlefield.player.draw_card() + self.battlefield.start_game() + self.ai_player = AIPlayer(self.battlefield) + self.ui.clear_selection() + self.state = "playing" + + # --- State: Playing --- + + def _handle_click_playing(self, pos): + bf = self.battlefield + player = bf.player + + if self.ui.end_turn_btn.collidepoint(pos): + self.ui.clear_selection() + self._start_ai_turn() + return + + if self.ui.target_mode == "order_target" and self.ui.selected_card: + self._handle_order_target_click(pos) + return + + if self.ui.target_mode == "deploy" and self.ui.selected_card: + if self._handle_deploy_click(pos): + return + self.ui.clear_selection() + return + + if self.ui.target_mode == "move" and self.ui.selected_unit: + self._handle_move_click(pos) + return + + if self.ui.selected_unit and self.ui.target_mode == "attack": + self._handle_attack_click(pos) + return + + card = self.ui.get_hand_card_at(pos, player) + if card: + self._select_hand_card(card) + return + + unit, owner = self.ui.get_field_unit_at(pos, bf) + if unit and owner == player: + self._select_own_unit(unit) + return + + self.ui.clear_selection() + + def _select_hand_card(self, card): + player = self.battlefield.player + self.ui.clear_selection() + + if not player.can_play_card(card): + return + + if card.card_type == "unit": + self.ui.selected_card = card + self.ui.target_mode = "deploy" + self.ui.valid_targets = [] + elif card.card_type == "order": + if self.battlefield.needs_target(card): + self.ui.selected_card = card + self.ui.target_mode = "order_target" + self.ui.valid_targets = self.battlefield.get_valid_targets(card, player) + else: + self.battlefield.apply_order_effect(card, player) + player.play_order(card) + self.ui.clear_selection() + + def _select_own_unit(self, unit): + self.ui.clear_selection() + player = self.battlefield.player + owner = self.battlefield._get_unit_owner(unit) + if owner != player: + return + + opponent = self.battlefield.get_opponent(player) + + if unit.zone == "support": + if unit.can_attack and not unit.has_attacked and unit.is_ranged(): + self.ui.selected_unit = unit + self.ui.target_mode = "attack" + targets = list(opponent.get_frontline_units()) + targets.append(("capital", opponent)) + self.ui.valid_targets = targets + return + if self.battlefield.can_move_to_frontline(player): + op_cost = player._get_op_cost(unit) + if player.provisions >= op_cost: + self.ui.selected_unit = unit + self.ui.target_mode = "move" + elif unit.zone == "frontline": + if unit.can_attack and not unit.has_attacked: + self.ui.selected_unit = unit + self.ui.target_mode = "attack" + targets = list(opponent.get_frontline_units()) + targets.append(("capital", opponent)) + self.ui.valid_targets = targets + + def _handle_deploy_click(self, pos): + from card_game.config import PLAYER_SUPPORT_Y, ZONE_HEIGHT + player = self.battlefield.player + card = self.ui.selected_card + if not card: + return False + + mx, my = pos + in_support_zone = (PLAYER_SUPPORT_Y <= my <= PLAYER_SUPPORT_Y + ZONE_HEIGHT) + + if in_support_zone: + slot = self.ui.get_support_slot_at(pos, player) + if slot < 0: + for i, s in enumerate(player.support_line): + if s is None: + slot = i + break + if slot >= 0: + apply_faction_passive(card, player.faction_id) + player.deploy_unit(card, slot) + self._handle_deploy_abilities(card, player) + self.ui.clear_selection() + return True + return False + + def _handle_order_target_click(self, pos): + card = self.ui.selected_card + player = self.battlefield.player + if not card: + self.ui.clear_selection() + return + + unit, owner = self.ui.get_field_unit_at(pos, self.battlefield) + if unit and unit in self.ui.valid_targets: + self.battlefield.apply_order_effect(card, player, unit) + player.play_order(card) + self.ui.clear_selection() + return + + if self.ui.get_enemy_capital_at(pos, self.battlefield.ai): + self.battlefield.apply_order_effect(card, player, "capital") + player.play_order(card) + self.ui.clear_selection() + + def _handle_move_click(self, pos): + player = self.battlefield.player + unit = self.ui.selected_unit + if not unit: + self.ui.clear_selection() + return + + if not self.battlefield.can_move_to_frontline(player): + self.ui.clear_selection() + return + + slot = self.ui.get_frontline_slot_at(pos, player) + if slot >= 0: + result_slot = player.move_to_frontline(unit) + if result_slot >= 0: + if self.battlefield.frontline_controller is None: + self.battlefield.claim_frontline(player) + self.ui.clear_selection() + else: + self.ui.clear_selection() + + def _handle_attack_click(self, pos): + unit = self.ui.selected_unit + if not unit: + self.ui.clear_selection() + return + + bf = self.battlefield + player = bf.player + opponent = bf.ai + valid = self.ui.valid_targets + + target_unit, target_owner = self.ui.get_field_unit_at(pos, bf) + if target_unit and target_owner == opponent and target_unit in valid: + dead = bf.resolve_attack(unit, target_unit) + self._add_attack_effect(unit, target_unit) + self.ui.clear_selection() + return + + if self.ui.get_enemy_capital_at(pos, opponent): + cap_target = ("capital", opponent) + if cap_target in valid: + bf.attack_capital(unit) + self.ui.clear_selection() + return + + self.ui.clear_selection() + + def _handle_deploy_abilities(self, card, player): + for ability in card.abilities: + if ability.startswith("draw_on_deploy:"): + count = int(ability.split(":")[1]) + for _ in range(count): + player.draw_card() + elif ability.startswith("damage_on_deploy:"): + dmg = int(ability.split(":")[1]) + import random + opponent = self.battlefield.get_opponent(player) + targets = opponent.get_all_units() + if targets: + target = random.choice(targets) + target.take_damage(dmg) + elif ability.startswith("gain_on_deploy:"): + amount = int(ability.split(":")[1]) + player.provisions += amount + + def _add_attack_effect(self, attacker, target): + from card_game.ui import _support_slot_x, _frontline_slot_x + from card_game.config import (FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT, ZONE_HEIGHT, + ENEMY_SUPPORT_Y, FRONTLINE_Y, PLAYER_SUPPORT_Y, WINDOW_WIDTH, + MAX_FRONTLINE_SLOTS, CAPITAL_WIDTH) + half = ZONE_HEIGHT // 2 + n_fl = MAX_FRONTLINE_SLOTS + + def _unit_pos(unit): + owner = self.battlefield._get_unit_owner(unit) + is_ai = (owner == self.battlefield.ai) + if unit.zone == "support": + x = _support_slot_x(unit.slot) + FIELD_CARD_WIDTH // 2 + zone_y = ENEMY_SUPPORT_Y if is_ai else PLAYER_SUPPORT_Y + y = zone_y + ZONE_HEIGHT // 2 + else: + x = _frontline_slot_x(unit.slot, n_fl) + FIELD_CARD_WIDTH // 2 + zone_y = FRONTLINE_Y if is_ai else (FRONTLINE_Y + half) + y = zone_y + half // 2 + return x, y + + try: + if hasattr(attacker, 'zone'): + ax, ay = _unit_pos(attacker) + else: + ax, ay = WINDOW_WIDTH // 2, ENEMY_SUPPORT_Y + ZONE_HEIGHT // 2 + + if hasattr(target, 'zone'): + tx, ty = _unit_pos(target) + else: + tx = WINDOW_WIDTH // 2 + ty = ENEMY_SUPPORT_Y + ZONE_HEIGHT // 2 + + self.ui.effects.add_attack_line(ax, ay, tx, ty) + self.ui.effects.add_damage(tx, ty - 20, attacker.get_effective_attack()) + except Exception: + pass + + # --- State: AI Turn --- + + def _start_ai_turn(self): + self.state = "ai_turn" + try: + self.battlefield.start_ai_turn() + self.ai_actions = self.ai_player.execute_turn() + except Exception as e: + import traceback + traceback.print_exc() + self.ai_actions = [] + self.ai_step = 0 + self.ai_timer = 20 + + def _update_ai_turn(self): + if self.ai_timer > 0: + self.ai_timer -= 1 + return + + if self.ai_step >= len(self.ai_actions): + try: + self.battlefield.end_ai_turn() + except Exception: + pass + if self.battlefield.game_over: + self.state = "game_over" + else: + self.state = "playing" + return + + action = self.ai_actions[self.ai_step] + self.ai_step += 1 + self.ai_timer = 15 + + if action[0] in ("attack_unit", "attack_capital"): + attacker = action[1] + try: + if len(action) > 2: + self._add_attack_effect(attacker, action[2]) + else: + self._add_attack_effect(attacker, "capital") + except Exception: + pass + + # --- Hover --- + + def _handle_hover(self, pos): + if self.state not in ("playing", "ai_turn"): + return + if not self.battlefield: + return + player = self.battlefield.player + card = self.ui.get_hand_card_at(pos, player) + self.ui.hover_card = card + unit, owner = self.ui.get_field_unit_at(pos, self.battlefield) + self.ui.hover_field_unit = unit + self.ui.hover_pos = pos + + # --- Escape --- + + def _handle_escape(self): + self.ui.clear_selection() + if self.state in ("playing", "ai_turn"): + self.state = "menu" + self.battlefield = None + + # --- State: Game Over --- + + def _handle_click_game_over(self, pos): + if "restart" in self.ui.menu_buttons and self.ui.menu_buttons["restart"].collidepoint(pos): + self._start_game() + elif "menu" in self.ui.menu_buttons and self.ui.menu_buttons["menu"].collidepoint(pos): + self.state = "menu" + self.battlefield = None + + # --- Unified handlers --- + + def _handle_click(self, pos): + if self.state == "menu": + self._handle_click_menu(pos) + elif self.state == "deck_select": + self._handle_click_deck_select(pos) + elif self.state == "deck_build": + self._handle_click_deck_build(pos) + elif self.state == "playing": + self._handle_click_playing(pos) + elif self.state == "game_over": + self._handle_click_game_over(pos) + + def _update(self): + if self.state == "ai_turn": + self._update_ai_turn() + if self.battlefield: + self.battlefield.update_effects() + self.ui.effects.update() + + def _draw(self): + if self.state == "menu": + self.ui.draw_menu() + elif self.state == "deck_select": + self.ui.draw_deck_select(self.player_faction) + elif self.state == "deck_build": + self.ui.draw_deck_builder(self.player_faction) + elif self.state in ("playing", "ai_turn"): + self.ui.draw_game(self.battlefield) + if self.state == "ai_turn": + from card_game.config import WINDOW_WIDTH, ENEMY_INFO_HEIGHT + text = self.ui.font_md.render("对手回合...", True, INK_WASH_3) + self.screen.blit(text, (WINDOW_WIDTH // 2 - text.get_width() // 2, + ENEMY_INFO_HEIGHT + 55)) + elif self.state == "game_over": + self.ui.draw_game(self.battlefield) + self.ui.draw_game_over(self.battlefield.winner, self.battlefield) + + +if __name__ == "__main__": + game = Game() + game.run() diff --git a/card_game/player.py b/card_game/player.py new file mode 100644 index 0000000..ada2569 --- /dev/null +++ b/card_game/player.py @@ -0,0 +1,187 @@ +"""Player class: manages hand, support line, frontline, HP, provisions.""" + +from card_game.config import ( + STARTING_CAPITAL_HP, MAX_PROVISIONS, MAX_HAND_SIZE, + MAX_SUPPORT_SLOTS, MAX_FRONTLINE_SLOTS, DECK_SIZE, STARTING_HAND_SIZE, + FATIGUE_START_DAMAGE, FACTIONS, DECK_PRESETS, +) +from card_game.deck import Deck +from card_game.card import Card + + +class Player: + def __init__(self, faction_id, is_ai=False): + faction = FACTIONS[faction_id] + self.faction_id = faction_id + self.faction = faction + self.is_ai = is_ai + + self.capital_hp = faction["capital_hp"] + self.max_capital_hp = faction["capital_hp"] + self.provisions = 0 + self.max_provisions = 0 + + self.hand = [] + self.support_line = [None] * MAX_SUPPORT_SLOTS + self.frontline = [None] * MAX_FRONTLINE_SLOTS + + self.deck = Deck() + self.fatigue_damage = 0 + self.turn_number = 0 + + def build_deck(self): + preset = DECK_PRESETS[self.faction_id] + self.deck.build(preset["cards"]) + + def start_game(self): + self.build_deck() + for _ in range(STARTING_HAND_SIZE): + self.draw_card() + + def start_turn(self, turn_number): + self.turn_number = turn_number + gained = min(turn_number + 1, MAX_PROVISIONS) + self.provisions = gained + self.max_provisions = gained + + # Faction passive: Qi extra provision + if self.faction_id == "qi": + self.provisions += 1 + + self.draw_card() + + # Reset unit flags + for unit in self.get_all_units(): + unit.reset_turn_flags() + if unit.zone == "frontline" and unit.turn_played < turn_number: + unit.can_attack = True + if unit.zone == "support" and unit.is_ranged() and unit.turn_played < turn_number: + unit.can_attack = True + + # Chu healer + if self.faction_id == "chu": + self._apply_heal_ability() + + def draw_card(self): + if len(self.hand) >= MAX_HAND_SIZE: + return + card = self.deck.draw() + if card: + card.zone = "hand" + self.hand.append(card) + else: + self.fatigue_damage += 1 + self.capital_hp -= self.fatigue_damage + + def can_play_card(self, card): + if card.cost > self.provisions: + return False + if card.card_type == "unit": + return any(s is None for s in self.support_line) + return True + + def deploy_unit(self, card, slot=-1): + if slot < 0: + for i, s in enumerate(self.support_line): + if s is None: + slot = i + break + if slot < 0 or slot >= len(self.support_line): + return None + self.support_line[slot] = card + card.zone = "support" + card.slot = slot + card.turn_played = self.turn_number + self.provisions -= card.cost + self.hand.remove(card) + return slot + + def play_order(self, card): + self.provisions -= card.cost + self.hand.remove(card) + + def move_to_frontline(self, unit): + op_cost = self._get_op_cost(unit) + if self.provisions < op_cost: + return -1 + slot = -1 + for i, s in enumerate(self.frontline): + if s is None: + slot = i + break + if slot < 0: + return -1 + self.support_line[unit.slot] = None + self.frontline[slot] = unit + unit.zone = "frontline" + unit.slot = slot + unit.has_moved = True + self.provisions -= op_cost + if unit.turn_played < self.turn_number: + if unit.can_move_and_attack(): + unit.can_attack = True + return slot + + def move_to_frontline_free(self, unit): + slot = -1 + for i, s in enumerate(self.frontline): + if s is None: + slot = i + break + if slot < 0: + return -1 + self.support_line[unit.slot] = None + self.frontline[slot] = unit + unit.zone = "frontline" + unit.slot = slot + unit.has_moved = True + if unit.turn_played < self.turn_number: + unit.can_attack = True + return slot + + def remove_unit(self, unit): + if unit.zone == "support": + if 0 <= unit.slot < len(self.support_line): + self.support_line[unit.slot] = None + elif unit.zone == "frontline": + if 0 <= unit.slot < len(self.frontline): + self.frontline[unit.slot] = None + + def get_all_units(self): + units = [] + for u in self.support_line: + if u is not None: + units.append(u) + for u in self.frontline: + if u is not None: + units.append(u) + return units + + def get_frontline_units(self): + return [u for u in self.frontline if u is not None] + + def get_support_units(self): + return [u for u in self.support_line if u is not None] + + def _get_op_cost(self, unit): + cost = unit.op_cost + if self.faction_id == "yan" and unit.unit_type == "cavalry": + cost = max(0, cost - 1) + return cost + + def _apply_heal_ability(self): + healers = [u for u in self.get_all_units() if "heal_all:1" in u.abilities] + if healers: + for unit in self.get_all_units(): + if unit.current_hp < unit.max_hp: + unit.current_hp = min(unit.max_hp, unit.current_hp + 1) + + def cleanup_dead(self): + for i in range(len(self.support_line)): + u = self.support_line[i] + if u is not None and not u.is_alive(): + self.support_line[i] = None + for i in range(len(self.frontline)): + u = self.frontline[i] + if u is not None and not u.is_alive(): + self.frontline[i] = None diff --git a/card_game/ui.py b/card_game/ui.py new file mode 100644 index 0000000..9f47124 --- /dev/null +++ b/card_game/ui.py @@ -0,0 +1,776 @@ +"""UI: all rendering — battlefield, cards, menus, HUD. Chinese ink painting style.""" + +import pygame +from card_game.config import ( + WINDOW_WIDTH, WINDOW_HEIGHT, + INK_BLACK, PAPER_WHITE, GRAY, DARK_GRAY, LIGHT_GRAY, + ZHU_HONG, SONGHUA_GREEN, DIAN_BLUE, TENG_HUANG, GOLD, SILVER, + ORANGE, JIANG_BROWN, + BG_COLOR, FIELD_COLOR, FRONTLINE_COLOR, + FACTION_COLORS, RARITY_LIMITS, + HAND_HEIGHT, ACTION_BAR_HEIGHT, + ZONE_HEIGHT, CARD_WIDTH, CARD_HEIGHT, + FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT, + CAPITAL_WIDTH, CAPITAL_HEIGHT, + ENEMY_SUPPORT_Y, FRONTLINE_Y, PLAYER_SUPPORT_Y, PLAYER_HAND_Y, ACTION_BAR_Y, + MAX_SUPPORT_SLOTS, MAX_FRONTLINE_SLOTS, + ENEMY_INFO_HEIGHT, ENEMY_HAND_HEIGHT, + FACTIONS, CARD_DATABASE, + SLOT_SPACING, HAND_CARD_SPACING, + INK_WASH_2, INK_WASH_3, INK_WASH_4, INK_WASH_5, +) +from card_game.effects import EffectManager +from card_game import ink_style + +# Derived layout +HAND_Y = PLAYER_HAND_Y + 5 + + +def _centered_x(n_items, item_w, spacing): + total = n_items * item_w + (n_items - 1) * (spacing - item_w) + return (WINDOW_WIDTH - total) // 2 + + +def _support_slot_x(slot_index): + cap_x = WINDOW_WIDTH // 2 - CAPITAL_WIDTH // 2 + gap = 10 + if slot_index < 2: + return cap_x - gap - FIELD_CARD_WIDTH - (1 - slot_index) * SLOT_SPACING + else: + right_start = cap_x + CAPITAL_WIDTH + gap + return right_start + (slot_index - 2) * SLOT_SPACING + + +def _frontline_slot_x(slot_index, n_slots): + start = _centered_x(n_slots, FIELD_CARD_WIDTH, SLOT_SPACING) + return start + slot_index * SLOT_SPACING + + +class UI: + def __init__(self, screen): + self.screen = screen + # Use SimHei (黑体) for Chinese text - clean and always available on Windows + self.font_sm = pygame.font.SysFont("simhei", 14) + self.font_md = pygame.font.SysFont("simhei", 18) + self.font_lg = pygame.font.SysFont("simhei", 24) + self.font_xl = pygame.font.SysFont("simhei", 36) + self.effects = EffectManager() + + # Interaction state + self.selected_card = None + self.selected_unit = None + self.hover_card = None + self.hover_field_unit = None + self.hover_pos = (0, 0) + self.valid_targets = [] + self.target_mode = None + + # Button rects + self.end_turn_btn = pygame.Rect(0, 0, 0, 0) + self.faction_buttons = [] + self.menu_buttons = {} + + # Deck builder state + self.deck_builder_cards = [] + self.deck_builder_faction = None + self.deck_card_rects = [] + self.deck_build_rects = [] + + # --- Main Draw --- + + def draw_menu(self): + ink_style.blit_paper_background(self.screen) + ink_style.blit_mountains(self.screen) + + # Title in seal stamp style + title_text = "战国卡牌" + title_w, title_h = 320, 70 + title_x = WINDOW_WIDTH // 2 - title_w // 2 + title_y = 60 + ink_style.draw_seal_stamp(self.screen, (title_x, title_y, title_w, title_h), + title_text, self.font_xl) + + # Subtitle + sub = self.font_lg.render("七 雄 争 霸", True, INK_WASH_3) + self.screen.blit(sub, (WINDOW_WIDTH // 2 - sub.get_width() // 2, 145)) + + # Faction buttons as scroll shapes + self.faction_buttons = [] + factions = list(FACTIONS.keys()) + cols = 4 + bw, bh = 240, 90 + start_x = (WINDOW_WIDTH - cols * (bw + 20)) // 2 + start_y = 200 + + for i, fid in enumerate(factions): + row, col = divmod(i, cols) + x = start_x + col * (bw + 20) + y = start_y + row * (bh + 20) + rect = pygame.Rect(x, y, bw, bh) + self.faction_buttons.append((rect, fid)) + + color = FACTION_COLORS[fid] + # Draw as a scroll with faction color tint + ink_style.draw_scroll(self.screen, (x, y, bw, bh), + scroll_color=(min(255, color[0] + 80), + min(255, color[1] + 80), + min(255, color[2] + 80))) + + # Faction name + name = self.font_lg.render(FACTIONS[fid]["name"], True, INK_BLACK) + self.screen.blit(name, (x + bw // 2 - name.get_width() // 2, y + 12)) + + # Passive name + passive = self.font_sm.render(FACTIONS[fid]["passive_name"], True, TENG_HUANG) + self.screen.blit(passive, (x + bw // 2 - passive.get_width() // 2, y + 45)) + + # Passive desc (truncated) + desc = self.font_sm.render(FACTIONS[fid]["passive_desc"][:16], True, INK_WASH_3) + self.screen.blit(desc, (x + bw // 2 - desc.get_width() // 2, y + 68)) + + inst = self.font_sm.render("选择你的国家开始游戏", True, INK_WASH_3) + self.screen.blit(inst, (WINDOW_WIDTH // 2 - inst.get_width() // 2, WINDOW_HEIGHT - 60)) + + def draw_deck_select(self, player_faction): + ink_style.blit_paper_background(self.screen) + ink_style.blit_mountains(self.screen) + + title = self.font_xl.render("选择对手", True, INK_BLACK) + self.screen.blit(title, (WINDOW_WIDTH // 2 - title.get_width() // 2, 40)) + + your_faction = self.font_md.render(f"你的国家:{FACTIONS[player_faction]['name']}", True, INK_WASH_4) + self.screen.blit(your_faction, (WINDOW_WIDTH // 2 - your_faction.get_width() // 2, 90)) + + self.faction_buttons = [] + factions = [f for f in FACTIONS.keys() if f != player_faction] + cols = 3 + bw, bh = 300, 100 + start_x = (WINDOW_WIDTH - cols * (bw + 20)) // 2 + start_y = 140 + + for i, fid in enumerate(factions): + row, col = divmod(i, cols) + x = start_x + col * (bw + 20) + y = start_y + row * (bh + 20) + rect = pygame.Rect(x, y, bw, bh) + self.faction_buttons.append((rect, fid)) + + color = FACTION_COLORS[fid] + ink_style.draw_scroll(self.screen, (x, y, bw, bh), + scroll_color=(min(255, color[0] + 80), + min(255, color[1] + 80), + min(255, color[2] + 80))) + + name = self.font_lg.render(FACTIONS[fid]["name"], True, INK_BLACK) + self.screen.blit(name, (x + bw // 2 - name.get_width() // 2, y + 10)) + + leader = self.font_md.render(f"君主:{FACTIONS[fid]['leader']}", True, INK_WASH_3) + self.screen.blit(leader, (x + bw // 2 - leader.get_width() // 2, y + 45)) + + passive = self.font_sm.render(FACTIONS[fid]["passive_desc"], True, TENG_HUANG) + self.screen.blit(passive, (x + bw // 2 - passive.get_width() // 2, y + 75)) + + # Buttons + self.menu_buttons = {} + back_rect = pygame.Rect(20, WINDOW_HEIGHT - 60, 120, 40) + ink_style.draw_ink_rect(self.screen, back_rect, INK_WASH_3, alpha=200) + t = self.font_md.render("返回", True, PAPER_WHITE) + self.screen.blit(t, (back_rect.centerx - t.get_width() // 2, + back_rect.centery - t.get_height() // 2)) + self.menu_buttons["back"] = back_rect + + build_rect = pygame.Rect(WINDOW_WIDTH - 200, WINDOW_HEIGHT - 60, 180, 40) + ink_style.draw_ink_rect(self.screen, build_rect, (60, 100, 60), alpha=200) + t = self.font_md.render("自由组卡", True, PAPER_WHITE) + self.screen.blit(t, (build_rect.centerx - t.get_width() // 2, + build_rect.centery - t.get_height() // 2)) + self.menu_buttons["deck_build"] = build_rect + + def draw_deck_builder(self, faction_id): + from card_game.config import DECK_SIZE + from collections import Counter + + ink_style.blit_paper_background(self.screen) + self.deck_builder_faction = faction_id + faction = FACTIONS[faction_id] + faction_color = FACTION_COLORS[faction_id] + + title = self.font_lg.render(f"组卡 - {faction['name']}", True, faction_color) + self.screen.blit(title, (WINDOW_WIDTH // 2 - title.get_width() // 2, 10)) + + deck_count = len(self.deck_builder_cards) + count_color = SONGHUA_GREEN if deck_count == DECK_SIZE else (TENG_HUANG if deck_count > 0 else INK_WASH_3) + count_text = self.font_md.render(f"已选:{deck_count}/{DECK_SIZE}", True, count_color) + self.screen.blit(count_text, (WINDOW_WIDTH // 2 - count_text.get_width() // 2, 42)) + + # Left: available cards + self.deck_card_rects = [] + left_x = 20 + left_y = 75 + self.screen.blit(self.font_md.render("可用卡牌 (左键添加)", True, INK_WASH_3), (left_x, left_y)) + left_y += 25 + + available = [c for c in CARD_DATABASE.values() + if c["faction"] == faction_id or c["faction"] in ("neutral", "ally")] + available.sort(key=lambda c: (c["cost"], c["name"])) + + small_w, small_h = 160, 32 + cols = 4 + for i, card_data in enumerate(available): + row, col = divmod(i, cols) + x = left_x + col * (small_w + 8) + y = left_y + row * (small_h + 4) + if y + small_h > WINDOW_HEIGHT - 60: + break + cid = card_data["id"] + in_deck = self.deck_builder_cards.count(cid) + max_copies = RARITY_LIMITS.get(card_data["rarity"], 3) + can_add = in_deck < max_copies and deck_count < DECK_SIZE + + rect = pygame.Rect(x, y, small_w, small_h) + if card_data["faction"] not in ("neutral", "ally"): + bg = tuple(max(0, c - 30) for c in faction_color) + elif card_data["faction"] == "ally": + bg = (100, 90, 55) + else: + bg = (80, 75, 65) + ink_style.draw_ink_rect(self.screen, rect, bg, alpha=180) + border_color = PAPER_WHITE if can_add else INK_WASH_3 + pygame.draw.rect(self.screen, border_color, rect, 1, border_radius=3) + + cost_s = self.font_sm.render(str(card_data["cost"]), True, TENG_HUANG) + self.screen.blit(cost_s, (x + 4, y + 8)) + + icon = {"unit": "兵", "order": "谋"}.get(card_data["type"], "?") + ally_tag = card_data.get("ally_state", "") + if card_data["faction"] == "ally" and ally_tag: + icon = ally_tag + self.screen.blit(self.font_sm.render(icon, True, TENG_HUANG), (x + 22, y + 8)) + self.screen.blit(self.font_sm.render(card_data["name"][:4], True, PAPER_WHITE), (x + 40, y + 8)) + + ct = f"×{in_deck}/{max_copies}" if in_deck > 0 else f"0/{max_copies}" + cs = self.font_sm.render(ct, True, count_color if in_deck > 0 else GRAY) + self.screen.blit(cs, (x + small_w - cs.get_width() - 4, y + 8)) + self.deck_card_rects.append((rect, cid)) + + # Right: current deck + self.deck_build_rects = [] + right_x = WINDOW_WIDTH // 2 + 20 + right_y = 75 + self.screen.blit(self.font_md.render("当前牌组 (右键移除)", True, INK_WASH_3), (right_x, right_y)) + right_y += 25 + + deck_counter = Counter(self.deck_builder_cards) + deck_items = sorted(deck_counter.items(), key=lambda x: (CARD_DATABASE[x[0]]["cost"], CARD_DATABASE[x[0]]["name"])) + + for i, (cid, count) in enumerate(deck_items): + card_data = CARD_DATABASE[cid] + row, col = divmod(i, 3) + x = right_x + col * (small_w + 8) + y = right_y + row * (small_h + 4) + if y + small_h > WINDOW_HEIGHT - 60: + break + rect = pygame.Rect(x, y, small_w, small_h) + if card_data["faction"] not in ("neutral", "ally"): + bg = tuple(max(0, c - 30) for c in faction_color) + elif card_data["faction"] == "ally": + bg = (100, 90, 55) + else: + bg = (80, 75, 65) + ink_style.draw_ink_rect(self.screen, rect, bg, alpha=180) + pygame.draw.rect(self.screen, GOLD, rect, 1, border_radius=3) + + self.screen.blit(self.font_sm.render(str(card_data["cost"]), True, TENG_HUANG), (x + 4, y + 8)) + icon = {"unit": "兵", "order": "谋"}.get(card_data["type"], "?") + ally_tag = card_data.get("ally_state", "") + if card_data["faction"] == "ally" and ally_tag: + icon = ally_tag + self.screen.blit(self.font_sm.render(icon, True, TENG_HUANG), (x + 22, y + 8)) + self.screen.blit(self.font_sm.render(card_data["name"][:4], True, PAPER_WHITE), (x + 40, y + 8)) + cs = self.font_sm.render(f"×{count}", True, SONGHUA_GREEN) + self.screen.blit(cs, (x + small_w - cs.get_width() - 4, y + 8)) + self.deck_build_rects.append((rect, cid)) + + # Bottom buttons + self.menu_buttons = {} + for key, lbl, color, rx in [ + ("back", "返回", INK_WASH_3, 20), + ("clear", "清空", (120, 50, 40), 160), + ("preset", "加载预设", (50, 90, 50), 300), + ]: + r = pygame.Rect(rx, WINDOW_HEIGHT - 55, 130 if key == "preset" else 120, 40) + ink_style.draw_ink_rect(self.screen, r, color, alpha=200) + pygame.draw.rect(self.screen, PAPER_WHITE, r, 1, border_radius=5) + t = self.font_md.render(lbl, True, PAPER_WHITE) + self.screen.blit(t, (r.centerx - t.get_width() // 2, r.centery - t.get_height() // 2)) + self.menu_buttons[key] = r + + confirm_color = (50, 110, 50) if deck_count == DECK_SIZE else INK_WASH_3 + confirm_rect = pygame.Rect(WINDOW_WIDTH - 180, WINDOW_HEIGHT - 55, 160, 40) + ink_style.draw_ink_rect(self.screen, confirm_rect, confirm_color, alpha=200) + pygame.draw.rect(self.screen, PAPER_WHITE if deck_count == DECK_SIZE else GRAY, + confirm_rect, 1, border_radius=5) + t = self.font_md.render("确认组卡", True, PAPER_WHITE if deck_count == DECK_SIZE else GRAY) + self.screen.blit(t, (confirm_rect.centerx - t.get_width() // 2, + confirm_rect.centery - t.get_height() // 2)) + self.menu_buttons["confirm"] = confirm_rect + + def get_deck_card_at(self, pos): + for rect, cid in self.deck_card_rects: + if rect.collidepoint(pos): + return cid + return None + + def get_deck_build_card_at(self, pos): + for rect, cid in self.deck_build_rects: + if rect.collidepoint(pos): + return cid + return None + + # --- Game Drawing --- + + def draw_game(self, battlefield): + ink_style.blit_paper_background(self.screen) + ink_style.blit_mountains(self.screen) + + self._draw_enemy_info(battlefield.ai) + self._draw_enemy_hand(battlefield.ai) + + # Zone backgrounds with ink wash + ink_style.draw_zone_bg(self.screen, + (0, ENEMY_SUPPORT_Y, WINDOW_WIDTH, ZONE_HEIGHT), + FIELD_COLOR, INK_WASH_3) + ink_style.draw_zone_bg(self.screen, + (0, FRONTLINE_Y, WINDOW_WIDTH, ZONE_HEIGHT), + FRONTLINE_COLOR, ZHU_HONG) + ink_style.draw_zone_bg(self.screen, + (0, PLAYER_SUPPORT_Y, WINDOW_WIDTH, ZONE_HEIGHT), + FIELD_COLOR, INK_WASH_3) + + self._draw_zone(battlefield.ai.support_line, ENEMY_SUPPORT_Y, "对方营地", battlefield.ai) + self._draw_frontline(battlefield) + self._draw_zone(battlefield.player.support_line, PLAYER_SUPPORT_Y, "我方营地", battlefield.player) + self._draw_player_hand(battlefield.player, battlefield) + self._draw_action_bar(battlefield) + self._draw_highlights(battlefield) + self.effects.draw(self.screen, self.font_lg) + + def draw_game_over(self, winner, battlefield): + overlay = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT), pygame.SRCALPHA) + overlay.fill((20, 15, 10, 150)) + self.screen.blit(overlay, (0, 0)) + + if winner == "player": + text = "胜 利 !" + color = TENG_HUANG + else: + text = "败 北 ..." + color = ZHU_HONG + + # Draw result as seal stamp + seal_w, seal_h = 280, 70 + seal_x = WINDOW_WIDTH // 2 - seal_w // 2 + seal_y = 250 + ink_style.draw_seal_stamp(self.screen, (seal_x, seal_y, seal_w, seal_h), + text, self.font_xl) + + p = battlefield.player + for i, s in enumerate([f"回合数:{battlefield.turn_number}", f"都城剩余HP:{max(0, p.capital_hp)}"]): + surf = self.font_md.render(s, True, PAPER_WHITE) + self.screen.blit(surf, (WINDOW_WIDTH // 2 - surf.get_width() // 2, 340 + i * 30)) + + self.menu_buttons = {} + restart_rect = pygame.Rect(WINDOW_WIDTH // 2 - 150, 430, 130, 45) + menu_rect = pygame.Rect(WINDOW_WIDTH // 2 + 20, 430, 130, 45) + for rect, label in [(restart_rect, "再来一局"), (menu_rect, "返回主菜单")]: + ink_style.draw_ink_rect(self.screen, rect, INK_WASH_3, alpha=200) + pygame.draw.rect(self.screen, PAPER_WHITE, rect, 2, border_radius=5) + t = self.font_md.render(label, True, PAPER_WHITE) + self.screen.blit(t, (rect.centerx - t.get_width() // 2, rect.centery - t.get_height() // 2)) + self.menu_buttons["restart"] = restart_rect + self.menu_buttons["menu"] = menu_rect + + # --- Zone Drawing --- + + def _draw_zone(self, slots, y, label, player): + if label: + lbl = self.font_sm.render(label, True, INK_WASH_3) + self.screen.blit(lbl, (10, y + 5)) + + self._draw_capital(player, y) + + is_player_zone = (not player.is_ai) + for i, unit in enumerate(slots): + if unit is None: + continue + ux = _support_slot_x(i) + uy = y + (ZONE_HEIGHT - FIELD_CARD_HEIGHT) // 2 + self._draw_field_unit(unit, ux, uy, is_player_zone, player) + + def _draw_frontline(self, battlefield): + y = FRONTLINE_Y + half = ZONE_HEIGHT // 2 + + # Center divider line + pygame.draw.line(self.screen, INK_WASH_3, (0, y + half), (WINDOW_WIDTH, y + half), 1) + + lbl = self.font_sm.render("前 线", True, (ZHU_HONG[0], ZHU_HONG[1], ZHU_HONG[2])) + self.screen.blit(lbl, (WINDOW_WIDTH // 2 - lbl.get_width() // 2, y + 3)) + + n_fl = MAX_FRONTLINE_SLOTS + for i, unit in enumerate(battlefield.ai.frontline): + if unit is None: + continue + ux = _frontline_slot_x(i, n_fl) + uy = y + (half - FIELD_CARD_HEIGHT) // 2 + self._draw_field_unit(unit, ux, uy, False, battlefield.ai) + + for i, unit in enumerate(battlefield.player.frontline): + if unit is None: + continue + ux = _frontline_slot_x(i, n_fl) + uy = y + half + (half - FIELD_CARD_HEIGHT) // 2 + self._draw_field_unit(unit, ux, uy, True, battlefield.player) + + def _draw_capital(self, player, zone_y): + cx = WINDOW_WIDTH // 2 - CAPITAL_WIDTH // 2 + cy = zone_y + (ZONE_HEIGHT - CAPITAL_HEIGHT) // 2 + + color = FACTION_COLORS[player.faction_id] + rect = pygame.Rect(cx, cy, CAPITAL_WIDTH, CAPITAL_HEIGHT) + + # Draw as seal stamp + ink_style.draw_seal_stamp(self.screen, (cx, cy, CAPITAL_WIDTH, CAPITAL_HEIGHT), + player.faction["name"], self.font_sm, color=color) + + # HP text + hp_text = self.font_md.render(f"{max(0, player.capital_hp)}/{player.max_capital_hp}", True, PAPER_WHITE) + self.screen.blit(hp_text, (cx + CAPITAL_WIDTH // 2 - hp_text.get_width() // 2, cy + 30)) + + # HP bar + bar_w = CAPITAL_WIDTH - 10 + bar_h = 6 + bar_x = cx + 5 + bar_y = cy + CAPITAL_HEIGHT - 12 + hp_ratio = max(0, player.capital_hp / player.max_capital_hp) + ink_style.draw_ink_hp_bar(self.screen, bar_x, bar_y, bar_w, bar_h, hp_ratio) + + def _draw_field_unit(self, unit, x, y, is_player, player): + w, h = FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT + color = unit.get_color() + dark_color = tuple(max(0, c - 30) for c in color) + rect = pygame.Rect(x, y, w, h) + + # Ink wash card background + ink_style.draw_ink_rect(self.screen, rect, dark_color, alpha=220) + + # Border + if unit is self.selected_unit: + pygame.draw.rect(self.screen, TENG_HUANG, rect, 2, border_radius=4) + elif unit.can_attack and is_player and not unit.has_attacked: + pygame.draw.rect(self.screen, SONGHUA_GREEN, rect, 1, border_radius=4) + else: + pygame.draw.rect(self.screen, self._rarity_border_color(unit.rarity), rect, 1, border_radius=4) + + # Name (3 chars) + name = unit.name[:3] + name_surf = self.font_sm.render(name, True, PAPER_WHITE) + self.screen.blit(name_surf, (x + w // 2 - name_surf.get_width() // 2, y + 3)) + + # Unit type icon + icon_char = {"infantry": "步", "cavalry": "骑", "chariot": "车", + "archer": "弓", "siege": "攻"}.get(unit.unit_type, "?") + self.screen.blit(self.font_sm.render(icon_char, True, TENG_HUANG), (x + 3, y + 3)) + + # Operation cost + op_cost = unit.op_cost + owner = player + if owner.faction_id == "yan" and unit.unit_type == "cavalry": + op_cost = max(0, op_cost - 1) + oc_surf = self.font_sm.render(str(op_cost), True, TENG_HUANG) + pygame.draw.circle(self.screen, (40, 35, 30), (x + w - 12, y + 12), 9) + self.screen.blit(oc_surf, (x + w - 12 - oc_surf.get_width() // 2, y + 12 - oc_surf.get_height() // 2)) + + # Attack / Defense + atk = unit.get_effective_attack() + dfn = unit.get_effective_defense() + self.screen.blit(self.font_sm.render(str(atk), True, (200, 80, 60)), (x + 5, y + h - 22)) + self.screen.blit(self.font_sm.render(str(dfn), True, (60, 80, 160)), (x + w - 20, y + h - 22)) + + # HP bar + if unit.max_hp > 0: + bar_w = w - 8 + bar_x = x + 4 + bar_y = y + h - 6 + hp_ratio = unit.current_hp / unit.max_hp + ink_style.draw_ink_hp_bar(self.screen, bar_x, bar_y, bar_w, 4, hp_ratio) + + def _draw_player_hand(self, player, battlefield): + n = len(player.hand) + if n == 0: + return + start_x = (WINDOW_WIDTH - n * HAND_CARD_SPACING) // 2 + for i, card in enumerate(player.hand): + x = start_x + i * HAND_CARD_SPACING + y = HAND_Y + if card is self.hover_card: + y -= 15 + if card is self.selected_card: + y -= 25 + self._draw_hand_card(card, x, y, player.can_play_card(card)) + + def _draw_hand_card(self, card, x, y, playable): + w, h = CARD_WIDTH, CARD_HEIGHT + color = card.get_color() + dark_color = tuple(max(0, c - 30) for c in color) + rect = pygame.Rect(x, y, w, h) + + # Ink wash background + ink_style.draw_ink_rect(self.screen, rect, dark_color, alpha=220) + + if card is self.selected_card: + pygame.draw.rect(self.screen, TENG_HUANG, rect, 2, border_radius=5) + elif not playable: + pygame.draw.rect(self.screen, INK_WASH_3, rect, 1, border_radius=5) + else: + pygame.draw.rect(self.screen, self._rarity_border_color(card.rarity), rect, 1, border_radius=5) + + # Cost circle top-left + cost_surf = self.font_md.render(str(card.cost), True, TENG_HUANG) + pygame.draw.circle(self.screen, (35, 30, 25), (x + 14, y + 14), 12) + self.screen.blit(cost_surf, (x + 14 - cost_surf.get_width() // 2, y + 14 - cost_surf.get_height() // 2)) + + # Op cost top-right + op_surf = self.font_sm.render(f"行{card.op_cost}", True, TENG_HUANG if playable else INK_WASH_3) + self.screen.blit(op_surf, (x + w - op_surf.get_width() - 3, y + 3)) + + # Name + name = card.name[:4] + name_surf = self.font_sm.render(name, True, PAPER_WHITE if playable else GRAY) + self.screen.blit(name_surf, (x + w // 2 - name_surf.get_width() // 2, y + 28)) + + if card.card_type == "unit": + icon = {"infantry": "步", "cavalry": "骑", "chariot": "车", + "archer": "弓", "siege": "攻"}.get(card.unit_type, "?") + self.screen.blit(self.font_sm.render(icon, True, TENG_HUANG), (x + 3, y + h - 40)) + self.screen.blit(self.font_md.render(str(card.attack), True, (200, 80, 60)), (x + 5, y + h - 22)) + self.screen.blit(self.font_md.render(str(card.defense), True, (60, 80, 160)), (x + w - 15, y + h - 22)) + else: + self.screen.blit(self.font_sm.render("谋略", True, ORANGE), (x + w // 2 - 12, y + h - 40)) + + desc = card.description[:8] + desc_surf = self.font_sm.render(desc, True, LIGHT_GRAY if playable else DARK_GRAY) + self.screen.blit(desc_surf, (x + w // 2 - desc_surf.get_width() // 2, y + h - 8)) + + def _draw_enemy_info(self, ai): + # Semi-transparent bar + s = pygame.Surface((WINDOW_WIDTH, ENEMY_INFO_HEIGHT), pygame.SRCALPHA) + s.fill((*INK_WASH_5[:3], 160)) + self.screen.blit(s, (0, 0)) + + color = FACTION_COLORS[ai.faction_id] + self.screen.blit(self.font_md.render(f"{ai.faction['name']} (AI)", True, color), (10, 10)) + self.screen.blit(self.font_md.render(f"都城:{max(0, ai.capital_hp)}/{ai.max_capital_hp}", True, PAPER_WHITE), (200, 10)) + self.screen.blit(self.font_md.render(f"粮草:{ai.provisions}", True, TENG_HUANG), (400, 10)) + self.screen.blit(self.font_md.render(f"牌库:{ai.deck.remaining()}", True, INK_WASH_2), (550, 10)) + + def _draw_enemy_hand(self, ai): + y = ENEMY_INFO_HEIGHT + n = len(ai.hand) + if n == 0: + return + card_w = 30 + start_x = (WINDOW_WIDTH - n * (card_w + 5)) // 2 + for i in range(n): + x = start_x + i * (card_w + 5) + rect = pygame.Rect(x, y, card_w, 40) + # Card back as ink wash + ink_style.draw_ink_rect(self.screen, rect, JIANG_BROWN, alpha=180) + pygame.draw.rect(self.screen, INK_WASH_3, rect, 1, border_radius=3) + + def _draw_action_bar(self, battlefield): + s = pygame.Surface((WINDOW_WIDTH, ACTION_BAR_HEIGHT), pygame.SRCALPHA) + s.fill((*INK_WASH_5[:3], 180)) + self.screen.blit(s, (0, ACTION_BAR_Y)) + + p = battlefield.player + info_parts = [ + (f"{p.faction['name']}", FACTION_COLORS[p.faction_id]), + (f"粮草:{p.provisions}/{p.max_provisions}", TENG_HUANG), + (f"回合:{battlefield.turn_number}", PAPER_WHITE), + (f"牌库:{p.deck.remaining()}", INK_WASH_2), + ] + x = 10 + for text, color in info_parts: + surf = self.font_md.render(text, True, color) + self.screen.blit(surf, (x, ACTION_BAR_Y + 15)) + x += surf.get_width() + 30 + + passive = self.font_sm.render(p.faction["passive_name"], True, TENG_HUANG) + self.screen.blit(passive, (x + 10, ACTION_BAR_Y + 17)) + + # End turn button as seal stamp + btn_w, btn_h = 120, 36 + btn_x = WINDOW_WIDTH - btn_w - 15 + btn_y = ACTION_BAR_Y + (ACTION_BAR_HEIGHT - btn_h) // 2 + self.end_turn_btn = pygame.Rect(btn_x, btn_y, btn_w, btn_h) + + if battlefield.current_turn == "player": + ink_style.draw_seal_stamp(self.screen, (btn_x, btn_y, btn_w, btn_h), + "结束回合", self.font_md) + else: + ink_style.draw_ink_rect(self.screen, self.end_turn_btn, INK_WASH_3, alpha=180) + pygame.draw.rect(self.screen, GRAY, self.end_turn_btn, 1, border_radius=5) + t = self.font_md.render("结束回合", True, GRAY) + self.screen.blit(t, (self.end_turn_btn.centerx - t.get_width() // 2, + self.end_turn_btn.centery - t.get_height() // 2)) + + def _draw_highlights(self, battlefield): + if not self.valid_targets: + return + for target in self.valid_targets: + if isinstance(target, tuple) and target[0] == "capital": + player = target[1] + cx = WINDOW_WIDTH // 2 - CAPITAL_WIDTH // 2 + cy = ENEMY_SUPPORT_Y + (ZONE_HEIGHT - CAPITAL_HEIGHT) // 2 + s = pygame.Surface((CAPITAL_WIDTH, CAPITAL_HEIGHT), pygame.SRCALPHA) + s.fill((100, 200, 100, 50)) + self.screen.blit(s, (cx, cy)) + elif hasattr(target, 'zone') and hasattr(target, 'slot'): + unit = target + is_ai = battlefield._get_unit_owner(unit) == battlefield.ai + if unit.zone == "support": + zone_y = ENEMY_SUPPORT_Y if is_ai else PLAYER_SUPPORT_Y + zone_h = ZONE_HEIGHT + ux = _support_slot_x(unit.slot) + uy = zone_y + (zone_h - FIELD_CARD_HEIGHT) // 2 + else: + half = ZONE_HEIGHT // 2 + n_fl = MAX_FRONTLINE_SLOTS + zone_y = FRONTLINE_Y if is_ai else (FRONTLINE_Y + half) + zone_h = half + ux = _frontline_slot_x(unit.slot, n_fl) + uy = zone_y + (zone_h - FIELD_CARD_HEIGHT) // 2 + s = pygame.Surface((FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT), pygame.SRCALPHA) + s.fill((100, 200, 100, 50)) + self.screen.blit(s, (ux, uy)) + + # Deploy highlights + if self.target_mode == "deploy": + for i, slot in enumerate(battlefield.player.support_line): + if slot is None: + sx = _support_slot_x(i) + sy = PLAYER_SUPPORT_Y + (ZONE_HEIGHT - FIELD_CARD_HEIGHT) // 2 + s = pygame.Surface((FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT), pygame.SRCALPHA) + s.fill((100, 200, 100, 50)) + self.screen.blit(s, (sx, sy)) + + # Move highlights + if self.target_mode == "move": + half = ZONE_HEIGHT // 2 + n_fl = MAX_FRONTLINE_SLOTS + for i, slot in enumerate(battlefield.player.frontline): + if slot is None: + sx = _frontline_slot_x(i, n_fl) + sy = FRONTLINE_Y + half + (half - FIELD_CARD_HEIGHT) // 2 + s = pygame.Surface((FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT), pygame.SRCALPHA) + s.fill((100, 180, 220, 50)) + self.screen.blit(s, (sx, sy)) + + # --- Hit Testing --- + + def get_hand_card_at(self, pos, player): + n = len(player.hand) + if n == 0: + return None + start_x = (WINDOW_WIDTH - n * HAND_CARD_SPACING) // 2 + mx, my = pos + for i, card in enumerate(player.hand): + x = start_x + i * HAND_CARD_SPACING + y = HAND_Y + if card is self.hover_card: + y -= 15 + if card is self.selected_card: + y -= 25 + rect = pygame.Rect(x, y, CARD_WIDTH, CARD_HEIGHT) + if rect.collidepoint(mx, my): + return card + return None + + def get_field_unit_at(self, pos, battlefield): + mx, my = pos + half = ZONE_HEIGHT // 2 + n_fl = MAX_FRONTLINE_SLOTS + zones = [ + (battlefield.player.support_line, PLAYER_SUPPORT_Y, ZONE_HEIGHT, battlefield.player, "support"), + (battlefield.player.frontline, FRONTLINE_Y + half, half, battlefield.player, "frontline"), + (battlefield.ai.support_line, ENEMY_SUPPORT_Y, ZONE_HEIGHT, battlefield.ai, "support"), + (battlefield.ai.frontline, FRONTLINE_Y, half, battlefield.ai, "frontline"), + ] + for slots, zone_y, zone_h, owner, zone_type in zones: + for i, unit in enumerate(slots): + if unit is None: + continue + if zone_type == "support": + ux = _support_slot_x(i) + else: + ux = _frontline_slot_x(i, n_fl) + uy = zone_y + (zone_h - FIELD_CARD_HEIGHT) // 2 + rect = pygame.Rect(ux, uy, FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT) + if rect.collidepoint(mx, my): + return unit, owner + return None, None + + def get_support_slot_at(self, pos, player): + mx, my = pos + zone_y = PLAYER_SUPPORT_Y + for i, slot in enumerate(player.support_line): + if slot is not None: + continue + sx = _support_slot_x(i) + sy = zone_y + (ZONE_HEIGHT - FIELD_CARD_HEIGHT) // 2 + rect = pygame.Rect(sx, sy, FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT) + if rect.collidepoint(mx, my): + return i + return -1 + + def get_frontline_slot_at(self, pos, player): + mx, my = pos + half = ZONE_HEIGHT // 2 + n_fl = MAX_FRONTLINE_SLOTS + zone_y = FRONTLINE_Y + half + for i, slot in enumerate(player.frontline): + sx = _frontline_slot_x(i, n_fl) + sy = zone_y + (half - FIELD_CARD_HEIGHT) // 2 + rect = pygame.Rect(sx, sy, FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT) + if rect.collidepoint(mx, my): + return i + return -1 + + def get_enemy_capital_at(self, pos, ai): + mx, my = pos + cx = WINDOW_WIDTH // 2 - CAPITAL_WIDTH // 2 + cy = ENEMY_SUPPORT_Y + (ZONE_HEIGHT - CAPITAL_HEIGHT) // 2 + rect = pygame.Rect(cx, cy, CAPITAL_WIDTH, CAPITAL_HEIGHT) + return rect.collidepoint(mx, my) + + def get_faction_at(self, pos): + for rect, fid in self.faction_buttons: + if rect.collidepoint(pos): + return fid + return None + + # --- Helpers --- + + def _rarity_border_color(self, rarity): + if rarity == "legendary": + return ZHU_HONG + elif rarity == "rare": + return TENG_HUANG + return SILVER + + def clear_selection(self): + self.selected_card = None + self.selected_unit = None + self.valid_targets = [] + self.target_mode = None diff --git a/card_game/utils.py b/card_game/utils.py new file mode 100644 index 0000000..ebdefbd --- /dev/null +++ b/card_game/utils.py @@ -0,0 +1,11 @@ +"""Utility functions.""" + +import math + + +def distance(x1, y1, x2, y2): + return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) + + +def lerp(a, b, t): + return a + (b - a) * t diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..18caa77 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pygame>=2.5