pybridge-0.3.0/0000755000175000017500000000000010637763637013127 5ustar michaelmichaelpybridge-0.3.0/bin/0000755000175000017500000000000010637763637013677 5ustar michaelmichaelpybridge-0.3.0/bin/pybridge-server0000755000175000017500000000432510636730622016725 0ustar michaelmichael#!/usr/bin/env python # PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import os import sys from optparse import OptionParser def main(): PORT = 5040 try: # If PyBridge is installed system-wide, this finds it automatically. import pybridge except ImportError: # Locate the PyBridge package. currentdir = os.path.dirname(os.path.abspath(sys.argv[0])) basedir = os.path.abspath(os.path.join(currentdir, '..')) # The package path should be relative to the base directory. if os.path.exists(os.path.join(basedir, 'lib', 'python')): pythonpath = os.path.join(basedir, 'lib', 'python') else: pythonpath = basedir sys.path.insert(0, pythonpath) # Place PyBridge package in PYTHONPATH. try: import pybridge except ImportError: raise SystemExit, "Fatal error: could not locate PyBridge installation." parser = OptionParser(version="PyBridge Server %s" % pybridge.__version__) parser.add_option('-p', '--port', type=int, dest='port', default=PORT, help="listen for connections on specified port") options, args = parser.parse_args() from twisted.internet import reactor from twisted.python import log log.startLogging(sys.stdout) # Log to stdout. # TODO: replace with a service. from pybridge.server import factory reactor.listenTCP(options.port, factory) reactor.run() if __name__ == '__main__': main() pybridge-0.3.0/bin/pybridge0000755000175000017500000000511510636730606015421 0ustar michaelmichael#!/usr/bin/env python # PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import os import sys from optparse import OptionParser def main(): PYTHON_VERSION = sys.version_info[:2] # Some version dependencies. PYTHON_REQUIRED = (2, 3) PYGTK_REQUIRED = '2.0' TWISTED_REQUIRED = '2.0.0' # Check version requirements. if PYTHON_VERSION < PYTHON_REQUIRED: raise SystemExit, "Error: Python %d.%d+ required" % PYTHON_REQUIRED if hasattr(sys, 'frozen'): # For py2exe distribution. os.environ['PATH'] += ";lib;" else: import pygtk try: pygtk.require(PYGTK_REQUIRED) except AssertionError: raise SystemExit, "Error: PyGTK %s+ required" % PYGTK_REQUIRED import twisted.copyright if twisted.copyright.version < TWISTED_REQUIRED: raise SystemExit, "Error: Twisted Core %s+ required" % TWISTED_REQUIRED try: # If PyBridge is installed system-wide, this finds it automatically. import pybridge except ImportError: # Locate the PyBridge package. currentdir = os.path.dirname(os.path.abspath(sys.argv[0])) basedir = os.path.abspath(os.path.join(currentdir, '..')) # The package path should be relative to the base directory. if os.path.exists(os.path.join(basedir, 'lib', 'python')): pythonpath = os.path.join(basedir, 'lib', 'python') else: pythonpath = basedir sys.path.insert(0, pythonpath) # Place PyBridge package in PYTHONPATH. try: import pybridge except ImportError: raise SystemExit, "Fatal error: could not locate PyBridge installation." parser = OptionParser(version="PyBridge %s" % pybridge.__version__) options, args = parser.parse_args() import pybridge.ui pybridge.ui.run() if __name__ == '__main__': main() pybridge-0.3.0/bin/pybridge.desktop0000644000175000017500000000033110637710234017055 0ustar michaelmichael[Desktop Entry] Encoding=UTF-8 Name=PyBridge Comment[en]=An online contract bridge game Exec=pybridge Icon=/usr/share/pybridge/pixmaps/pybridge.png Terminal=false Type=Application Categories=Application;Game;CardGame pybridge-0.3.0/pixmaps/0000755000175000017500000000000010637763637014610 5ustar michaelmichaelpybridge-0.3.0/pixmaps/pybridge.png0000644000175000017500000002534610467146724017126 0ustar michaelmichaelPNG  IHDRFksBIT|dtEXtSoftwarewww.inkscape.org< IDATx{$Y]?'u}{zgK`=,(/Udc]UqU6BEp5$Ucw w p" " edGwOO߾u땏deVWֽu:ߎ<~F1++o \{po9•T8[/ta>/O)=ޗ /x+|CSχ{[Hwx񎷼\}_?|u-[[1S++ 0;z/?۷62 ƴ{n?4!?zn}@CLCTYϼՇ^_6yk w5&!D*aRrD*sJf7}ݝwcuw^'Vc&X|j{k>p1+ۀ n: w$U8rYg>q c&B[r_*w|KB {ر@x><kCWes>^7x~qǞpŸ# 9ÿ{Ͼۚv&bVwX_tz9QLC|7=\n J0'9N0MJ'yO|#o`q!˛?|Nv 1+oYi^u[)l\7}f k 7fp*xݟ㍿Zr#ܤ o{H(_v{`W*W ^K0?/@J^13@8b@-Ə̻~/>kpD8-ƏbEYܜ"l6Vq ܗ.`MVLDHBJIբj. fa%HRnD~0aH9Q#3E~K||KB p;;yk r6/D6R4~{Kk\| "Wv7]RyGG# 8fsd2א$B πJxJJ6]>&M_x}s\zjJV;rdJ=7,|kS|7XǾ866ƨ i{n;{n΄mگ|҆Ibzm@\ xۦ`γ8LL<4sR_~y+np*;xTj[1 lbmB9NN;Ŗ>N,ulS؜A(2ŖNu.&iZ$rY;o'}J9 O7ӿ#e~{^ɟgZj2N2$31mN\k߾;Ro$;cO^ʧ4UJ˶6xdzLp39ycq]i*;3o.+?hϤ߼N0a\++?z~2&srўn$<5_l/W0QLIM4X[. &l/&|psW,s-+LH8=toZ7Nn-3H8MΛ\/dZ) ~(+7L^-wLLN͛%,NqbIW&KN5ci')jt1S!f1s 7l.-p4& нT BpEz%vKK94R^#fo%l輇c.t/u&%Dm8BKhKK@l^ȡ)+7$~KT:nrPW*WہW&k 7R^?π^l4퇛 6o??|םSrVAoP*W yŃ<6l,koyݭ\t+8(B)H%܌}&jsM2Kqw-+Z[&$\<3CJuvwwݥhy^ ֘o|MV_W*W:h{A;7N謠`5-RJaoo/Zͺht-8R}#KN ?h[Y&O,Wf Rw=å+5@oqzNZʕ+TUZ|Ra/nǾr/wwϹSĕ "}s\f !ߑpO^3}pC/rPVi48^WJ]>E|c--dkKlo_Q7YSԨ|N-BJ\$0*=6yʊRJ8+ 1.i9jˑӄ <|ߏaWdKUY[ bT gڒ, ^3:!΋ %Tk;t:sL>_S-/xnTڼ:MRD NhG{qp'A^fIwTjnQzל( AGqk?m8SDk~  wpkʼnԏL rr|'\Vr͉'Lp:}loMV!1$IyD(|-7r^ZkvAtK8`R  ^Ézr1?}[\Yy:GT^oeo8Ҡ:p}f iL-e)U bǴ؏Su׬ b*i߆@To;n$džS wzUBhpH8MY0mSk Ouq#n?o_0\{z$q}Znw/ӏpZI8 ehǭ.eWH++65sYhL<~Rgjj$A5|z\iG}wQמZEF@mP9*uҐRjۣV)7;NpRR#GFuz2t]k &e?HGƏп:CX ^^K6V{q-mF%ՕE5H'{8!DCH:~ 7NNxogHFNEF8pBX^՚ދAJH=f\i`g8mH M1o=wʕf-M W*W7kElT: qNpGƌ;*FA ,3X gt$?Q$NOm,E#BȎ:TU,A&91j-bގڵg"9tX3"5ty8?9Ԫo~HsM)b|H wRha.<*~G `QoyR;W*W2K\f9ON4s C~$!X[qJ=,rTܰİ:JQtm@ nJy~+"6iBPj>N8ӍKtC: p8.ٓO(Fc!N+]vy'-ɵ M08HAp3C]l>?pڷ8|׊QvKToS: FAZeggW*]d 1OQ_ y\VɪRyq C%\\ypmR4M|_F"sŞRCWA4ƋAsb=Oz>!"J'E2Ԇ4NQ'!r;$DgMlZQ*F6!m: =m}ytƆS: @H4Nlvј<6\0M?-BC}q#nc-LJ4,L/Ú15+ϡIgrE%46pUj\e p 9|I4՗rKi80Gd\q"^Rrz={:~TwCppӘ,.iHq(>aSR4Y&t]ONfb>;P")mPX֖ ujT a#DP# 0+#8q@b K*>啅av!-*uZnwN R>"=4-i(u*ᖊ8)Y1=C .0M 0- 0̞׻ng$]t(+~.!I3B: u!a Ob߅Й;)ZG=QVmO='޹Gnaj6&>lCwݱ}=x;G>>QB`iaXL+9J)1M\ƌ踭T,momF]v&OZ[Gt Ȱ1= |Rj2O>P,qA $ oKKA(IUiFđlE:6Ce_Uv}!Z 8?>Y>G{zFg&~i_tIwv >\8:^-v"Lͥ+r@8E.-T$\.s<6\̰'$rnPTOd?gȸМC~=fV0Lfp*"\\ĺ!X(="ui٘_sp[-(c}c_?Hd|H?~wUo6tW/ud^>ڔ__sz('Fb!vqQ⧳aI"|G }/4x~=Ls6T; HH8Eo`Z E L0eXG6} ܐӗ> 0ITbN0 nJQ 18"Hca]nΤ]xT?T۞~yT&<㲳SM^p͆lk z 7ԏ$D;:<`"n4rkP="2~Ⅿ{JCcl`}54\zŴ[17jku)ӗv,s0Ս='L=ڑ &z 2'MaئQ8 nÎl>sזZO_mg]g Yƍ=8w]KKߔP`d WL(44!p+.c4qY ?b`?]*{`.Oi$U꠸r1ϳ_~!i$\G?Ę6#FE8ѳ4{J8E:-M8^N7F_H*UcH Nx<,8{: CC yZ[;֦G0$a5ƾ"\μH:TB%4R p(NK8@>EvnYl8fkiiA :AXKA .iig/P Їp\5XCDw'R!6hi#FPS*6g'/ihtA^⪿ JΦ4p{d,VW np-Qo2di Y;PpqBiiA.au-Q^KW!\W<595 SbȬ%0XXȒ؝eڼiق6óؘh} 6 aP,XY)힖fe5!` a|.C.QH&:+DǛ-D'.A@8TMyFH|.GfI]$F]} UV" SY{昀jBJ8l #88L$lC 쎣B rpڄ;BXlbǫ)P"Sf: :[jxX\=7 \&hrd;#\N;r, = lIDAT e4 ߌ I6g Z-G:u! cd2&L[IcP==0ZM}zaL 6;X0afTᝆf}qGib[٬ecd HQJԾ؉KN'U%FOAƶ,61 cqhMp6E}f.4feeYZ̳"b\6@ !DWBė D "EԀpq @*6m 2{a~Z)eG5M  vpVa1C l۲3՗9Er`Gq񥈯c+epדV;p[juuWfJ,b!515&N4qT,v"~j?*XuTӎ5,C sSQ! zXUx~Pٌb^#X@jٕ7]WZ ժa,/-Ref+'T*tF-O 0gld`%yۼHeK"&aR(dy3ħƜ U6k=s?Նk9מӃiv24QOD_Wqy sjoئѕw/EpxjwDube?7 ]儀j1éRy<+;&1pDV_{B>cj׏I.YpwznK>'K8ϻ %0PgOvlޟ|L UmčĶnv>em ._z\p,m?}8/юhG#v|D;>ю]BI(=8 d$Ib|]9юhG#v|D;>юhGA@)e4vՈv|D;>юhG#v|D;>]wD8tE;wD;>юhG#v|D;>ю.I_p$I]!#v|D;>юhG#v|$Ɇ    0 JI,!Qy}8<< |8G=>n]s\}Wng;ݢ]Ϭ\}կ)}g"om_NMZqV6` mۯp=w<]mvl.Z A7Uvs]_۴}[2B|I;Ѯg>g>tyE|vUJ_}{,~/7n@p$(:?/W|o_t\ǥo]sW]}wCuNK~fCNnzm[r!+ccNqh?x! !`*~  0q뽽kkCS7p청 Nme\=$:e ]`8~87$lvWi ˶a XChJG3C3b"D %Թ3+wS$# `@x$F`] ?nyqHl%Br˴39'CnmC׀eI ?b!N(UnI;^3kϿvWo>ZY2˾}CЋc7(lsjC\/Ku:vqVԵ/.\bu0/}"ݦ}z_8?'N vonnBR 677][iyu_݂ U;0;>vCEEƼWOO{'jM }D]t]|.[')Y>e(Tmz")jEu+?me(>v?Ucq۷/ p"nvKs}n-WUv)A mxKSn dH0 <_w.nP|' q_}"*9OT7olՍcMPs ep.v}nuM2|I>kkwvew>vep}n?vEH=]~s˿q0,Oۭ훁wl羦X HK쌀{hL:[:6-UIJNv]a].0s!*-Cnw!||>Y۾:/@y=>v@!sxظ^Il~W}֗n2o3t\.騡xI'4֮RvWiG߃e~E;}p7MܔjkhB;q˴Kv]~!vhg>U\"q(nj_w|ig:}ChW}}z}T*6]m}R_{nϴl]yl2&w_֕-M1292n]ݡtEL|vG>.MT#v(F&ڥT}9 ]Վb ՎImn~gҞ @sοK @w|iWu?ve\m0hGSc7mӴƔ@}_oU3]dv0trZ䴑  P*K?MVR~v#~;SU- l%t.vS; \ ٮS3Kي6m/!ۧٴ@aLwv9vl\uAۥ~&]ٮt]UmR|>WqPͲڹ0ҥKLSFKrD;>evE;>юhG#]$<݄M9YY?ז$ ⱎ6|:E?y_):(Du^f._vq yݰݦ`վ`wt] LSquCڸө[h %LuGD;>1j""ډv>v|D;>юO״T6⣶; @8w۝6i~5 ]ʊ c8vL,߅s.*ڙxaZvu?۷~?o}D:.D;D;>юhǧkَ禋Ͼܥ]g A;_pE02.v|D;>.,2߯s}t_hJ;NZv,n~_֕i1m͔ET1j7t:US5hN'*c7X6trK_/ڤh4*Dюϲevh'ډvhc<~n4O?QnvaDţ.m+k-ϧgӷ¥xB.&;kh?j4ֵ\ `=ؽ2 a`B-K갮K굢ek&D;ND!h{oiG=88]ϾJ)U}pp;O0v[fnn-3 v!;~mѮvsaڙ .q8 mtrZ ?t!<ю2eSvh'ډv]ӝf}N?tr<~.mڎqk+G3a:[P0tҎjez4]gվ tv>b76? ;wvΝFI)Ѯ.M+ډv1DvҎVʎU* xT2t-F0n!j7 .G(Rf:Eðqu#zpGBX]#v|D;>mn?uP]ձ{y }qefޤvJ/Gω2fՖ)#_IF8zϿ{?#G7`ro}еetv|D;>юO(..UEvIX m&Nۓ$aYǮ_6$ZnMkJ-Bx ݧMT;NBu 06Ai4!ut|ۨ{trJ̈v|.hW#ivGyc6n,]D;>mծ My<>r]$p~:לs|xpV]/^s;^?:``:ǡ $ 4uLNLf3t)OSFhǧIbٿvE;>6jwGuvჇhǧuݶip[mwE;ĸ'sggWdG6Df쓛݄O>~4Νx <9Y*ob 8צ]2vz'h EoߤWh m O 6G_- ?]E#v|ڦ]e iv]"vpkW0Ma6Fpit)02^5#lnCl6=pᢂ$5ʂhհ*CC  ,hG#IbܰiTg"iv]#v6/Gc7nȝPN0χw˜E`8.ڃuWedVV >?^<ٞ_>ڠ]Cj*  L.dv28lkԱAЯT±M!AAWƥ.l ]k7$U6Li_>L9)Н1: rq8EwZmf!ԩԝRw u^S TZpA 0>xhLZvgSZ!¢;P.̝j8|=ւ0Z_laB{:T})z.f\lI,;P?D;юhGө+ڹ.\v(btR]'qyIжbSɩu?trn(j r6p`Y9N_^uEm3o3RPڙ룓/ExM\ۥ E ۄhG#nȕ"5²ۍ)'O?}8M849-;]1p ]ZZ0$I`lYtǸuǺI׵\>{fU; ¤ƖѬu(i% RhG#nȕ"eĚM!FffKуW7E.jgΏP+MgX!it?Ӈ\nyQ68IfMSC^ꀚJݞ-m&X|Y۴vtL,U{A~5 xu  &Te!"5Bfqa4lvNr8z+betG䛊!3z: up q_^0z ^֔2jZvn? +{Ʃ)@@|h@AwiJv#O A  Bi2ĚM8pmMܺ*n0ݽ]'ԑҋuwǚg @I$!,2if@.&v3STpz ((  B)e盬Lݐa6-2MfL*VbK﮷"`"p~+!2ښP^BG(2QvיB# C6~]tst.e6-ƥ&ف5BŬ52gY `euϒB@!dC ^"t}ee!.leD;>юh'v!+/юObͦ{юOS }*W&@Ђ4$mE)@ENCp]i  \Y+dm&ivsb?TTG_ilY<.&tюhG#]1.l gq49N4WxNSf@k~LmkѧB6m&B$`-Uhp$?Me:\43>@VDWA V&s_(8F ϐvfS-ݐvurg ϟAax@@j yTp0T Yu7al^:d`yz{:  @"}$ L^Ngw1O<(G-llb}l[n[ij6*0C, z)gO˖/|=a/6|\ZZ/@`@32i(`e5u\y@p:9UH@aFtru Q{>{o7ehv] ŲUt8n:.]D;>uo/?ϝ&ڵ]J]@AQ۶:ls)<DZa}׵Gt+z}lq|z{%L.?eϪ9.t?[;z5jkrLw3z0=Q^o xfؗCm=y<~rM.6bٿvE;>ڝNNK,vmoRW;w)]OQkӍ`@1.:e: A-;-юGMuF~( ]k N^N{NN[1\ va.6I_tWdmui7~e+.l >Ǟšmu]7Iam5u(>(`p ۭ+ O)iass|3fCS3˟M O@\aۍ_|ud'.L^NP\ XAwюhsgFF#ع76sl\݀g׾j7&lv1Im:|HS\RgH+>.ӥ2:Uڒo/%Pv`O{O3 EFMgNc$З6}H1>2\kkA`ZҺL..G|zetv~M>ˆ>h`v;x +":;i@ N`M*.8eLH)#Y ut4 I-(u^f @fc1; oI#ӌ ,M_ʻ2LIRD۴zPhvCwG#څ^w(}6nS22X1?sN1|!aδeo05g?>ǔ}PuFz:d"=^b D3s| y?Їxn|nʮskR@pM u+놦mui7~e+4}ǙM!Y|iv]jve *E y c:?sgG)-F*mn.1uv|O*ML3 ѫa iT`>aڟO@@?vzmmuy=̳ؗb0@_j o:;;Ⴧj:H5 [ɩ_/c|el BFυ IDAT~R2zelE;>[.12) dΣ;|] P\*3ǚ~U[V@\NNN>@b±/13`0`X?c'vW7}eٸ]],c.uS͏țR鵙Rٹzcj3 cN3__Wζ ` p:Ѧ]fu:d6f3ƮQG/)/ XAwюh/ԧj45FPMsBnA:`<]Zu2`BaOsRu^+[_oiZWNjE'ڲ|}`s$pdSeO S]/Q <ȹ@?';-°zet?T0tId褕P㹵Vg bA[wZM t z~6^_i~\TM{h vNVSg_YͧpQ qWk  }c6K`:h4u! geApQ-j%VTo.plRx8?2εm6 Q:a Nz7 G.lԍ.F͠ʻcTQٴƯ7ZqaX ŲvqveF#_/4eU] }Ў:Iun]Ow ei%ڹ Ϋ$vKju<|1dpt1Q xTaކL Эۋ~Xd`}DFme e|e 76)O?~ :|'}6ol.~PXAwюh=,LY a?ݶˆ>hg=޹.ޅ !) (`.\p ۹tvQL4;"3-HP ` {$HSA Y`W 2N.(ؾOliMRv]R76sl\݈>rlLlb[wюhǧ :.mЮk"u(VO(_t7\Wv7m:zY4Y tfkyfl3,e[Q6zc{̀h;wv׏۟<~2-888;wvz$,h/^tƊt,r.XAwюhǷ U?<]n~B/Gp[l~U݃uSwPUS=+e{H_d7_5@YVRhKb<:4=-?=3N22\>MVGh A3|¬W7 8m\G/`\۸VMK^@0?ڬ{trJ̴YfbٿvE;>M^NtrjPff.vI(ts|`9]'/\Tw:L;<+` ul?ۇCl}v@6䟓E7O᧟h~1/_lqb6IƉbE;>ulS+|T9tDz]&lv>O<c{$q,nbI:Hu:Y5P:I(,`lid^ j0`ssGL-۹BhFQ+ f.}]Sdž:]nP,#69b bv@MP>`iQ=kF@-4#x6{FIlx&S5ZAAeпՌkÜsj7$]3|@ٌNoމ+:`@3-aVNy] @S4tրZCWom2dKf>TLfMd@AAl67#ؼ ;ȥRonkq<pakqu6nj5ߕvC"5MXV l]͕ێhG#v|bj׆ul6[;n~t>x>;wv'١6olΊ.hW U_nʻpVMuXZc@l:?fys"y|{lڪ#T8{@z\4谦WVijse3SUk  `"v|D;>юh'vϿ{^hЮL&m]vmՎ3E״ģjlR|-?ם6}0`= =O6b}|.+ ` ۭs<7` `իv&   emj! aRjwٜ:΂jh=tRQ߿gTN@@WtH}m5>`:4PAAA+2`rb>y9lhbvCµ,]Дv\C 9N):@q&:@}Gi q)`m2L>ҡ.6BO AA ԭޝnLg tݐ05-x#-"3 łsmf3vmӻ2emCYS7brA59v&PLf@4Х=dlo-3E(^)@}fG#v|D;>և[+cΦ9 M߾4˦ g<2ib[;:U%'?__O>N}i7ksQ_MTXm;c`v 1iP|m1k7h_M1+[s&3R; AA_Ëln}T,v[p:9]E@@S}ֶW&/'@,fxSc :]6j=32>+@lq`x |wa@DkZdM).$L& AA00c0Eoo>b}(~.]!qm|FA;=S Nl>,u tlfWc a0eH`n6k@L]7Wd t v|D;>юhǧmJФvG-^o\ZmeSv{fEP v l}_Lw @2Ď۠4~"iRX/]юhGF~vXჇKJmӮX`v9,+3USp>-\;QFY}Tl2hV!] @|3tV]=#"V۔H4],ѮhG#ivu+Y.6B Ov>!.`-(vz=mph( t@FzshK̀!,m Qys_o8U Ųv|D;>6l\pvk7$umf@@Z_`6A;_Li._6M!t.N[@P:e]6t(Cדֵ~%*vTm\X\J)v{J)6olٸiVJCb[wюhG#Iini9oK]@e\Wqm=q /W^o:iMucbvWLk{l;vZj`:{ebwUkZ;';;jΎzqahюϲev]vE;.Fݰ_52vnN:ݎ0(C/؍t>ۂt+sji[eu3]V] Dy'N@)_4 mx%R\|0},SX7ծh'ډvh'Y;:} 09\6`T=y$n늩휦nʨ5Ulr! ]/^h@QvΝj4G/ 'v:޲_ ~kQJòevh'ډv۽[S{ګ]c1rR :]xM1H_;@@ڢtG[kQؖ;;aݦ,Њv|]vh'ډvP{qB=|P=|x:Ǹ\/\/\_ sKC;|%ez (U ѭ-Miztе_]d62V ]XAw.nr!!!Q5EOvn|9xv:92N/85,[X7Ѯh'U!v|D;>]k=k؎q_./γLLr:+N[+{4j~'5:h4=<-_' ]XAwюhG#vPJA$p~}=~]eWVӺ.8vέEz lO){?1R0Nc6n~j7DtbhGӽlюOLb]NA#v|@y>mtxbfcΤxdэ^xlL 2]2?~`Ĩ f3tR>FђюOhb]юhG#v|jG~.l#Yd}xx6u677BhH@^GK81`uktՍ^?Pڕ] |Wߦ`վk;Ԉm]'ݸ_4>\B}xxq}8}Q|v93}Ю >تձ\4~ؖZl?ڳ _ۗ WmAR vXyw}mnwK WBe[l]㈩v۴Ч͠AcS;vwIJnv;88(U-eNUt~!ٸَ.giQPʧȧW]l>6|:T o>e.Yۺ*Lѡ,W\ t[\&+q.6v1]n/A} ep}Nm6hjwb]]RmP]_EڙΩInnwmvC4Yk }0TaӣBߎp.i/´e_}1vؾ4 pA;owRjn~nm(KjkwveKӮ>N.kgdvCw۪0hE(.vKRۦyN@Ud>x4A4?ƦGeM\uӾW㺟-cAq}wi=߼?'888`5#8>Syc6n4ަN&\l]].wdv!>v>ߗ߈iGq[]F~'w!*mZS-pڷj糯3?_?趻 M0-}*=&\6i7enȽ%|~ߕϺ]E*2=]uq}k]5*n>}׍b|}qgB)?:v皶e|^?#U?_)!Cؙpۤ]"x]"x}n dH0vkHAݕmJ >>v~3c8!*礯<W]{ө[!w v_Z_G! Bj?$S<\6J#0C o(:E8/;šnR~v۴;>vs>}`ϟY_]CҮ:ZG! ῝*뫿 $eFlttp(YJ-mkgCWS1)unn: #zH0OE'bwi0cCV)bbaк=88vvg~hYvlF붮#> _SnP@}n\t$ }#e&I~n]d1ڂsWB؍¶>߅{h ahg~-mm?w nkTNm^6w|6ig;}nCʄݮ E˺Nn-w!h3kۿ/P۶]0{}: ]h a8#ğc<}PRJɟRB}I;sj v| _߀a IDAT^W]JtFюhG#v|D;>юhG$ T AAA_JZ)$T"#v|D;>юhG#v|PwD@wP8|-ڹ#v|D;>юhG#v|va"`5zCDjD;>юhG#v|D;>ю][HD/X0u:hgG#v|D;>юhG#IAAAAÀ$Idl))m] юhG#v|D;>юhGA@)eL;v|D;>юhG#v|D;>vp>vv|D;>юhG#v|D;>]|J?юhG#v|D;>юh'I) (   C4p$eHԶϟ^da7=ζF>8nFv[|q|߿j-w5~Ƿ[5eLk+PnCw%3 4yN/U' N~Pn!'݊} Q88jPn C?ݢ]ݦs gvG{yE{uTm}ݿyN}p֔ʖ{O5 hwd\=%CԴ;?YNGĐvԳ[n.CRBM WF8̝p XUp>O1azgB"k\/'3ݭ_z.Kjm3= ~{{B-ڙ_ B:~nrrP:m R}?:=O n4m]wє#ftv[=e<`}Wj $d s=4"Nt]2{/\Pwau,dA@J~'̋PkFPAƀ9*6lo>9zPvVԶ[jfhchvK E[ǭwLJ;1Mp5f nAk{ c Դ{y-YV/;}n[+}$IgYgtH1d)XtUvvu7MZ5BiW$I:pvr"pg|.w1|6; p~[;]ew}W}g|ڎ/^;hUlu~c7ts[; UD_kC{ϲKMxlv׶ t|s3J ˦k"Bh/a:t6j9Y\,Q:vw;S"_vv+M[XctLϵ[=EWg7;;]P?.sR>~$|L`}ަN$p~0Y\w]un5|H:C)3nؾbŕv\ )r6f j+žWUvj8P8PQ^Nd^)qsm&ʦ[ʰHm@aUUevwSU]nюB;w\~GlTLcp אָ +PT1Zʮ5n<qX気gEmJ_/[yw=&ڀ{`plr~ۦXmH=?KD+ympE?N ۶;\R3O|ۺ6U8'}p}9Y)m7}v ?.ݢ]gCEBb.jn|fM粝?G#O>1>0g؂@@%춝gb~.t8u4nl$!igP tA2P>NB0%c}Ӄ.hǧbٿvE;>юhG#񩫝{kk`tί[ 8])g7kюO b]NѮh'hWb ǖ`vVg) `@=`;6ccj"e jS0Qa݅ gwKw!^v/'A^I aŲvE;~]ю߮hoW+mvk?udܶGJY*|| Jݶ1S?vuHbN͇.cBZᆥݥElo7o+cp`4 -]],hW+vvE;~%i(h~q6V]{3=X \ŴƀV~R}EM @ sD<3^NƵt 9F#|1 p_Å¿]Yvo]ю߮hoW+vv]$Sv0:c6  UNٲ ~`<[uR锃z0ya1aravBgմN<0Ytjg nsφɉ5ӝa"Ӡ˂kznaL SV@q ej!1 \PvюhG#pektus  400ጇoЃ,t]z@N/@N}5M41`1pe ^%mP {SS|;&CѮֵ,]XnW+vvE;~}Rrn4rAOF1z;.X8Yͱ- tL;=Tyðۜpe [u  !^v\4߷xŲ?t]ю߮hoW+vt^7Kړ$cZv'Ѐ@1C2t8֎f)auAug'OΎ}>`7TiLbakBC,ډv1Dvn嵋,A;mzǹqt{|n[rYl=?KҮ~x}ʎl_b&"]юOcŲv|D;>юhǧ:֓%I6ݘLvޢ.LN -fH^ywizAz~jg6߯[oLk롏YxLS1uLҩ=HԂBwymPdŲv|D;>юhLJk׏ <~\-“'@M R@ XeRb"`rjUlX>U@Np`Y}WǕS ĕ1]\vюO*7nlD;>ñN>k|]i:ݚ~skn luW~lNY]IoAۍK$L?x&^U8߃o ZLpn-< 28} `Tw8<4WFev|Z]c=V^N5ӭuDnp~OG,n*P6:j.C}yᢂ$ 5UHǟǸu3*ģJE`m~> ,W=%[BJP6TrXG e4ˏ.  ]XAwюhǧ _}p2.%i-v|3Ն ~Uf0p:W-ﲿ}PQѷ9g۪t?ݑ8im KFX ~NE@攛( >vnw;hG#R8[69m]hǧuڽE\_sG싡О$In̺ W Y68ǣhrEˮM;k6hG_/_?Ve uf,O2AA`'u-=vB|v;0y9юhǧu}ZlCRh,6i!k7}jA$u*rC ۋuގ)IAaα !ȜoӘ|v0zPm GL5h:3@LXtюhG#ivpۭ]~\miD;>k3)|tGlfvs@`|e\^3ݚ4 @r벀6\A="zH!Җ`NYPNk6\:L[0@lP5J0 >1D;>юhǧ5څL$D;>j' *70P LVAj8 5A\ffjR`M4cBϯyh𩚪eCϒU7HAAgeneS) Ѯ|6l՗o7$>6\VT[Yc $IE.*8?^,&p'@9u +X)׵  p^Ν^G)Ux_S{L׺Gڿ/U=sSFEZ5w,:hG#Dnk#5mvukUtH;[Pti{'Id:>0؏̆|Q̆c^8+{L >em{!9H0@Aʃ0f_?4xW΀)DpЌu׺Їvn^4IdF#v|Z]ɗh'vfSnk.{jU^] tZ:l3 " Nu lfk:(~}Ŷ)q P`&Dِ$+W,z P1lYf$a  pA473DnU*ZnHBh?}vL| J&kʇ Ͻ)9S+sR@ 5!oߤe@YUUfx m/}܃սM%  }'de1dbͦЇYB0Okms]t4q]\Vm /#BbӠKF{eA󳴏<?9c,d5hPAalm-=x>ͻ/ Q!iķmnHᣛv-BN;4ΩX~sڷ}@(+~W  @0+6D`M bvlA[ ŖW` Ϡ](JT@E*AԭL>d]0h;Eg6Μy-#m(E.\Ts{qO:}UV LkOedc╺ .t=1zaKdrRۋ-t[F@C8t XUA&°5Bfq9Z&J1Q=W|,IÐT@>c ӂ}g3VʊQp R&ְa|<]a Ųsl2NY?]k7ǹ\̦PvLMLb*Nڅ ޾;g4P^4w w=wo;@ 949߶* #@ :T@Ev fA,T@;9|UA]{_>F*򨣴V ^˓I++BVZbٿv[]c=^o\,Hwi4UN!uSS bF@Y 즶t<<Ԃ6mz3ZkɦzL=3@  l޷lC^' `eQ Wf0bh ][SV+Zp6ڸݕB|%Lg/c_0bZaX6ALw*lpKlOLh-t"ۅSKp. 0(z  7ilz4SV7 *$ɷTCҾ̀O?oViոeNVMN[#J:B+zev|Gpb)2auEuuf3 Nm\)+~.M}! .M;wq:[@*o  Tv\;YͦG p(} 0S Xm}*ShG0`6oU:B0)tn- Ųv|DpfSaYbˆiWu}e5NZ倚iC 1#B ]jvwAL5Li! [F,2l؂ z}E r/GdA`rPݗ^dzetvpi4K>/7>X6tH;׻4 P9Mt-\1,;v.0Io28@V06/.‚C (6e 1u% c6nUf)o?ܒ>Cw.,),m! Ү<P^G^{ IDAT?I]g;kq@MB⣃MS7bяGGd zM1i,׆4eMZl>'s4df ^r @(wӀ@~8vro7.d~Lgc.}]#5G7FSWbQ*;ck[1e}9tL>ڥOSgeQL k9`94@_FMĖ2qys_} '=]n8z*Zf鎻hZvǽ<8LІʊ pf9 FnhG09k 0qt& JD}pDzx'=? ׇ6&`鯚N絪OqچR*pڍMCnlA2ʺ\+'6Cw.,t^SY/X6A;hQGtG :cV@z< ;:EL&=uӀBk 8'Z 5EtL\1 !… @i-Ҁ ]׮nH @Õ1\ku_nܩ],hG W_t6Kg ~vJ,o+&zffco 'Ij^HP3~ o$^i{Yv%@x-6N{U@@w cz D?+Ǘu"r.tMX+!_ T2~>.r'_?Zz=d p о\v㧥\~;s;5UY po7 `.}]#OԵneCǴsnc 04һXLХ n/&4,@A3h̀:¢怩v$@=gXl[@`6> kwvj~Te<~NB2~>.x|N?,>yµnm .n*&X~Lq_mSMcTT4{>Kv Y@njew@OT3uh@(pٮ`q:jFh+cX#w'Jl׃jׁvj$Lcw=VLtc؆Ygj:Pvtx`^iT!N):tzU1_Q@k$vj5A@À@֧sײ8`rz >Y_lnڍM80Oֲ@SFnE 2 Ųv|0p/2lo?ܪt*>܂ G/춹IJY>)Ҧ@*R@w|EM^]m6ӻ;u`]fMX=U G; @.`3G= d׋$(3(z'ۙ7v ~*D;>юhGJ^Nd^nuA{9nij.p$VsD;>Mg *9P̙3ڛP|@if@UV@ s`ױ=T#Jс^4<85޾&M  0@:Vv* =dzk hQ5N k =8` }MC}YUlAS t1I4xSv'r-=Ї`@. t"eIA0tvvXDk Su)54 Ȏ- @4KqD, ņmc3@XpZ0},ʜbKFtAh @š  Ħkqe ^x91clŢY (v>u@sN/Ԇ+{lZk3N@Dge5?T-J'hH"a{Q7@4@-8?˯AAl?ܪn0R[h~8KnHB0Ow;e;bmUY:;e5 r\|u (geUN:^f`a  Mro7yd̏]&)_{𰙂ϱ I^Nԥ8QuvewCtx|,cJ6'v#YqZf:nU$IA\¾ tM C``R0N-rZ9QGv|D;>юOt>'eKX&;Mјv*Q\ 𷃰Dl:֓!8>eBkxXNf:f|EM-h+bg"mK\g. JĞZV@5p?:=I{N)XW; 0'Èv|D;>юh'vlCcڽܪi.i#kʵ6.N:+矟L1L)(h@:j] 1-3 4 vP}4(Azula  :=ֈvow9j7$Bb1lc]4=oł%L 1讯mmC0}=uxW]oMpюhG^NƵ"F#ܒ@@fzxy2Y^%%BhG3;ȘPUiپ'IbM +36o&:Aǔ@%Tf@cL*gO.@;Mb) 0٧*ݭك3O{nH\mbVb;g!u랂{p~:kJ7ű!6EnH́@{;ˁ2ø~IdB@ Б&cAD;>юhG:^"ӮCDn*2|r IniKf kE  G.3Ll-@+  `?F#v|D;>.V5}OZ]G݀g09\PLcCT:=[~,<^oۂxk_8`ٵ`؆e}^$9|M/*A Q a *oRix%LUKҢn)I@IVB?-JUwo<+ytZ\RŅjPcȄ,]xff-|9~옏cע;6ߖ)P,`@XюhG#tRTv=!v# Н2m:;z sYZSU|ٮ^/@~k&W@@f||vюhLJcCcծƤɆ++ŢNferΞt"7.2h1ZI]wm66˂#zv :k)o @2$HU7юOg]юhGYR@gAn BYT>[M3hXW^(u3m|<~[f\o*Hz-xv}Rf 3 }ΦdUS8>9wٛ*#M͆o/J݄#;«D)e; /"`ʞ ˏVNH[^we,#k5+v|D;>iviewe>k(Lwp^tpݹT+~@ۻئtvkfAe>>h`f83֤d w['vюhG#| ٍm,{.RV) 6Tϻ]]o@W7?f`&֥;pr c!/k@/s ]RivvE;~]ю߮hoݹ.v5iJ@ tvҶ^evœ/+gxXs 0arDm(Db"(`kY;e[@wn"vv_R]Rhv]vE;.E8{v.@KpCB8tD%δG\4T5]+ڹl5^5֯څ!JΎNIRǫD6.D;D;>юhWX}?jѠ=`t[~VרKO#r^Mzyts]~wv g vaOgj~IW$1^v''pig,^BTnW+vvE;~Cw!hd/gu\6):R( C0f.Mf%_kh\X^$Ү kz|rϮ<܀@ שݓ_}2hZ֮]*c+vvE;~!ikg8x{ɉ~sP;,gktB/h $Nf+Za _X(صKetv|D;>юhfzZ]8[Avp@~lm@>6ePLxZn:L%GTC gP]k*\LͻsZK' 8N.i}Z]ZKe*vvE;~]ю߮howڙ{?4v4A꜅XwY;~xkNq~皮۸U *p3J`2Oysk F5 $P;sf)\ʪK}0@@ץݲL[tU]Yv_E]ю߮hoW+&7#f4NgA 0LuNYQuVbܯqm *dj>LץJvmXvCh'ڥiWR+ډv)]x{)LN/hXj{~:E#k׾P֙bU_vMe_~Ksw|ήL0v7vujj_ ehNKѮh'ڥhWRR9XK9߭׻& .~=@u f~_ض65LRkA4vuhg%ڥv|D;>юhGV;<:eMu3zv]o[k{ k!a S7x&nMOKc[v pm;v:ϗJ]ojTvqׯ|"RAv;onݡQ~CRAvv1#>}ՎdKV=njc_ }8=59t_s=2-zMXS>믩4\`yBt]6L}we,T_ ~}jb7+S ;wume_vm;v)FRٟs xGb-ՐÑQ ߵ[>5|6o;jn{ΤgJQQU BP/ w]s'm^?{o1ATrLg[Kc`u 2m#g2hajxїetF}52kGJ)VbK'MN_+WUR;OMUӆm7n~T%SËijhWC;9kg4-D>vjgx= YQ{`3k{nl;*>s]P9F+f4f~_u@ߎp6\PL7\C;_vX4 =-Q9>VO&yɔvR`@ ۥk"UajwPS*lJO3b;T1;mmoz]ߕw}g}\MfS vƀqc16xlc0`飴  `@n]cb-ŵ0a-OO~}p]POS 7GiWC/ɤ:hQJ۸4XFB7H׮ E xmr2jp_ []I~:ǐu bZ_:rgZ6wSB;?KP5 ;wvL},X@pC%+*ƕ]|q4`#cPi5*1Cokv`YduY(˪=$1 - O>8scph[;D|v&.B%:?:JCcevvCkcws2Tb-;ݎ!T3kY%GnA=#- vvMb}0gL }!v~ 8'n+}'ΈO̴}P}N&BNg鿃"v7;jz }1nԂ8\Nepؤ IDAT-dX OX c?ݢ] _GY[kw0A^vk33@c v|D;>юhG#v|D;> AAATyJ,T&څ#v|D;>юhG#v|PpZG#v|,D;v|D;>юhLJjW ;"5#v|D;>KjEfv|D;>ю]aɲL/X0u:hgG#v|D;>юhG#IAAAAÀ|RlRRD;юhG#v|D;>юhGd׀Rʘv"5#v|D;>юhG#v|t~ … (#v|D;>юhG#v|vYZ +Uvv|D;>юhG#v|D;>,ˤ    g0(ˊeLx~܋"-ٖ!19vk}nw|EVY۹ߪXCo5Zmz cdWW:˷pΰ?c0V1߷j>X c?ݢ] ݦsgv'yEV`^U/i CW_| `T\ojz<@fX>:-On}'1cs~Ƿ[k>vǤ!ZikQ?W9D8v7f U&8vɟZݰ|]l1Bl1;cl۱ k iajfİ[3.ul64 |kJtn=5h,ڶv/. *#ZaUv;7Zm P݂ Ll_zlET1ȵ1oD\}i 1춎x@ Hc7@ݘyqo/ CH݂ Q B`m#Ƕ[Ons2ЊvKV@ӌm -n hյX~'۽m4Sj)1kvXAp?#syyAAdvKVn}lcIc-vK0eFAgD)`nA4~SF(E]Ȑ t؝-յ nv}r3aۨѯ~&Nu [Υϐ ۧ Y/Mv;,:_ Cfo ~;o!CE/;C.n[s!bweͳ H `\vw;~_W/fUna`)FE[GSpʴCZA@mveYZ. ]})2 wS۽J\v#`jE ǰS icvw8Ǎ.-??c2VS2X)Jp(َ8;>g|S4T;CzNi\[Xff цڶvmp OݯJ ]­"BXAcwrRvro`5mC\vmvS=:SRVv/9zm'Ovvi{1֑~g~@vQzG^)[GYftgYgMƲWr2gYfu4R[v%oB)zc\osss[Qԡg[ M;suveZDϾ(~2)]*pT&ùע_sW ۨ~vvv|>޿ CЎc;ݢn;| v3hkr];w;^}<;P5Ol=i:@==,n?h~N[5P\w9ڙE;N˅7]xlMx6±!;{v~=Yvq2Kd: nvGJkyvF̱}jQ+BOeuw zvvv1[:$c<[>-?j)FX9@ ;ٴo"˲"U]/t3zV5k7^;v9?XV;mv@ q7Ll\ުI6ןR;S@@ǦdӾ)e6ߧ9v]TA>/ڥ96&}_KslLhhUd_AɿGY_,mxonسm ` u5vmtӇP@*&xvH;= ЖO`FaqUmK ۘN~GMG7 yڐBTnW+vvE;~5kWq,`_]<2-$vyݸUIc7=R;赝? :L v ݲ +]C\T pWiWkkprXaGlU5ehW x WbؽN^Xڥ?Xюh?V+nCxLC"B Z6)z]l`SIvSԞkBBT>V+NJvcE;ؔLaR=i-g#^@*Zf?7 0a x v7 M1 UA=t]hjo{la{= 8vSZr1h'vݮhoW+vvE;~Mל\Tu|ljb p1#Z4Ev[1Fz.|0 89/ÂT^…GXv[XvOdM\LZ4 ڥ?v]ю߮hoW+Q;c' 8zx_ٟZCk`v| YLn,zB'CJ4z|]׎k{ :۔)]=3`2 `4t8vS+S޻_8]RhvvE;~]ю߮hoW㷛ݕ)W*]zvvR;Oݐ&{h-b ̖#l4[j|T)~L!@e~2QD6.Dv]vEakpήL}E/*;2U}MXczu==t.NX;?YQW'Φ^?.x ~TkzQ_Z)*M;&m|~&}Wh-OuSRb[6mLT]юhG#$M1o_ֶ]LbQ#54?z|lߚUƉjjLѻHiN#@ Lk-j.5~6% ;w;w;vk!.v|D;>_y`-w@|c0۳nSTmq١mpf"@S)םXlKn ZpiDSų@ `ɻKΧ/WbFjRٿvE;>юhG\ހo~:,];>Gבp]>e؎@f~zsFޮ'LLJdL ?d)t }zصKetv|D;>юO S; #. Kꐛ3[TC#+SDϬ] `]G#v|:wq /#4ק!v=#v?ف?<mȝY=Pn0msf֑搢S?5 ([4GjPՔvjv: 4=b Bg  ؙE82ϦS:/t G՗t9n:ZY ]Gػay3)vJ)rg>̔r,?@/-.o~mɲE;z M!LԠ(T|<WHf#v|D;>ɵ]|v|kck]}uQҀ*;f2f굱i>dѩ۹ YvI+_^HPt. V4iE횎1͖t@=;;݅ /"T,`?=F#v|k=8zm֡;w޹$׮tB;PCVL61M5`u8)euw1o39M!4Pۭؕa  ;D;!wŀ̒ >pHP瞦ӻ̦1^P.x=]2)Nw`w}SupvZ :<> B:cpAa2(T\¥>vvW+Tz=#a?ʲb{v]UBٲ}@ǟDwLݩE*L%yFiX,2ͮ`ڮgH Գ#AANz6 ;0lkjTlZ /6=磕N3s=8;݅,,.wzPܶgpim1Ut>@n Z S<ԞZ*  Py*s>8 +EgSxmX˧E)\{ݘİaxZ!ˋ \sLNi\< :ܻz; /(8;:)_w͕,: lfwH!d}zמ>sۤz@5(L*Pz5ɵqe֌]T)$šH20{8;=UFdSxisc:^"xpU yJ,w*fU`22V ؂4G) ( BUm.cF w(MN%|$ ; Kfw,wߎoG̀lFwEN,ZM;<d^ :d@JXf@:\dYƝ `NSvel\Ì fD 0Mi'ܙbڵ{ccGT;l4ڧv  0a¶~(u3XlVz&̺t'؆د\oNϡ>C ܅ˀ@̠Aa,zʮqtfc36nH1]̙MCk4 +^.~mր|:}}2)N PA  ˜L.vfS,]kpv:f(u,^3 u{c! [ 3LL2|] b,+eк !,I!=F#v|ExJ̈v|idϮlNd/]cT__-VWn:9yNX^C]9B @FXtq9x'ەZU Mk'X4-P6,GlAuhsyp]n^MOeY2;x dQ"_|ȏAŅϾhN.v|6Bvc"ڵNlRMn}`*h gQej2lSѝ!*Uv wi_gYsC'P2}82Lsq2Kwc<{19{8O;]S}w2^ΤuJ)Rٟ]юh'vzùb۴ˤSX.):t;OS8#մ!wi`Fsmlwwu С4gg,6˩XtpSk_A ,T'nמ΀AlwߧfqI=8z kxAhdpl:v3RWcwѮ=b?YRSSb$jU5 @)up @lΘ]^wnf|tLӗm֕q.3JNeɵ;9gө9pD,8Ţ~gc$׮$n.uv|V}gf~v#\H;(M.䗯A 5 L}O}wp>gP:T;Zs 93 ڙ6.>+x^LZfk>33Q1#oXM> Xo BTAwюhb?YRsY0=[6eTg(Ttbw5;q.ΟgΚ MPLz 걹60 kjK`ߧݮʆic^u~vJϕ [(ȦHh %e;I/P(%lY?/I@@HDY> )gxNXa܌ߥ~.ͱkq&?R@}EtC*yz)pvj3(⻀O )ԹG{9 }E8WہCcDvO IDATS؜R{a%Ԃy!]0 ׄhAAFGs,xhD!J,gY1;pY#yM2 PԦ"`u4|*+A?g9ua3%3 SNs0_>;z::t&ǟf`cL/nnGmoToB{Ket]cZAD WQLʎMMk̝.9>P<@/9ڍhp@E/:I8Dg0) V e0`g;Ԃifmcq3~!GE4v/N'b`bkYQ19659Yx=gb[ @~ʌ n4h*&~ufAyY  @K}$tFxivc[X|R?E;>]nF\|Mwi}"`:Kiuԋbe҉5t(Mv` t^ & Ì9i$t;wb@ GXߥv|D4~xV⺣orHMΩ)cܕY @6E0@`c_f< =ֆO @/P;XMui´/p [bN+:pܹ F)\\Η~A}KetvwbuR :!Q5.[9XejC%@^aL? sKѣ[pt{Y64re.@7@ ;k@=P Y~}߄82,,w^…GgWrzHwp]f_?LJn. v|V}w>}.,7vtJV;Srܴ~LߦN&vir{m;fmlǗSGlN4ΩRe*=:س:}:nR@VR6m].@@W(t;an0,k?Og*(H](zO1Wt, N? [okf7~Й@@2Nj7߇v*f睛Nj7~5+tRTIz*2Ʀ^|M@ݹwptxg ;ض@ΟgxM#JLNO=Ѕj =3(ˊwRsR0~+dZ KW;6HٝsȄ2~!.x|;xz7kv@*z_>t:O<>#& rHMSF бePlSGS Hhq>]S#Ak8_ @,h9 `xܹ:]|tYed -юh@qC *cۚn:ߖs`@`l}fn|:]`Hfyk` 5Zf@= h`:fq;~:r\p0{vp/A!cwV6]U#;N%[ox{$aځmZ]Н]tutlU3V2Ο\fhjp)w ,RgXP0B 3 څ,K7fclw ҡ@tL <qff.e:юhG#vygW_/N&pQmѮ QCZݙs"ߑMvM Řz;Z H@,g>();B4@k:1-)8Pvݪ@2$ ATfݷJ}юV@N~}!¡_KP[jPT3Pi e f@{.*/[|O`oeNϨn0@AK8NG 0π4;0;_1\L@G۱66YyPC[E ԟ0d4@,燞8, \rL.H0@AVfgs>>V+ ꨛs{a8+;m) f>wxl|>ec?thFvfW>&$  bYݔ~}|rkZ@>l]osoS;o;Ǿ,:zf󉁀R0S%0@hPu-#@߿z.^bAA@* ,}AC6 @>flYf#p[)F)e ZmLrh}Z1~UMbwOiRͳ ԃewȇ   x0 goD̞d\dE6=`  @;B3S)/4 @;;ݭ8ffP;9]  >w"N&'W'oko9DCȊhTl1 OW;x^iT?u-lAR-2L@AD+OulLVa,>؆8uY3!H.؆ ؂jY%p<+2h!C}thR)3o` 蘆`pϋ}'Ç,o+hG#v|k]}uKW#_?Q;9gө12{6}em.-;ͩ]IpNN~P`i~|_v!voY3 O߃؍̀j`pAtAA)$#v|D;>d5g;6/ȟ}wri<9Gw1~$An9{ Vc| árfY>A@sR  =S -"z@@GMiP,`RzhG#v|k'Q ClteUЙu9t( }M]ڤ 1O6%6Su~>۫UY:`";Pw)M]ޤQ~'AAtP])Y,`9,`(*EOzAZ4ut ]6:J,soc*bL=Mz_S۶,y@]J$  3BO,K3e`;ze.:6z|z Tz0A9:{j:o;n:0&I#!юhG#I]mH]I݈g m1ٶqڡ-t0L ͙(ѱe{saPvTn^O z2L@A!&+bf}\f}?2K} >mg sK_nH닞M^! ].v^-eJ= 䏻2 Ξ)u&#=F#v|D;>.U5}&Үg$ną+ jP[WنSi96 P:O_#-ӥV@UZ@Y!Ԟ~\A_R7MSnƮ]*hG# O&aǦڍh+ S =[90(c^}10юhG\ހ#t:xF`Gk 6M1M__M`G- !SgsQ6[fs _݃œ,,jصKetv|D;>ю bUhS7qz:**PqzqS.@?>S֟0kĊ&=߇v*fak@2RٿvE;>юhG\سXH;+6<xן6oZ?Gw-uu;vgͼ>+;kj3;`İ{};x7jrغ.Dv]vEjs!hP|.~ '0Wb @;_X`ÖZ5i HK;eeX(xlgx 3}@8_R]Rhv]vE;.Ex;xùRmG5\iy.Xt0hѯli?nU7U7ڥ?v]ю߮hoW+v/];,.tokɢvGvݰ1 ;2M؆ m[v46P/:´N`ڙ''p~,,İ{=Yӿl<+B!I]*c+vvE;~]юnwzS ) ޓ Y=W_{֬RX3jlL4׆կ~EH1AuF֬9 œGİ{ڽwߎ]RhvvE;~]ю߮hoWۡwM3 fS-~[sƊ3Xc w0 )VЋs:#sZ5k}Ŵ Єd3n RXa= v+WSˬCgie\D;>)KevE;~]ю߮hoW+]v;^(@1L._ɿݷ^32Mfba{l!SԌ? Z;0A ffh٦ծ!i{_a:׼*5!I]*c+NJvcE;Xюlߵ6 d@-e#ڛ\Vv6 ucH"hі]:CSRhښUAJ ש]i6bO'I]*c+NJvcE;Xюl~ S:=+`%t@;DN%hFJӉQb0gl[0 G0޹5!qDPD;>юhG#t@;llwcɘuhC 2kc~uXhG9;- =PPC5\#s 0OY'oGu~D.uv|D;>юhǧvXlmj78x^N嚟,`po_})L=o1۟|Zqd-U3ޣKCwSz^_s=ع$c ̳ O;Ʈ]*{>/ڥ96&}_Ks\J)LWa?ނRy6[V@ǡ} Clu*6GW `˰p[ 2d[߉a7Zpq21nf6n8whv^}zܫc!Z?r>[}s5? n_;Bcv6]ߙi|fyYx}vvpzR0s ^ Ģ9Bkp4\Tτk["m|wR@z}c?݊ru>Nlڹ<+O&J}E/ITנ6iR5񱯻#)w^?bvùߚvvv|w><4~Ƿ[n[ !wn=mYvZ>ڮ}7}_y}f6]jJj@==,-sMjm[M >;-ߴB&i~YeV}| nWӱ8x_q}qjVv;~\}hi퇖Q*#bwSCdڅ-ڕtlX;&ߕg#^B;kG`kjz}+j\uHM)(}/.g^)^vć NwMֹ X}X{09`yWug8 jjw'2R:BAN`lN)ݡڍnk0hߕw}g} 1у mPS vm7 cK%V0@+Mz0@3]ڦu}]K>9v?O>կ/|i}XݯfÝzF';^evw?5avO1y$X*c܏j׸1ׁۻ6eY忢3ss:Шn>_)WEm}0"!qٝpfJl9/0?°˱}WWܯJ [r-cR_8v{_Uidw#KGWvv#`@gZJhv!a7c .c!cP)3`5Z={8N~,j gfo0$v Hk%+qh+۽[YZ5lߗ9$mƻc;VAZ3:v[8Q6), vN;@@d%+ nqhyv iXnAH v7&0xPEQs`Ёvc !c;%G}wdc] 08rCj]A E;kA2_oMQN@٦s?c؍s?V[S6i'p}0nc3gZW;jbw{ﻒ(w;LL2=Ϭn톨PmQlwW$~7O>E_ ;˲!8\NA.q IDATe-dX OX c?ݢ] _GY[kw0A^vk33@c v|D;>юhG#v|D;> AAATyJ,T&څ#v|D;>юhG#v|PpZG#v|D;>юhG#Pa"`3zCDfD;>юhG#v|D;>ю][ȲL/X0u:hgG#v|D;>юhG#IAAAAÀ,dA6΁hG#v|D;>юhG#ѵ 52v͈v|D;>юhG#v|D;>]p!vv|D;>юhG#v|D;>]BJ]8юhG#v|D;>юh'2) (   c ȲXD[?9^yߏ|ݣ82DVau3{} ݺ }nw|EvY۹ߪXCo:m6i=~ǵ[b>v۶_~r<]*/;)!v;>jcDݜ~7TB6s .n+}'DY|LYӹG;[ wV)>Pb~+7 mkׯy vs UP;/;qv}kݡn n~W߉v9Ϭ} Mm|=&ByEB]?t +;-nW&n=d|`@I`kׯ E!Z及N϶3z[; Է{Luh]}֏5ng0@x?6SƂݮmXMhCuf {FL:P|fvd4#>  }-:v~dW(ۥ9vl~v~XlPv@Jx :ucPmI;3kbȿv7om!Z Y7\g!bw1E q6?дں~(Ķ{vvvk'O 'G?hb}5PJ˵ v|voF;ڽ :J)ގvm]"ݯP݂ MԢ;0;!vCCEƼ7OOz'kM}D[t}3Bvt,E2}fqìvC̆ݴN>dBbȺb[?qol}bwCk8d|Kc{&|m[99PG!n Bن Y;`@qunڅ~QB`F`@Gf]vRhg#OJm*9ڽ~'\pE I ^I;Jbv.8w>N>ijwH_±еWSW&Zjcjb7|}jE&PJj ۥ]\1H] _.mv|9"1 I|OڅݤE!}ҎcNpC.Ϭm;\.@9}vZ.z V;zm1-o\vֻ ٺp&9Ae_]_x!561_P׽En)_E6oڅ-ڕtiG[]|fiӟ^7 ; {;yvZOw|Cig:V;j+ r< k hnkZ^ ~zM}wiɱ컺{۵ε/]-ںՕT;cוeBP}ߥ?v1ibd]NgoCߴ-D>ilY mjwh3i`uο!v۴k:Վڣ7I#u6[VE6L첃0A{tL}mt6 (5`oڻ핶T`Re*_G]ݢ]n_ \bc2-*m?tmҦPCM{.qi ؍o_?oj؃@  6t'cM;3VN~Ћ}}~+9vmtWC/>a/R_^ю*Kev|D;>юhǧvM>^cwS[v.h'vݮh'ڥhWR+ډv)]vc*7Bx5-v0 `KuϷ*tpUo ]]x.X([ߌ]"Y3|nx~?6׳.RٿvE;.Eh]NK:skNtEwB}pq^ v>>B2t}t[nz>w^G|=kҫnW[vw+D6[T]NkB#v|X!Nwm{C0єR;@~Wٷ+7m]W?۵XLHGԮ~*.bKc5L;d2YIhgڥvh'ډvh7 c/KyRm,7@x0>o j.T7]ih*$\ kQt .Vꦔ-rRpi@)@evvʐb['Ycw-pqry}%ݢo{sюhG#v|wpZv7~]7~#NCCFkJMBT}MtY;1|nZg*68vP_}c& wn}u& lv6Wb-ډvv|D;>юhG3${0|-O>{Zޥvf^y, /~)ҙN/&g  vw ۞dɹoPmwsMN b٭t/GRJMLRJ=:x4zH`z6L/&OnRٿvE;Ѯ NxX g>R;ӱb/G߆)L|4 .T7z Fm>ocztif@x,@ozB@w[R޹e&ihg]ڥv]vE;.E8b/]][!vu,utΘ  Ϲ*|u٩hຂvk~Gczl|2b7 Mf::ؾ]mVJaIh'vOhG#v|鎿N~k:>P>9\ Ȿ_WnBR_S;zc`Xja9#)lo@O}a;R]nvюhG#e× _~qӏWn??݂s?mln mw&hֲÖ]qG8tA hBv/N_NjljČhgڥv|D;>]_}ZEen{߆/>"j\j)zAW[>s1>nNδYhGE~V@p򖵿6ss^tա=99i#a2$:Uju+tY@xerN,|HS@/P돕mt]]uhi0|vm)–U$P +sMw]pYLюhG#vth"Kv|]vov1wM:Nhm tš5e,I[^" NwI\Vksj:I0tǢv|D;>юOW{˕TG_QюOW;99t '''AM&X,ISh 0ou:D@Z 8j\x!@(^Pe-c;!hZg9k HAAz{|pfWD;a^p " ?(Y}7.oUE輛x$28tOd}Ȳt]~l 66yVR7kUqAkpqS?9{zX,t9c il 0B+ %~߷:vN ^SwjΟgi l V ϳ:nZ{IgUG ͙MHf  ~.v1#U9!1vc"%˲bwqqv}`q`"۰\ ?C:$nm6}u`,]'  Ƿ293UX`#t6Ѯ=tvZMGzw}V3MsJ6sve4ei)`&aWXGVd  =)rvcѮ>qݘpێve@ wdMRٛc=y{XlV  zDt9|rt sz tCp&$u>#v|D;>sevfRͦvSkGܽsqAT}v':]9}+3KпX:[PnkSUgRث;ys42P ح.جi:fg^X9`<а?ejY2  Her>]T) a_aFա߾ ޹& zqS ]QaBǒ}>nR@ll4wS@jhʙt&$?Cfl =3˘olV@&&\  @i[|̈vU\) K1 a9ܽsny1d'b|߿ w5v4`s9f/ePD tZDP [ o =K!4ӂ'sԵ@5UXk:{0w 9}  "=>zTƄkíwn=mM)gq#Ћ>v gcά`]uj ^T'`;<+L{De`ݢfuц]<va 7@s S{}2M+؅z x 4C? NZ u ArgD;>юh'vL.D;>1K5BYD;>1cf^L+7&HP J)+Wd5pz[|,^IT3 v0b&]ρ-p;;@K9 l  ԉ]|L]T) a.0]cuz Bzf@]8{zX,:!!@ kyb 񈅞!@BdCtNpAA(U|YT) a>ll-<=w>M|B9V^ӿC\x#k ==lPEï3 z̀.:}>s9ۯ>D`r- x|exth0csJ]* m+s2]n>%258t8lyW >$ ^CGK͵ 6.o0E~~>E M?;4 (B"~gfߊǻw&a,sgGcw\B*lKviez N/:U}Л&p6.oiw~թ3 dtA χo  QL :x>;؄mx0œE1vW]AX%岿r0~! ]3M!ʆ!hc>mA ar_A_6"Rє¾qoJ;ׇ@PLYyz]ZdO];8ESe,eH_o:Rٟ]юhgwƴ?6tE>V;<^wt?Y7eN`Wi M:쩳R1t9gZ:%#%@*~49btA;[m ҅]ЮtAT]#;>]Юݕ79Mڣn0Nm] wqٝZ;OS0 Ĺ-P_ >~>v+6/LjNNN`:IqETR1I]I]*S.gent>wۼzk|oZw+Ux? IDATӱ^PeA+{mv6gO5qkT?g˾#]SLMHn+ۓN-/{_ ս\0R?E;>]nЎq*LC{csUG}I;'nНDt-G=NRqyXbqvZ} |}ɂxhuœ.HIЬZP(JHEI=YEP.Ѓw!qGº?Xh-?ii)(tHJU$@vTPY]]UEtLu}d#6r*͋߾\ʶ4+4֟MvS~D[4{A &(>W/oKrGZ*ktˍON>#ԲUI諾K'K[kgWδJ |~"""""*z5PnNP^}-/WߵɩjEGwD?\r٪1wc)V,.\AcEs]F8Nau<1x^IUWO /Xc\G {*y|k 7#yΆ`N| c&QG0\ya^}@%iڬ:)u2,>6]Yܧn c&VhrOrܪ+Ә->`/e#@X@YO#SA vD]G Tc+aWWK^O.C:2UȀJgWqF<| CGG'\Giϻ? 3.<{:Xj N 7766p[ѾӏΥY[5eaH iCV{nhX_ڏnQ,ﺫA9F*PQk EE]" Y&yDJ|9}2Z7tד3W;[mEYlq~^J)*d|lyy3딩 M*d7]Y/3wfˮ ٕ4y{ 5XyNX&mx/2ʏ{ܺ瑝>^~vXW{u U~O-Z7~ʳ^: -~oxyԾۍfF)" FC{g=6MQ|mvUNs~O.~"W%iT+ee# ,Oru}}˙qQB{W+|5upeVB]8û^<2@,ۑ/_]A@?~u1*@#9uu#Rn `~>Ĉ[G` 0GpȣNiCv8^⃋}\Y3K+(WÖo.F*gWuUnϻ_q]v.<;ęhOIU9W7;HW^uW^eЍ0H;Z ltNPj -w6jr2^/(V#vy?'WF/Qm?}td@TYƩ{isNC0 Uq"pO/?{>9v&u]/ϧen~ޕUYȝe7Nƙ}Tm8nʪ,d7:"`ʽ!߻vjtMZ/X>8oy Foϫ4j~u F5 'Ϙ!g~GGȽE1Mmg .(|;JhZNTX """R:iyL[Z}rٝyH]zXrAz%Edđ}b1]v.;fˮ*=c'b2!ۤ[Lpz'2[vv+YƷ72{r~\n4VZwܒwãa .[:}u^.jT::H7B`ݒW۫82 nz\nA$aQUIF.;fˎe첫RvI:L>;yy8nM3`5z/Dv 3#lU Zګ69/>4,CHZPikԇ v;8`g@IOʴav1]v.;f]ղ;sS33{ղ&eg7΀"Lbd~HJ7?0zwݓݠFGz Fd!Ʈ(q~Z:(dDDDDv3r,VmU]e#}?]֑'yp:NJ͛ FC@t ):tJ׀^NI' Ȩicv1]v.fWliT5iPvv헆X;uuf~=Y_őaOPIF?maF04*E.#ZFsGdgf1O}ht@Ҏz{ 6w6J:B ,G1BA~rϽK;Q-IF;g¸0c\ &R*ϻyϮB.;fn:,//cKLI*xe;֓3ON}*2tsu=*nZQ:Wz2F!S]o70e1Mz/>r=٠x,J;2Mi[)yϮB.;fn:d}j)M9S,); vZ!AuW̓4^g!Jœ կz@L~yrK_NvL ^@V?ڿBcc666p[Ѿs=Kgzq:':#sRv2f{K5dl(Q#<y:TO<첽>ˎecv1]v]ҧX䥌G ~4LI=w$ަԯd=xQ9⎓6fAڑJ_NN wqǷ[380Iw]_Eu0Ȯ}\f8.cve12[Dvb$#ӼiE5Dy[K4!$;y90a9`EmQ٩3d@]x!(\܉W4u8-.IdWV>.ˎecv1]vf3U`^m]Ʃ{٩/u2gd<*@$۟zev^E[.>$MZ$]/pW;,uס|?]wYUqQMzMcvnX٥]':dqr}Ӝ]zgvظ?ŶIϿi.mڗ3@6bIOX-Wzmu0i;+ͭpG@΀٥wXvߓa;ʭ?QYί$]zK3g3}\|?΀ 3~䩌|?]ˢg=nz:owӖ]z3/[.Xa无6٥wTvQ:w7mӜw茶Ee;mvp%}/. 4);YzQY_yWﴍ@tC>vGܺr%l{1igvRg}~&US:;x{%<ċz֤w<}򷶷 ;*Ya.J٥wTvyܖud9nzOY?8]sL7b%7ϻ<;˾~?t;aW<֍;^TvY5 ,3@HS<([zAei|4HSowywQ\;.Y*JzG9PiQ˒Nh6+;IpNn3IC,MdI BWWF匿wĎW{f=Nv~HiF@2a:t[]^MIN7٥7;0;wZy~S',Ic;y%MBeG@|~6M"4fv m\dVK[,?ϳ.id7j}W&m6fUznǿ_HFTim2IfCrvi[&~mT@z$Iߛv Ȏag@vy_ߝy bjUfRzQҧ&z/}z Ws{Zmfü-OW)0. |Jʣg~/6Gޛwy{;!z3;DU&y;oǩ觎j̪,U [~=}|(muمm?`0 IDATze1Y餩w%q;޻}7;w;fl"md7/;lwV^4;(y8{o;TICޣ1X79cu?BI }?)t].>fɸمͱH[$EzG~fUHZf0BP]f]P("""""LsA}pvfeC"""""Aڨev1!@DDDDDy`,1S%Ke[(@*iiTE(嵲@DDDDD3T9K XZ="""""[W!*μw7N"""""<{.,;#`2.gDDDDD&@5#Ly(< ϣޝھ?tի溑+U^oG?QB;Dcx:|-LK4Ez>m[t""""""4mghr@h1wXyhu """""i }[6kwGez/=oHMWx΀Z6@>Vj3[ojzP|uY#"""""*Cg] U$ ˪;LDU!W!]ziCDDDDD7e}eҿ/eg0;""""""S΀N&pÃyMek*rvQWLZ"""""I˘mtjcCw_م߀w6l_mKG-jvKB﫟aa^zd ;}#Q pww0Mxo:xACwyӁwA]<σsőO,QʉUs]Yt}-ɲNcJMR:@M}ʧk.:?;@'vE0@Gӝ]\Ywe]~'i)OcF}Uxgt /M~eWl}Åf^σ.j\Ok!u{kvS{I0;ƿ} ~$$Ԑj :6n0Xz\j.~]FQ+w~yԕ< """""B }eS/=} 6[tm~o v֡&;Gw;ErFe7ɞ>}y2?uݸǤQ%uנ2 0}Fgg[.6~p;_b߯,v%dNlnskSЋO:b@W}P"""""*D'օm]p][7<8]x w׭V ]lwll<Ԝd+ $i uÆG {ġ(¨c&yhR&Կ<Οo½4VpucOmM7^d5Jn#+@dN׀_y$#[vh?w5= uF|o~<.=tTh=0i{k6k<˓V-%@DDDDDEa<σCܽZawv;'xo:ht]]nݟٰllmoV ewcS﷯i*+͇:z=C}&ۺmw{ }2{MP7_yi u]K2^޸zhvo04/l`j“'30n5//aٵZ-ϛp]x7N7ľCueU<ҁVX -1yyh|v\qe0zo .ytٵ ;V+x@.v=zJrsVx:V`WX`vF6pÃW&bKAVvj:GR7Q}-+DDDDDT M<ehzU[|pA]M~vI^v_^p:nDDDDDDUP _EMpwuV&9˻#<8UvQTbAkCp7ܘ5-}VW5ލNm\G gt0v Q#"""""*CF||Q/x~ʇa6G@։VW0iNDDDDD4i P}eajJvqWu_dۦWO """"U6"YT:~–ۦ9DDDDDT@K4 ^]W7zQQ:0!@DDDDDeagM=]>l"A᯾OD퇈 @SO4> {`T94 H:T?jqMs? """"3¸s[7 *B@DDDDDeLg@VÙ'g.ԙt h׺<@Nj+/QJ X 6 }}[X==s"V"""""222`K_c&:}q]x,X-X<yVQ {mAv;9.騁<UEjQ7E}p{Lv3yǁ y76.re-/-b=Q7<8OvF}yIOjJ3`wEZzh}A.}ë+3};O-a gۻmjh\fvj:}$s dy2AQ.$"""""*C+KϏi}ov]>l~xnk`o ~d@]&9]4Z g a곋k@q$9NڲMZn~w`&Vև0n7`&e=^x=됅mw0nx1G=CAv;M,ե^dwgWf?#RȀϬg?`:duBijn0 <`yh|ޛ.0u?۶q\|~'1 և-wٵZ-t]hv v{z0uݜ1ցQDDDDDT:יŅvР# 0Mu4L3 W;p\5/~@}VQE/]hv,+0Mmv=o od@<q)Q!"""""npWj 0hx E_XGp\k\t]=u}ͻZ4p*f}&v³{}/w13=~c΀Ϭ}u؂q`w˰[8'N$`\, QDݾ>o:\^vweWS y7 """"2еm0A {u>5l6|o;?y`6h뾅"_KRv5}!:llȆwm/{Yw6'՘81ԲrTFuOƭf=ݗmx7=Xmw f80p; /<"Poo= f0݋u`sdz^YADDDDDT%L hf0CZ&FV+x@whN`{놁zme/u-]\G > { t?fPO![[X>XDl@4aux׺σyc_3]8`k{ ;)CLynoTQ7 EujY9@DDDDDe+i?wF5 8NGOg@a^wa &:{{Z}>$3߇i.n=޸I.K\1@DDDDDE+dd|+,m̞806 7 0n5p@ɮtgXv;nGQ  [GYҧh4p?^OWF]Y!@DDDDDe*ld@{[}yi8EXނu؂}Նe`x.Y~1x _`wkR!E#.ݡN4F$ݏ*Y [/>DDDDDDVX6hcmqW n7{>cz04&wO}q|)) qvkLOvthЇ]u&qhR* +G zLm:Aal}1x<ޑEXÿͲjP󀁥nKGbۧd"..c.khE6@qu"˟ǕDDDDDTR P-||lcXP^#~{0h/,uN kh`(&.v_SUe'ן>~u$AԜIR'Dgu[X"WV.pk{hB6w5`hKh~(z]_c巏\u9٩uQi֕ߋ*;l h?ІZw6jO`Kt e0go{[4ƹ?zNݝ]\Z7@ْsQ*ֽM4 6ƭ@ >d60n 67ZjR{ÈIDDDDDDy rWyH|wgo}>늪 pybridge-0.3.0/pixmaps/table.png0000644000175000017500000001272310467145050016372 0ustar michaelmichaelPNG  IHDR@@iqsBIT|dtEXtSoftwarewww.inkscape.org<eIDATxݛi\UsVUWW=tw$,$$$ IQYTEF@G: BA? ?W &!ʒd%Wu{]|NwuUwp/{Ͻs#|Pu|X,']]ck6;GJ]^ȴ} h]gQ9-TUS^QF4 qp(roW@5ꁣ@F\2f0  0 EQ(-q8֝麪} hRfٰi@/C ,fŊu!xÙP}_=أoѲuC(5LWu|}ZlG*.{!7RVPVfi;ݢ&zێKWSxgM{LiZ@"Ĕ& d7]S}Z|y ppoAG#D~_M)/R ,1B4=fۃ[pGs})MR#D\\{?y&$淡D Jv+@w{_C8JJa$)ROջSѫ7QlOJƂD\_Ջ~q 7KARZZB8eђ3t vG4 r!-6IBis[3E& CNX^Upahc"3 "?yWK)J K }HC2!Mbw8$SYU-8Ntta&>پ\[nR*|ܼ"q- RPeQEb@R: |u]5;vC롸IqyU!b O'zy-j/&xχ]ji'^fwsx5i*2B'"Uxƥm[j;؍FO%(jO o&i\d kA8kQ^Yю'͆UQS/ξ\ҵ(Un0 M1a x/l 8@CJsJA~+l::1B鯴6bwWL!5E7F]}LK1NO9$'3C ]hd3 gd,nj_'',()s Pm $gZ}+i .ӣ8nӹOja7A0Pdu?omFtВ aX(H*uώ N)uKVA0/{ueT6sFOm係#%Xqοd{*IS(ilnxG?PCO ޴?{b b?* v/*+J;i5(ʰӿI4&F`=Hl8@_QYK$`N<A74El{qݔJY}C-|h1Pݐcs `H9#RxAX=$GwYƋv8ط/r,73mڶbwzBjږ?ԍnhxrl}i Z-_ý}cx$߳LZ֬@PQʫkXmyuM#8ΤorEgO9ڛ8i=qTu$RO2ƶ Ek} /$tnZd+rbyA/iwCr4C#_7m>:mف>޸iIG,/QTmL aV] # ZX,*iHGܶaeK9QC'?Slsu1Q=cB lŀ@SJSVT~޺{] O#NP^ 62ܹ{q8J9D3@Q4jgZwj0`$@FᐻuMVUUyOh@O18LGD: 1wy/>+U;c|A緙3ZʦMsb9S\ڲE;-tchFüωiI[\/R[g7pR:J+pwGzEm-F0h .ze_~b:0?܎:MCKW&#!_S\}'κQ ~YMQn:޺6?JLՂݩi)Wh6۔pӯhOuCaĒa|H}(0L"(5NM8ҹ&qmZ`׋)7Ȼ&Mϼޔƣ"1$'%cn@ }t nzrIHLަ$oTث.WbFJ>qLT *zh*s*"{at\98J]4jAKixL*OޞqRbHq-ws# 6 7\@͔Uv9odΏK)d0I '". I%Jg`4NCJ#D:*XxWH%S RZb<. kR@ _0K3gn+`*B?dBOh*~R}c#J?r "⟙ʓt kvR,u(q!Mþ̞rRc=^ boͅäETZ;?==8~!pu۩k~syGD]גF-=E$r5wg~;ZUkuPvL](g>R(",ǩ>L]EO9Ƶrf  u`7紎zYkT4ZjM/ʧ+ KrWQ[_Cilyu0(J#=% tG? wbGt[X(rRUW͙[8#]HIJQUݹP&Mwn yDH8kӛ8t0^W/pg;kAT{DK8GhYEMϓþݴxLST(gw~؊BTTvƮLW(VM}C8ieL5"͟p6(`0̰;Se֯X,|NIS$umQx΃5댧()4(JS0s>^Qk9"1BGti,h!fSY& tUegngsZlGw@bQmAU7_ð Ro㞡3V 3kƦ?xzF}.tCQ:3̣w8oilEmfZ5ꑼ`a.aN *wrGu#5#*kkX4#gi޼= pL[x=?c=W^'p8ty:G 8y]09qBd(jaz,ss)C8yJm5=þ[:#t%(-a0t<q #4֬8ׂN,fL"P[QHc*jLMCQ֠+'ixXˌ\Ωyl-Ii+%Xߏ xS)eWԛrTH$rjϔm桨UFW#w¸x  :HI4 XoH&AO".`tGHƢDtį#󽩐Q㛲*[#1*&\ 令|ֹiXs}y=C>02]4Ze )/3).-tdצu>9E.X f)Ry#+{ծ_9؂EcEM! I+RunZ7RҺfJ`3@qY9ѤcI4ϔIj]{T ӉjCQU MCK%ID"Q%9?8_+`$}lWM3fCp,8@:Xx}ݩ%  PIENDB`pybridge-0.3.0/pixmaps/user.png0000644000175000017500000000430110467145144016256 0ustar michaelmichaelPNG  IHDR@@iqsBIT|dtEXtSoftwarewww.inkscape.org<SIDATx՛Kw?3~{_&fC+괛nR*D j@*"!"ąC{@q aU@JTP҈QU!mM6jj3&Mk{36kό=g6+Y+?g~_0 Q*ׁG}>\"KW,@ H3'O{1Ʊ1>10 *&|RYŎŒSIwsgyeto~6B<Rw?{fǏH$P,s5T1TűX];|BXWYP6kn|b'|yMY&ֳo{EB7^a*<ȒC5Lw"0ǎ?|TǬy&ᔴT"Tk\JUNOƜ^N3q+ܢ'4._EӃԔ[tq9Cu{;\{:J-6ƈ_ p8o渹wb1 y}{E]׹z:]@15 "D]:$"m bzq:Kpkcp߾ @<Etn yy7"Cx;}D 쐦il.Rr:Gb$PZC `EnDbq&vr2GjUt@CĒ@ 8(KhЬ-/0@D̠En КM79(?ա\@ZoUoӻv01ƒ@pRn_dj ZX4aB8|;Z*:NHAIњvqc ald-g0UmA={]{ֵ|r`D+Py;Q aPl V m49C* XQrooPW5ҫ à\S,9%gWLW tx(N-pkun5YfTڦ +Ap =\Sظ50 jy^fCEPdGx0 a7Z_܂3߁R8XOTk6UREVd'9 rʰxVkpa^e:zǁcz* C!D1a躆i4umon!, !`*گ#(9 Ԛ {#Jg 3\^&rGpElBh@e>b<`;^:Y3tKgƴųųijijŴiGa=zsMy}]V/AY2F¢{[V/ N%{\|z]78ĦM[6o;нN!ŭz[BsR-uU-lJ ( @ hBpponmllkjiihhgfvVO&4J rСu̝pʜpʚnƘmƘlÖl”jjihfeddP)J $tϟr̝pʜpʚnƘmƘlÖl”jihgfedcv]8M$;Ɯxϟr̝pʜpʚnƘmƘlÖl”jihgfedcpiDU-Z•o̚m̝pʜpʚnƘmƘlÖl”jihgfedckqNS)p”lZbȘlʚnƘmƘlÖl”jihgfedcftRN$|ŖlZXX\bg•k”jihgfedcctQU,‘fZXXWVVVZ^bffedcbtQJ ^4aZXXWVVTT~S~R|Q~TX^cbxTJ e;]ZXXWVVTT~S~R|Q{PyPxOyPY|WJ 5h=־ZZXXWVVTT~S~R|Q{PyPxOwNvMrLP&Qj=ZZXXWVVTT~S}R|Q{PyPxOwNvMrJP&j\1j>j>i>i=h^=]=\<\<]K#O%Y\|RƮpMqMc>J xS~TīuS`X2qK괅^Ǭ|\fS+m_8mƬ}]|^V//Q(sàū}\vWJ O%5vǨfnLJ e˯qZ4xP&X\6J ǫb>7Ũ_:3¤N'ĸĸķĶö´gtSa~N!ʽɽ}}ȻvV@ĶhqPZsRZsSZtTZvVZiG-yiɽ˿ӽc?(@ @J #R)L#L#L#L#L#L#M#M#L"L#L#L#L#L#L#M#M#M$M$M$M$M$M$M$N%N%N%N%N%N%M$O&J !O&ɺsηѻϹθ͸̷̶˵ɴȳDzDzƱİïî­ӾӾҽѼлκ͹̸˷ʶɵŰbP'J U-̴Өϟr͝q̝p˜pʛoʛoɚnǙmƘmƘmŗlĖl•k•jkjjiihhggfeeeedlįM%J J 'a;¥ϟrϟr͝q̝p˜pʛoʛoɚnǙmƘmƘmŗlĖl•k•jjiiihggffeeddccb«T-J J ‘eZZZYXXXWWVVVUUTTTS~S~R}R|Q|Q{Q{PyPyP~U\bcbalb`=^=]<]<\<\<\;[;[;Z:^>Y7O'J od:]}R}R|R{Q{QzPzPxOuLpJmGjFhDfBdBdAcAcAcAb@b@a@a@`?`?_>_>^>^>^=]=]=\b?c>a>a=b=`<_<_;^;^;^;]:]:\9\9\8[8Z8Z8Z7Y7[9]=]=\pI!Fŷb=mIB´`Ծ^8dK:ѺϷO(AL!6ζuUM2̲fc>nI$fīz}^ʹiEuN!mKʰտӼѹ϶zϷd{c{]bxfC@yZoNqPqPrQsRsSsStTuUuUvVkI`<kItSQ)6ͳd@joMgԵvݵuܴt۳r֟oNJ ??????????????????pybridge-0.3.0/pixmaps/baize.png0000644000175000017500000000072610423140145016365 0ustar michaelmichaelPNG  IHDR22?gAMA1_IDATxˎ0c $3삲ymu$EƘ04h. J)\8}Yk-]F3˲8y۶;0`&Icu.˒q ZTIs:(pq$MSv85h)˒iq %3{OZ<F\Dv]dEv]dEv]dEv]dEv]dEv]dEv]dEv?t*tIENDB`pybridge-0.3.0/pybridge/0000755000175000017500000000000010637763637014734 5ustar michaelmichaelpybridge-0.3.0/pybridge/bridge/0000755000175000017500000000000010637763637016170 5ustar michaelmichaelpybridge-0.3.0/pybridge/bridge/deck.py0000644000175000017500000001343610610463033017431 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from copy import copy from operator import mul from random import shuffle from card import Card from symbols import Direction, Rank, Suit # See http://mail.python.org/pipermail/edu-sig/2001-May/001288.html for details. comb = lambda n, k: reduce(mul, range(n, n-k, -1)) / reduce(mul, range(1, k+1)) # TODO: consider making Hand a subclass of List, with additional constraints. class Deck(object): """A Deck object provides operations for dealing Card objects. A hand is a collection of 13 cards from the deck. A deal is a distribution of all 52 cards to four hands. A deal is represented as a dictionary, mapping Direction labels to lists (hands) of Card objects. There are exactly 52! / (13!)**4 (comb(52,13) * comb(39,13) * comb(26,13)) distinct deals of 13 cards to 4 positions from a standard 52-card deck. """ cards = [Card(r, s) for r in Rank for s in Suit] cardSeq = copy(cards) cardSeq.sort(reverse=True) # Required order: Ace of Spades -> Two of Clubs. Nmax = comb(52, 13) Emax = comb(39, 13) Smax = comb(26, 13) D = Nmax * Emax * Smax def isValidDeal(self, deal): """Checks that structure of deal conforms to requirements: * 4-element dict, mapping Direction objects to hand lists. * Hand lists contain exactly 13 Card objects. * No card may be repeated in the same hand, or between hands. * The cards in hands may be in any order. @param deal: a deal dict. @return: True if deal is valid, False otherwise. """ return True # TODO - if invalid, perhaps give reason def randomDeal(self): """Shuffles the deck and generates a random deal of hands. @return: a deal dictionary. """ shuffle(self.cards) hands = {} for position in Direction: hands[position] = [] for index, card in enumerate(self.cards): hands[Direction[index % len(Direction)]].append(card) for hand in hands.values(): hand.sort() return hands def dealToIndex(self, deal): """Computes the index which corresponds to the specified deal. This implements the "impossible bridge book" encoding algorithm by Thomas Andrews, see http://bridge.thomasoandrews.com/impossible/. @param deal: dict representing a valid deal. @return: integer in range 0..D-1 """ assert self.isValidDeal(deal) cardSeq = copy(self.cardSeq) # Make a copy for modification. indexes = {} # For each hand, compute indexes of cards in cardSeq. for position in (Direction.North, Direction.East, Direction.South): indexes[position] = 0 deal[position].sort(reverse=False) # It is desirable to remove cards from cardSeq when adding their # indexes, instead of doing so in an extra step. # Removing cards backwards preserves the indexes of later cards. for i, card in enumerate(deal[position]): indexes[position] += comb(cardSeq.index(card), 13-i) cardSeq.remove(card) # Deal index = (Nindex * Emax * Smax) + (Eindex * Smax) + Sindex indexes[Direction.North] *= self.Emax * self.Smax indexes[Direction.East] *= self.Smax return long(sum(indexes.values())) def indexToDeal(self, num): """Generates the deal which corresponds to the specified index. This implements the "impossible bridge book" decoding algorithm by Thomas Andrews, see http://bridge.thomasoandrews.com/impossible/. @param num: integer in range 0..D-1. @return: dict representing a valid deal. """ assert type(num) in (int, long), "index must be an integer" assert 0 <= num < self.D, "index not in required range" cardSeq = copy(self.cardSeq) # Make a copy for modification. deal = {} # Split index into hand indexes. indexes = {Direction.North : (num / self.Smax) / self.Emax, Direction.East : (num / self.Smax) % self.Emax, Direction.South : (num % self.Smax) } for position in (Direction.North, Direction.East, Direction.South): deal[position] = [] for k in range(13, 0, -1): # Find the largest n such that comb(n, k) <= indexes[position]. n = k-1 # n < k implies comb(n, k) = 0 # comb(n+1, k) = # n-k = -1 => comb(n, k) * (n+1) # otherwise => (comb(n, k) * (n+1)) / (n+1 - k) while comb(n+1, k) <= indexes[position]: n += 1 # Remove card index from indices, add card to hand. indexes[position] -= comb(n, k) card = cardSeq[n] deal[position].append(card) cardSeq.remove(card) deal[Direction.West] = cardSeq # West has the remaining cards. return deal pybridge-0.3.0/pybridge/bridge/game.py0000644000175000017500000003532010637437026017444 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License or (at your option) any later version. # # This program is distributed in the hope that it will be useful # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not write to the Free Software # Foundation Inc. 51 Franklin Street Fifth Floor Boston MA 02110-1301 USA. from twisted.spread import pb from zope.interface import implements from pybridge.interfaces.game import ICardGame from pybridge.interfaces.observer import ISubject from pybridge.network.error import GameError from bidding import Bidding from board import Board from playing import Playing from scoring import scoreDuplicate from call import Bid, Pass, Double, Redouble from card import Card from symbols import Direction, Suit, Strain, Vulnerable class BridgeGame(object): """A bridge game models the bidding and play sequence. The methods of this class comprise the interface of a state machine. Clients should only use the class methods to interact with the game state. Modifications to the state are typically made through BridgePlayer objects. Methods which change the game state (makeCall, playCard) require a player argument as "authentication". """ implements(ICardGame, ISubject) # Valid positions. positions = Direction # Mapping from Strain symbols (in bidding) to Suit symbols (in play). trumpMap = {Strain.Club: Suit.Club, Strain.Diamond: Suit.Diamond, Strain.Heart: Suit.Heart, Strain.Spade: Suit.Spade, Strain.NoTrump: None} def __init__(self): self.listeners = [] self.board = None self.bidding = None self.contract = None self.play = None self.boardQueue = [] # Boards for successive games. self.visibleHands = {} # A subset of deal, containing revealed hands. self.players = {} # One-to-one mapping from BridgePlayer to Direction. # Implementation of ICardGame. def start(self, board=None): if self.inProgress(): raise GameError, "Game in progress" if board: # Use specified board. self.board = board elif self.board: # Advance to next deal. self.board.nextDeal(result=self) # TODO: proper GameResult object. else: # Create a board. self.board = Board() self.board.nextDeal() self.bidding = Bidding(self.board['dealer']) # Start bidding. self.contract = None self.play = None self.visibleHands.clear() # Remove deal from board, so it does not appear to clients. visibleBoard = self.board.copy() visibleBoard['deal'] = self.visibleHands self.notify('start', board=visibleBoard) def inProgress(self): if self.play: return not self.play.isComplete() elif self.bidding: return not self.bidding.isPassedOut() else: return False def isNextGameReady(self): return (not self.inProgress()) and len(self.players) == 4 def getState(self): state = {} if self.inProgress(): # Remove hidden hands from deal. visibleBoard = self.board.copy() visibleBoard['deal'] = self.visibleHands state['board'] = visibleBoard if self.bidding: state['calls'] = self.bidding.calls if self.play: state['played'] = [] trickcount = max([len(s) for s in self.play.played.values()]) for index in range(trickcount): leader, cards = self.play.getTrick(index) for pos in Direction[leader.index:] + Direction[:leader.index]: if pos in cards: state['played'].append(cards[pos]) return state def setState(self, state): if state.get('board'): self.start(state['board']) for call in state.get('calls', []): turn = self.getTurn() self.makeCall(call, position=turn) for card in state.get('played', []): turn = self.getTurn() # TODO: remove this hack if turn == self.play.dummy: turn = self.play.declarer self.playCard(card, position=turn) def updateState(self, event, *args, **kwargs): allowed = ['start', 'makeCall', 'playCard', 'revealHand'] if event in allowed: try: handler = getattr(self, event) handler(*args, **kwargs) except GameError, e: print "Unexpected error when updating game state:", e def addPlayer(self, position): if position not in Direction: raise TypeError, "Expected Direction, got %s" % type(position) if position in self.players.values(): raise GameError, "Position %s is taken" % position player = BridgePlayer(self) self.players[player] = position self.notify('addPlayer', position=position) return player def removePlayer(self, position): if position not in Direction: raise TypeError, "Expected Direction, got %s" % type(position) if position not in self.players.values(): raise GameError, "Position %s is vacant" % position for player, pos in self.players.items(): if pos == position: del self.players[player] break self.notify('removePlayer', position=position) # Implementation of ISubject. def attach(self, listener): self.listeners.append(listener) def detach(self, listener): self.listeners.remove(listener) def notify(self, event, *args, **kwargs): for listener in self.listeners: listener.update(event, *args, **kwargs) # Bridge-specific methods. def makeCall(self, call, player=None, position=None): """Make a call in the current bidding session. This method expects to receive either a player argument or a position. If both are given, the position argument is disregarded. @param call: a Call object. @type call: Bid or Pass or Double or Redouble @param player: if specified, a player object. @type player: BridgePlayer or None @param position: if specified, the position of the player making call. @type position: Direction or None """ if not isinstance(call, (Bid, Pass, Double, Redouble)): raise TypeError, "Expected Call, got %s" % type(call) if player: if player not in self.players: raise GameError, "Player unknown to this game" position = self.players[player] if position not in Direction: raise TypeError, "Expected Direction, got %s" % type(position) # Validate call according to game state. if not self.bidding or self.bidding.isComplete(): raise GameError, "No game in progress, or bidding complete" if self.getTurn() != position: raise GameError, "Call made out of turn" if not self.bidding.isValidCall(call, position): raise GameError, "Call cannot be made" self.bidding.makeCall(call, position) if self.bidding.isComplete() and not self.bidding.isPassedOut(): self.contract = self.bidding.getContract() # TODO: make a property trumpSuit = self.trumpMap[self.contract['bid'].strain] self.play = Playing(self.contract['declarer'], trumpSuit) self.notify('makeCall', call=call, position=position) # If bidding is passed out, reveal all hands. if not self.inProgress() and self.board['deal']: for position in Direction: hand = self.board['deal'].get(position) if hand and position not in self.visibleHands: self.revealHand(hand, position) def signalAlert(self, alert, position): pass # TODO def playCard(self, card, player=None, position=None): """Play a card in the current play session. This method expects to receive either a player argument or a position. If both are given, the position argument is disregarded. If position is specified, it must be that of the player of the card: declarer plays cards from dummy's hand when it is dummy's turn. @param card: a Card object. @type card: Card @param player: if specified, a player object. @type player: BridgePlayer or None @param position: if specified, the position of the player of the card. @type position: Direction or None """ if not isinstance(card, Card): raise TypeError, "Expected Card, got %s" % type(card) if player: if player not in self.players: raise GameError, "Invalid player reference" position = self.players[player] if position not in Direction: raise TypeError, "Expected Direction, got %s" % type(position) if not self.play or self.play.isComplete(): raise GameError, "No game in progress, or play complete" playfrom = position # Declarer controls dummy's turn. if self.getTurn() == self.play.dummy: if position == self.play.declarer: playfrom = self.play.dummy # Declarer can play from dummy. elif position == self.play.dummy: raise GameError, "Dummy cannot play hand" if self.getTurn() != playfrom: raise GameError, "Card played out of turn" hand = self.board['deal'].get(playfrom, []) # Empty if hand unknown. if not self.play.isValidPlay(card, playfrom, hand): raise GameError, "Card cannot be played from hand" self.play.playCard(card) self.notify('playCard', card=card, position=position) # Dummy's hand is revealed when the first card of first trick is played. if len(self.play.getTrick(0)[1]) == 1: dummyhand = self.board['deal'].get(self.play.dummy) if dummyhand: # Reveal hand only if known. self.revealHand(dummyhand, self.play.dummy) # If play is complete, reveal all hands. if not self.inProgress() and self.board['deal']: for position in Direction: hand = self.board['deal'].get(position) if hand and position not in self.visibleHands: self.revealHand(hand, position) def revealHand(self, hand, position): """Reveal hand to all observers. @param hand: a hand of Card objects. @type hand: list @param position: the position of the hand. @type position: Direction """ if position not in Direction: raise TypeError, "Expected Direction, got %s" % type(position) self.visibleHands[position] = hand # Add hand to board only if it was previously unknown. if not self.board['deal'].get(position): self.board['deal'][position] = hand self.notify('revealHand', hand=hand, position=position) def getHand(self, position): """If specified hand is visible, returns the list of cards in hand. @param position: the position of the requested hand. @type position: Direction @return: the hand of player at position. """ if position not in Direction: raise TypeError, "Expected Direction, got %s" % type(position) if self.board and self.board['deal'].get(position): return self.board['deal'][position] else: raise GameError, "Hand unknown" def getTurn(self): if self.inProgress(): if self.bidding.isComplete(): # In trick play. return self.play.whoseTurn() else: # Currently in the bidding. return self.bidding.whoseTurn() else: # Not in game. raise GameError, "No game in progress" def getScore(self): """Returns the integer score value for declarer/dummy if: - bidding stage has been passed out, with no bids made. - play stage is complete. """ if self.inProgress() or self.bidding is None: raise GameError, "Game not complete" if self.bidding.isPassedOut(): return 0 # A passed out deal does not score. contract = self.bidding.getContract() declarer = contract['declarer'] dummy = Direction[(declarer.index + 2) % 4] if declarer in (Direction.North, Direction.South): vulnerable = (self.board['vuln'] in (Vulnerable.NorthSouth, Vulnerable.All)) else: # East or West vulnerable = (self.board['vuln'] in (Vulnerable.EastWest, Vulnerable.All)) tricksMade = 0 # Count of tricks won by declarer or dummy. for index in range(len(self.play.winners)): trick = self.play.getTrick(index) winningCard = self.play.winningCard(trick) winner = self.play.whoPlayed(winningCard) tricksMade += winner in (declarer, dummy) result = {'contract' : contract, 'tricksMade' : tricksMade, 'vulnerable' : vulnerable, } return scoreDuplicate(result) class BridgePlayer(pb.Referenceable): """Actor representing a player's view of a BridgeGame object.""" def __init__(self, game): self.__game = game # Access to game is private to this object. def getHand(self): position = self.__game.players[self] return self.__game.getHand(position) def makeCall(self, call): try: return self.__game.makeCall(call, player=self) except TypeError, e: raise GameError, e def playCard(self, card): try: return self.__game.playCard(card, player=self) except TypeError, e: raise GameError, e def startNextGame(self): if not self.__game.isNextGameReady(): raise GameError, "Not ready to start game" self.__game.start() # Raises GameError if game in progress. # Aliases for remote-callable methods. remote_getHand = getHand remote_makeCall = makeCall remote_playCard = playCard remote_startNextGame = startNextGame pybridge-0.3.0/pybridge/bridge/__init__.py0000644000175000017500000000000010573061212020242 0ustar michaelmichaelpybridge-0.3.0/pybridge/bridge/board.py0000644000175000017500000000525110635716400017614 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License or (at your option) any later version. # # This program is distributed in the hope that it will be useful # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not write to the Free Software # Foundation Inc. 51 Franklin Street Fifth Floor Boston MA 02110-1301 USA. import random import time from deck import Deck from symbols import Direction, Vulnerable class Board(dict): """An encapsulation of board information. @keyword deal: the cards in each hand. @type deal: Deal @keyword dealer: the position of the dealer. @type dealer: Direction @keyword event: the name of the event where the board was played. @type event: str @keyword num: the board number. @type num: int @keyword players: a mapping from positions to player names. @type players: dict @keyword site: the location (of the event) where the board was played. @type site: str @keyword time: the date/time when the board was generated. @type time: time.struct_time @keyword vuln: the board vulnerability. @type vuln: Vulnerable """ def nextDeal(self, result=None): """Generates and stores a random deal for the board. If result of a previous game is provided, the dealer and vulnerability are rotated according to the rules of bridge. @param result: @type result: """ deck = Deck() self['deal'] = deck.randomDeal() self['num'] = self.get('num', 0) + 1 self['time'] = tuple(time.localtime()) if self.get('dealer'): # Rotate dealer. self['dealer'] = Direction[(self['dealer'].index + 1) % 4] else: # Select any player as the dealer. self['dealer'] = random.choice(Direction) if result: # TODO: proper GameResult object. # TODO: consider vulnerability rules for duplicate, rubber bridge. #if result.bidding.isPassedOut(): # self['vuln'] = result.board['vuln'] #elif result.getScore() >= 0 self['vuln'] = Vulnerable[(result.board['vuln'].index + 1) % 4] else: self['vuln'] = Vulnerable.None # The default value. pybridge-0.3.0/pybridge/bridge/symbols.py0000644000175000017500000000562110610463033020210 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ This module contains enumeration types used for the implementation of bridge. The particular ordering of the values in each enumeration is assumed throughout PyBridge, so it is vital that the order is not changed. """ from twisted.spread import pb from pybridge.enum import Enum, EnumValue class WeakEnumValue(EnumValue, pb.Copyable, pb.RemoteCopy): """A variant of EnumValue which may be copied across the network. Since the enumtype reference (an Enum object) cannot be maintained when this object is copied, it is discarded. An undesirable side-effect is that comparisons between WeakEnumValue objects with identical indexes and keys (but belonging to different Enum types) will result in True. """ enumtype = property(lambda self: None) def __repr__(self): return "WeakEnumValue(%s, %s)" % (self.index, self.key) def __cmp__(self, other): try: assert self.key == other.key result = cmp(self.index, other.index) except (AssertionError, AttributeError): result = NotImplemented return result def getStateToCopy(self): return (self.index, self.key) def setCopyableState(self, (index, key)): # self = WeakEnumValue(None, index, key) self.__init__(None, index, key) pb.setUnjellyableForClass(WeakEnumValue, WeakEnumValue) # Bid levels and strains (denominations). Level = Enum('One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', value_type=WeakEnumValue) Strain = Enum('Club', 'Diamond', 'Heart', 'Spade', 'NoTrump', value_type=WeakEnumValue) # Card ranks and suits. Rank = Enum('Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Jack', 'Queen', 'King', 'Ace', value_type=WeakEnumValue) Suit = Enum('Club', 'Diamond', 'Heart', 'Spade', value_type=WeakEnumValue) # Player compass positions, in clockwise order. Direction = Enum('North', 'East', 'South', 'West', value_type=WeakEnumValue) # Vulnerability indicators. Vulnerable = Enum('None', 'NorthSouth', 'EastWest', 'All', value_type=WeakEnumValue) pybridge-0.3.0/pybridge/bridge/playing.py0000644000175000017500000002041710610463033020163 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from card import Card from symbols import Direction, Suit class Playing(object): """This class models the trick-taking phase of a game of bridge. This code is generalised, and could easily be adapted to support a variety of trick-taking card games. """ # TODO: tricks, leader, winner properties? def __init__(self, declarer, trumpSuit): """ @param declarer: the declarer from the auction. @type declarer: Direction @param trumpSuit: the trump suit from the auction. @type trumpSuit: Suit or None """ if declarer not in Direction: raise TypeError, "Expected Direction, got %s" % type(declarer) if trumpSuit not in Suit and trumpSuit is not None: # None => No Trumps raise TypeError, "Expected Suit, got %s" % type(trumpSuit) self.trumps = trumpSuit self.declarer = declarer self.dummy = Direction[(declarer.index + 2) % 4] self.lho = Direction[(declarer.index + 1) % 4] self.rho = Direction[(declarer.index + 3) % 4] # Each trick corresponds to a cross-section of lists. self.played = {} for position in Direction: self.played[position] = [] self.winners = [] # Winning player of each trick. def isComplete(self): """Playing is complete if there are 13 complete tricks. @return: True if playing is complete, False if not. """ return len(self.winners) == 13 def getTrick(self, index): """A trick is a set of cards, one from each player's hand. The leader plays the first card, the others play in clockwise order. @param: trick index, in range 0 to 12. @return: a (leader, cards) trick tuple. """ assert 0 <= index < 13 if index == 0: # First trick. leader = self.lho # Leader is declarer's left-hand opponent. else: # Leader is winner of previous trick. leader = self.winners[index - 1] cards = {} for position in Direction: # If length of list exceeds index value, player's card in trick. if len(self.played[position]) > index: cards[position] = self.played[position][index] return leader, cards def getCurrentTrick(self): """Returns the getTrick() tuple of the current trick. @return: a (leader, cards) trick tuple. """ # Index of current trick is length of longest played list minus 1. index = max(0, max([len(cards) for cards in self.played.values()]) - 1) return self.getTrick(index) def getTrickCount(self): """Returns the number of tricks won by declarer/dummy and by defenders. @return: the declarer trick count, the defender trick count. @rtype: tuple """ declarerCount, defenderCount = 0, 0 for i in range(len(self.winners)): trick = self.getTrick(i) winner = self.whoPlayed(self.winningCard(trick)) if winner in (self.declarer, self.dummy): declarerCount += 1 else: # Trick won by defenders. defenderCount += 1 return declarerCount, defenderCount def playCard(self, card, player=None, hand=[]): """Plays card to current trick. Card validity should be checked with isValidPlay() beforehand. @param card: the Card object to be played from player's hand. @param player: the player of card, or None. @param hand: the hand of player, or []. """ assert isinstance(card, Card) player = player or self.whoseTurn() hand = hand or [card] # Skip hand check. valid = self.isValidPlay(card, player, hand) assert valid if valid: # In case assert is disabled. self.played[player].append(card) # If trick is complete, determine winner. trick = self.getCurrentTrick() leader, cards = trick if len(cards) == 4: winner = self.whoPlayed(self.winningCard(trick)) self.winners.append(winner) def isValidPlay(self, card, player=None, hand=[]): """Card is playable if and only if: - Play session is not complete. - Direction is on turn to play. - Card exists in hand. - Card has not been previously played. In addition, if the current trick has an established lead, then card must follow lead suit OR hand must be void in lead suit. Specification of player and hand are required for verification. """ assert isinstance(card, Card) if self.isComplete(): return False elif hand and card not in hand: return False # Playing a card not in hand. elif player and player != self.whoseTurn(): return False # Playing out of turn. elif self.whoPlayed(card): return False # Card played previously. leader, cards = self.getCurrentTrick() # 0 if start of playing, 4 if complete trick. if len(cards) in (0, 4): return True # Card will be first in next trick. else: # Current trick has an established lead: check for revoke. leadcard = cards[leader] # Cards in hand that match suit of leadcard. followers = [c for c in hand if c.suit == leadcard.suit and not self.whoPlayed(c)] # Hand void in lead suit or card follows lead suit. return len(followers) == 0 or card in followers def whoPlayed(self, card): """Returns the player who played the specified card. @param card: a Card. @return: the player who played card. """ assert isinstance(card, Card) for player, cards in self.played.items(): if card in cards: return player return False def whoseTurn(self): """If playing is not complete, returns the player who is next to play. @return: the player next to play. """ if not self.isComplete(): trick = self.getCurrentTrick() leader, cards = trick if len(cards) == 4: # If trick is complete, trick winner's turn. return self.whoPlayed(self.winningCard(trick)) else: # Otherwise, turn is next (clockwise) player in trick. return Direction[(leader.index + len(cards)) % 4] return False def winningCard(self, trick): """Determine which card wins the specified trick: - In a trump contract, the highest ranked trump card wins. - Otherwise, the highest ranked card of the lead suit wins. @param: a complete (leader, cards) trick tuple. @return: the Card object which wins the trick. """ leader, cards = trick if len(cards) == 4: # Trick is complete. if self.trumps: # Suit contract. trumpcards = [c for c in cards.values() if c.suit==self.trumps] if len(trumpcards) > 0: return max(trumpcards) # Highest ranked trump. # No Trump contract, or no trump cards played. followers = [c for c in cards.values() if c.suit==cards[leader].suit] return max(followers) # Highest ranked card in lead suit. return False pybridge-0.3.0/pybridge/bridge/call.py0000644000175000017500000000565010637217660017451 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from twisted.spread import pb from symbols import Level, Strain class Call(pb.Copyable, pb.RemoteCopy): """Abstract class, inherited by Bid, Pass, Double and Redouble.""" def __hash__(self): return hash(self.__class__.__name__) def __repr__(self): return "%s()" % self.__class__.__name__ class Bid(Call): """A Bid represents a statement of a level and a strain. @param level: the level of the bid. @type level: L{Level} @param strain: the strain (denomination) of the bid. @type strain: L{Strain} """ level = property(lambda self: self.__level) strain = property(lambda self: self.__strain) def __init__(self, level, strain): if level not in Level: raise TypeError, "Expected Level, got %s" % type(level) if strain not in Strain: raise TypeError, "Expected Strain, got %s" % type(strain) self.__level = level self.__strain = strain def __cmp__(self, other): if not issubclass(other.__class__, Call): raise TypeError, "Expected Call, got %s" % type(other) if isinstance(other, Bid): # Compare two bids. selfIndex = self.level.index*len(Strain) + self.strain.index otherIndex = other.level.index*len(Strain) + other.strain.index return cmp(selfIndex, otherIndex) else: # Comparing non-bid calls returns true. return 1 def __hash__(self): return hash((self.level, self.strain)) def __repr__(self): return "Bid(%s, %s)" % (self.level, self.strain) def getStateToCopy(self): return self.level, self.strain def setCopyableState(self, state): self.__level, self.__strain = state pb.setUnjellyableForClass(Bid, Bid) class Pass(Call): """A Pass represents an abstention from the bidding.""" pb.setUnjellyableForClass(Pass, Pass) class Double(Call): """A Double over an opponent's current bid.""" pb.setUnjellyableForClass(Double, Double) class Redouble(Call): """A Redouble over an opponent's double of partnership's current bid.""" pb.setUnjellyableForClass(Redouble, Redouble) pybridge-0.3.0/pybridge/bridge/scoring.py0000644000175000017500000001240610571605200020163 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from symbols import Strain # There are undoubtedly many minor variations of the score values. # In the future, score values may be stored in separate XML format files. def scoreDuplicate(result): """Scoring algorithm for duplicate bridge. This code includes the scoring values from: http://www.ebu.co.uk/lawsandethics/the_laws/chapter8.asp """ score = 0 isDoubled = result['contract']['doubleBy'] isRedoubled = result['contract']['redoubleBy'] isVulnerable = result['vulnerable'] contractLevel = result['contract']['bid'].level.index + 1 tricksMade = result['tricksMade'] tricksRequired = result['contract']['bid'].level.index + 7 trumpSuit = result['contract']['bid'].strain if tricksMade >= tricksRequired: # Contract fulfilled. # Calculate scores for tricks bid and made. if trumpSuit in (Strain.Club, Strain.Diamond): # Clubs and Diamonds score 20 for each odd trick. score += contractLevel * 20 else: # Hearts, Spades and NT score 30 for each odd trick. score += contractLevel * 30 if trumpSuit is Strain.NoTrump: score += 10 # For NT, add a 10 point bonus. # Calculate scores for doubles. if isDoubled: score *= 2 # Multiply score by 2 for each isDoubled odd trick. elif isRedoubled: score *= 4 # Multiply score by 4 for each isRedoubled odd trick. # Calculate premium scores. if score >= 100: if isVulnerable: score += 500 # Game, vulnerable. else: # Game, not vulnerable. score += 300 # Game, not vulnerable. if tricksRequired == 13: if isVulnerable: score += 1500 # Grand slam, vulnerable. else: score += 1000 # Grand slam, not vulnerable. elif tricksRequired == 12: if isVulnerable: score += 750 # Small slam, vulnerable. else: score += 500 # Small slam, not vulnerable. else: score += 50 # Any part score. # Calculate "for the insult" bonuses. if isDoubled: score += 50 elif isRedoubled: score += 100 # Calculate scores for overtricks. overTricks = tricksMade - tricksRequired if isDoubled: if isVulnerable: # Score 200 for each doubled and vulnerable overtrick. score += overTricks * 200 else: # Score 100 for each doubled and not vulnerable overtrick. score += overTricks * 100 elif isRedoubled: if isVulnerable: # Score 400 for each redoubled and vulnerable overtrick. score += overTricks * 400 else: score += overTricks * 200 else: if trumpSuit in (Strain.Club, Strain.Diamond): # Clubs and Diamonds score 20 for each undoubled overtrick. score += overTricks * 20 else: # Hearts, Spades and NT score 30 for each undoubled overtrick. score += overTricks * 30 else: # Contract not fulfilled. underTricks = tricksRequired - tricksMade if isDoubled: if isVulnerable: # Score 200 for the first doubled and vulnerable undertrick. # Score 300 for all other undertricks. score -= 200 + (underTricks - 1) * 300 else: # Score 100 for the first doubled and non-vulnerable undertrick. # Score 200 for all other undertricks. # Score 100 extra for third and greater undertricks. score -= 100 + (underTricks - 1) * 200 if underTricks > 3: score -= (underTricks - 3) * 100 elif isRedoubled: if isVulnerable: score -= 400 + (underTricks - 1) * 600 else: score -= 200 + (underTricks - 1) * 400 if underTricks > 3: score -= (underTricks - 3) * 200 else: if isVulnerable: score -= 100 + (underTricks - 1) * 100 else: score -= 50 + (underTricks - 1) * 50 return score def scoreRubber(result): pass # TODO: implement. pybridge-0.3.0/pybridge/bridge/card.py0000644000175000017500000000464410610463033017435 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from twisted.spread import pb from symbols import Rank, Suit class Card(object, pb.Copyable, pb.RemoteCopy): """A card has a rank and a suit. @param rank: the rank of the card. @type rank: L{Rank} @param suit: the suit of the card. @type suit: L{Suit} """ rank = property(lambda self: self.__rank) suit = property(lambda self: self.__suit) def __init__(self, rank, suit): if rank not in Rank: raise TypeError, "Expected Rank, got %s" % type(rank) if suit not in Suit: raise TypeError, "Expected Suit, got %s" % type(suit) self.__rank = rank self.__suit = suit def __eq__(self, other): """Two cards are equivalent if their ranks and suits match.""" if isinstance(other, Card): return self.suit == other.suit and self.rank == other.rank return False def __cmp__(self, other): """Compare cards for hand sorting. Care must be taken when comparing cards of different suits. """ if not isinstance(other, Card): raise TypeError, "Expected Card, got %s" % type(other) selfIndex = self.suit.index*13 + self.rank.index otherIndex = other.suit.index*13 + other.rank.index return cmp(selfIndex, otherIndex) def __hash__(self): return hash((self.rank, self.suit)) def __repr__(self): return "Card(%s, %s)" % (self.rank, self.suit) def getStateToCopy(self): return self.rank, self.suit def setCopyableState(self, state): self.__rank, self.__suit = state pb.setUnjellyableForClass(Card, Card) pybridge-0.3.0/pybridge/bridge/pbn.py0000644000175000017500000002701510573113413017302 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ This module provides two-way conversion between BridgeGame objects and the PBN 2.0 (Portable Bridge Notation) format. The PBN 2.0 specification is available from http://www.tistis.nl/pbn/. """ import re import time from card import Card from deck import Deck from call import Bid, Pass, Double, Redouble from symbols import Player as Position, Level, Strain, Rank, Suit, Vulnerable class ParseError(Exception): """Raised when PBN parser encounters an unexpected input.""" def importPBN(pbn, complete=True): """Builds a BridgeGame object from a given PBN "import"-format string. @param pbn: a string containing PBN markup. @param complete: if True, expect the game described in pbn to be complete. Raises a ParseError if BridgeGame cannot be completed. @return: a BridgeGame object equivalent to the PBN string. """ from newnewgame import BridgeGame, GameError # Mappings from PBN symbols to their PyBridge type equivalents. BIDLEVEL = dict(zip('1234567', Level)) BIDSTRAIN = dict(zip('CDHS', Strain) + [('NT', Strain.NoTrump)]) CARDRANK = dict(zip('23456789TJQKA', Rank)) CARDSUIT = dict(zip('CDHS', Suit)) POSITION = dict(zip('NESW', Position)) VULN = {'All': Vulnerable.All, 'Both': Vulnerable.All, '-': Vulnerable.None, 'NS': Vulnerable.NorthSouth, 'EW': Vulnerable.EastWest, 'None': Vulnerable.None, 'Love': Vulnerable.None} game = BridgeGame() players = dict([(pos, game.addPlayer(pos)) for pos in Position]) tags, sections, notes = parsePBN(pbn) board = {} # Load values of non-essential tags. board['event'] = tags.get('Event') board['site'] = tags.get('Site') if tags.get('Date'): # Convert to time tuple. board['date'] = time.strptime(tags['Date'], "%Y.%m.%d") board['boardnum'] = int(tags.get('Board', 0)) board['players'] = dict([(p, tags.get(p.key)) for p in Position]) # Load values of essential tags. for tag in ('Dealer', 'Vulnerable', 'Deal', 'Auction', 'Play'): if tag not in tags: raise ParseError, "Required tag '%s' not found" % tag try: board['dealer'] = POSITION[tags['Dealer']] board['vulnerable'] = VULN[tags['Vulnerable']] # Reconstruct deal. board['deal'] = {} first, hands = tags['Deal'].split(":") firstindex = POSITION[first.strip()].index order = Position[firstindex:] + Position[:firstindex] for player, hand in zip(order, hands.strip().split()): board['deal'][player] = [] for suit, suitcards in zip(reversed(Suit), hand.split('.')): for rank in suitcards: card = Card(CARDRANK[rank], suit) board['deal'][player].append(card) # Validate deal. deck = Deck() if not deck.isValidDeal(board['deal']): raise ParseError, "Deal does not validate" game.start(board) # Initialise game with board. # Process Auction section: build bidding. # TODO: PBN does not need to provide an auction. pos = POSITION[tags['Auction']] for item in sections['Auction'].split(): if item.startswith('='): # A note identifier. continue elif item.startswith('-'): # Skip position. pos = Position[(pos.index + 1) % len(Position)] # Next player. continue # Extract call from item. if item[0] in BIDLEVEL and item[1:] in BIDSTRAIN: call = Bid(BIDLEVEL[item[0]], BIDSTRAIN[item[1:]]) elif item == 'Pass': call = Pass() elif item == 'X': call = Double() elif item == 'XX': call = Redouble() else: raise ParseError, "Unrecognised item '%s' in Auction" % item try: # Make call. players[pos].makeCall(call) pos = Position[(pos.index + 1) % len(Position)] # Next player. except GameError, err: raise ParseError, "Invalid call %s in Auction: %s" % (call, err) # Process Play section: build play. first = POSITION[tags['Play']] for line in sections['Play'].splitlines(): leader, cards = game.getTurn(), {} # Trick. # Extract cards from line. for item in line.split(): print item if item.startswith('='): # A note identifier. continue if item[0] in CARDSUIT and item[1] in CARDRANK: card = Card(CARDRANK[item[1]], CARDSUIT[item[0]]) cards[Position[(first.index + len(cards)) % len(Position)]] = card else: raise ParseError, "Unrecognised item '%s' in Play" % item try: # Play cards in trick. for pos in Position[leader.index:] + Position[:leader.index]: players[pos].playCard(card) except GameError, err: raise ParseError, "Invalid card %s in Play: %s" % (card, err) except KeyError, key: raise ParseError, "Invalid value %s for attribute" % key def exportPBN(game): """Builds a PBN "export"-format string from a given BridgeGame object. @param game: a BridgeGame object. @return: a PBN string equivalent to the BridgeGame object. """ # Mappings from PyBridge symbol types to their PBN equivalents. RANKS = dict(zip(Rank, "23456789TJQKA")) SUITS = dict(zip(Suit, "CDHS")) POSITIONS = dict(zip(Player, "NESW")) ''' def importPBN(self, pbn): """Builds a BridgeGame object from a given PBN "import format" string. @param pbn: a string containing PBN markup. @return: a BridgeGame object equivalent to the PBN string. """ # This lambda reverses the mapping between keys and values of dict d. # It assumes there are no duplicate values in d. invert = lambda d: dict([(v, k) for k, v in d.iteritems()]) tagValues, sectionData = self.parse(pbn) # Get dealer. dealer = invert(self.SEATS)[tagValues['Dealer']] # Get deal. deal = {} # Determine first hand in Deal string. first = invert(self.SEATS)[tagValues['Deal'][0]] seatorder = Seat[first.index:] + Seat[:first.index] # Split deal into hands, into suits, into cards. handstrings = tagValues['Deal'][2:].split(' ') for seat, handstring in zip(seatorder, handstrings): deal[seat] = [] for suit, rankstring in zip(self.SUITORDER, handstring.split('.')): for rankchar in rankstring: card = Card(invert(self.RANKS)[rankchar], suit) deal[seat].append(card) # Get vulnerability. for vulnTuple, vulnTexts in self.VULNERABLE.items(): # Since PBN allows variations on the Vulnerable tag. if tagValues['Vulnerable'] in vulnTexts: vulnNS, vulnEW = vulnTuple break scoring = scoreDuplicate # For now, just use duplicate scoring method. game = Game(dealer, deal, scoring, vulnNS, vulnEW) # # TODO - determine the calls made, load them in with game.makeCall # - determine the cards played, load them in with game.playCard # # Finally, return the BridgeGame object. return game def exportPBN(self, game): """Builds a PBN "export format" string from a given BridgeGame object. @param game: a BridgeGame object. @return: a PBN string equivalent to the BridgeGame object. """ def makeTag(key, value): """A convenience function to generate a PBN-style tag.""" return '[%s \"%s\"]\n' % (key, value) pbn = '' # # TODO: fill out all 15 fields, with respect to PBN spec. # # (1) Event (the name of the tournament or match) pbn += makeTag('Event', 'Unknown') # (2) Site (the location of the event) pbn += makeTag('Site', 'Unknown') # (3) Date (the starting date of the game) # TODO: use the localtime() method of the time module pbn += makeTag('Date', '%s.%s.%s' % (1,2,3)) # (4) Board (the board number) pbn += makeTag('Board', '1') # (5) West (the west player) # (6) North (the north player) # (7) East (the east player) # (8) South (the south player) # TODO: use 'Unknown' tags # (9) Dealer (the dealer) pbn += makeTag('Dealer', self.SEATS[game.bidding.dealer]) # (10) Vulnerable (the situation of vulnerability) # TODO: put game.vulnNS and game.vulnEW into a tuple, use tuple as index on self.VULNERABLE, # get first value (the [0]) from list. # pbn += makeTag('Vulnerable', self.VULNERABLE[( , )][0] # (11) Deal (the dealt cards) # (12) Scoring (the scoring method) # (13) Declarer (the declarer of the contract) # (14) Contract (the contract) # (15) Result (the result of the game) return pbn ''' def parsePBN(pbn): """Parses the given PBN string and extracts: * for each PBN tag, a dict of associated key/value pairs. * for each data section, a dict of key/data pairs. This method does not interpret the PBN string itself. @param pbn: a string containing PBN markup. @return: a tuple (tag values, section data, notes). """ tagValues, sectionData, notes = {}, {}, {} for line in pbn.splitlines(): line.strip() # Remove whitespace. if line.startswith('%'): # A comment. pass # Skip over comments. elif line.startswith('['): # A tag. line = line.strip('[]') # Remove leading [ and trailing ]. # The key is the first word, the value is everything after. tag, value = line.split(' ', 1) tag = tag.capitalize() value = value.strip('\'\"') if tag == 'Note': notes.setdefault(tag, []) notes[tag].append(value) else: tagValues[tag] = value else: # Line follows tag, add line to data buffer for section. sectionData.setdefault(tag, '') sectionData[tag] += line + '\n' return tagValues, sectionData, notes def testImport(): pbnstr = file("a-pbn-file.pbn").read() # Set path as appropriate. pbn = PBNHandler() # Create our handler. g = pbn.importPBN(pbnstr) # Do it! return g pybridge-0.3.0/pybridge/bridge/bidding.py0000644000175000017500000001721710610463033020124 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from pybridge.network.error import GameError from call import Bid, Pass, Double, Redouble from symbols import Direction, Level, Strain class Bidding(object): """This class models the bidding (auction) phase of a game of bridge. A bidding session is a list of Call objects and the dealer. """ def __init__(self, dealer): if dealer not in Direction: raise TypeError, "Expected Direction, got %s" % type(dealer) self.calls = [] self.dealer = dealer def isComplete(self): """Bidding is complete if 4 or more calls have been made, and the last 3 calls are Pass calls. @return: True if bidding is complete, False if not. @rtype: bool """ passes = len([c for c in self.calls[-3:] if isinstance(c, Pass)]) return len(self.calls) >= 4 and passes == 3 def isPassedOut(self): """Bidding is passed out if each player has passed on their first turn. In this case, the bidding is complete, but no contract is established. @return: True if bidding is passed out, False if not. @rtype: bool """ passes = len([call for call in self.calls if isinstance(call, Pass)]) return len(self.calls) == 4 and passes == 4 def getContract(self): """When the bidding is complete, the contract is the last and highest bid, which may be doubled or redoubled. Hence, the contract represents the "final state" of the bidding. @return: a dict containing the keywords: @keyword bid: the last and highest bid. @keyword declarer: the partner who first bid the contract strain. @keyword doubleBy: the opponent who doubled the contract, or None. @keyword redoubleBy: the partner who redoubled an opponent's double on the contract, or None. """ if self.isComplete() and not self.isPassedOut(): bid = self.getCurrentCall(Bid) double = self.getCurrentCall(Double) redouble = self.getCurrentCall(Redouble) # Determine partnership. caller = self.whoCalled(bid) partnership = (caller, Direction[(caller.index + 2) % 4]) # Determine declarer. for call in self.calls: if isinstance(call, Bid) and call.strain == bid.strain \ and self.whoCalled(call) in partnership: declarerBid = call break return {'bid' : bid, 'declarer' : self.whoCalled(declarerBid), 'doubleBy' : double and self.whoCalled(double), 'redoubleBy' : redouble and self.whoCalled(redouble) } return None # Bidding passed out or not complete, no contract. def getCurrentCall(self, calltype): """Returns most recent current call of specified type, or None. @param calltype: call type, in (Bid, Pass, Double, Redouble). @return: most recent call matching type, or None. """ if calltype not in (Bid, Pass, Double, Redouble): raise GameError, "Expected call type, got %s" % type(calltype) for call in self.calls[::-1]: if isinstance(call, calltype): return call elif isinstance(call, Bid): break return None def makeCall(self, call, player=None): """Appends call from player to the calls list. @param call: the Call object representing player's call. @param player: the player making call, or None. """ if not isinstance(call, (Bid, Pass, Double, Redouble)): raise GameError, "Expected call type, got %s" % type(call) if not self.isValidCall(call, player): raise GameError, "Invalid call" self.calls.append(call) def isValidCall(self, call, player=None): """Check that specified call is available to player, with respect to current state of bidding. If specified, player's turn will be checked. @param call: the Call object to be tested for validity. @param player: the player attempting to call, or None. @return: True if call is available, False if not. """ if not isinstance(call, (Bid, Pass, Double, Redouble)): raise GameError, "Expected call type, got %s" % type(call) assert player in Direction or player is None # The bidding must not be complete. if self.isComplete(): return False # It must be player's turn to call. if player and player != self.whoseTurn(): return False # Bidding is not complete; a pass is always available. elif isinstance(call, Pass): return True currentBid = self.getCurrentCall(Bid) # A bid must be greater than the current bid. if isinstance(call, Bid): return not currentBid or call > currentBid # Doubles and redoubles only when a bid has been made. if currentBid: bidder = self.whoCalled(currentBid) # A double must be made on the current bid from opponents, # with has not been already doubled by partnership. if isinstance(call, Double): opposition = (Direction[(self.whoseTurn().index + 1) % 4], Direction[(self.whoseTurn().index + 3) % 4]) return bidder in opposition and not self.getCurrentCall(Double) # A redouble must be made on the current bid from partnership, # which has been doubled by an opponent. elif isinstance(call, Redouble): partnership = (self.whoseTurn(), Direction[(self.whoseTurn().index + 2) % 4]) return bidder in partnership and self.getCurrentCall(Double) \ and not self.getCurrentCall(Redouble) return False # Otherwise unavailable. def whoCalled(self, call): """Returns the player who made the specified call. @param call: a Call. @return: the player who made call, or False. """ if not isinstance(call, (Bid, Pass, Double, Redouble)): raise GameError, "Expected call type, got %s" % type(call) if call in self.calls: return Direction[(self.calls.index(call) + self.dealer.index) % 4] return False # Call not made by any player. def whoseTurn(self): """Returns position of player who is next to make a call. @return: the current turn. @rtype: Direction """ if self.isComplete(): raise GameError, "Bidding complete" return Direction[(len(self.calls) + self.dealer.index) % 4] pybridge-0.3.0/pybridge/server/0000755000175000017500000000000010637763637016242 5ustar michaelmichaelpybridge-0.3.0/pybridge/server/user.py0000644000175000017500000001033010610463033017541 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import re #from twisted.internet import defer #from twisted.python import failure from twisted.spread import pb from pybridge.network.error import DeniedRequest, IllegalRequest class User(pb.Avatar): info = property(lambda self: {}) def __init__(self, name): self.name = name # User name. self.server = None # Set by Realm. self.tables = {} # For each joined table name, its instance. def attached(self, mind): """Called when connection to client is established.""" self.remote = mind self.server.userConnects(self) def detached(self, mind): """Called when connection to client is lost.""" self.remote = None self.server.userDisconnects(self) # Inform server. def callEvent(self, eventName, **kwargs): """Calls remote event listener with arguments.""" if self.remote: self.remote.callRemote(eventName, **kwargs) # Perspective methods, accessible by client. def perspective_getServerInfo(self): """Provides a dict of information about the server.""" info = {} info['supported'] = self.server.supported info['version'] = self.server.version return info def perspective_getRoster(self, name): """Provides roster requested by client.""" if name == 'tables': return self.server.tables elif name == 'users': return self.server.users else: raise DeniedRequest, "Unknown roster name \'%s\'" % name def perspective_hostTable(self, tableid, tabletype): """Creates a new table.""" if not isinstance(tableid, str): raise IllegalRequest, "Invalid parameter for table identifier" elif not(0 < len(tableid) < 21) or re.search("[^A-Za-z0-9_ ]", tableid): raise IllegalRequest, "Invalid table identifier format" elif tableid in self.server.tables: raise DeniedRequest, "Table name exists" elif tabletype not in self.server.supported: raise DeniedRequest, "Table type not suppported by this server" self.server.createTable(tableid, tabletype) return self.perspective_joinTable(tableid) # Force join to table. def perspective_joinTable(self, tableid): """Joins an existing table.""" if not isinstance(tableid, str): raise IllegalRequest, "Invalid parameter for table name" elif tableid not in self.server.tables: raise DeniedRequest, "No such table" elif tableid in self.tables: raise DeniedRequest, "Already joined table" table = self.server.tables[tableid] self.tables[tableid] = table # Returning table reference creates a RemoteTable object on client. return table, table.view def perspective_leaveTable(self, tableid): """Leaves a table.""" if not isinstance(tableid, str): raise IllegalRequest, "Invalid parameter for table name" elif tableid not in self.tables: raise DeniedRequest, "Not joined to table" del self.tables[tableid] # Implicitly removes user from table. class AnonymousUser(pb.Avatar): def perspective_register(self, username, password): """Create a user account with specified username and password.""" # TODO: consider defer.succeed, defer.fail, failure.Failure self.server.registerUser(username, password) pybridge-0.3.0/pybridge/server/config.py0000644000175000017500000000340010636756311020044 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ Manages PyBridge server configuration file. """ from StringIO import StringIO from configobj import ConfigObj from validate import Validator import pybridge.environment as env # Config spec spec = StringIO("""# PyBridge server configuration file [Database] Engine = option('sqlite', 'sapdb', 'postgresql', 'firebird', 'maxdb', 'sybase', 'interbase', 'psycopg', 'mysql', 'mssql', 'postgres', default='sqlite') DatabaseName = string # Or path to database file if using sqlite. User = string # Not used with sqlite. Password = string # Not used with sqlite. Host = string # Leave empty for localhost. Port = integer # Leave empty for default. """) config = None val = Validator() def load(): global config filename = env.find_config_server('server.cfg') config = ConfigObj(filename, create_empty=True, configspec=spec) config.validate(val, copy=True) def save(): global config config.validate(val, copy=True) config.write() pybridge-0.3.0/pybridge/server/server.py0000644000175000017500000000560710610463033020104 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from datetime import datetime from twisted.python import log import database as db from pybridge import __version__ from pybridge.network.error import DeniedRequest, IllegalRequest from pybridge.network.localtable import LocalTable from pybridge.network.tablemanager import LocalTableManager from pybridge.network.usermanager import LocalUserManager from pybridge.bridge.game import BridgeGame class Server(object): def __init__(self): # Set up rosters. self.tables = LocalTableManager() self.users = LocalUserManager() self.version = __version__ self.supported = ['bridge'] def userConnects(self, user): """""" log.msg("User %s connected" % user.name) self.users.userLogin(user) db.UserAccount.byUsername(user.name).set(lastLogin=datetime.now()) def userDisconnects(self, user): """""" log.msg("User %s disconnected" % user.name) self.users.userLogout(user) # Methods invoked by user perspectives. def registerUser(self, username, password): """Registers a new user account in the database. @param username: the unique username requested by user. @param password: the password to be associated with the account. """ # Check that username has not already been registered. if db.UserAccount.selectBy(username=username).count() > 0: raise DeniedRequest, "Username already registered" try: # Create user account. db.UserAccount(username=username, password=password, allowLogin=True) log.msg("New user %s registered" % username) except ValueError, err: raise IllegalRequest, err def userChangePassword(self, user, password): """""" pass def createTable(self, tableid, tabletype): # Ignore specified tabletype, for now. if tableid not in self.tables: table = LocalTable(tableid, BridgeGame) table.id = tableid table.server = self self.tables.openTable(table) #self.tables[tableid] = table pybridge-0.3.0/pybridge/server/__init__.py0000644000175000017500000000232310636754677020356 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2006 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from twisted.cred import checkers, credentials, portal from twisted.spread import pb import config config.load() from pybridge.server.checker import Checker from pybridge.server.realm import Realm from pybridge.server.server import Server server = Server() realm = Realm() checker = Checker() realm.server = server checker.users = server.users p = portal.Portal(realm) p.registerChecker(checker) factory = pb.PBServerFactory(p) pybridge-0.3.0/pybridge/server/database.py0000644000175000017500000001616610637724325020361 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import os, re from datetime import datetime from sqlobject import * from sqlobject.inheritance import InheritableSQLObject from twisted.python import log from config import config from pybridge import environment as env # Initiate connection to the appropriate database backend. # See http://sqlobject.org/SQLObject.html#declaring-the-class # This code has been tested with the SQLite database backend. If you experience # problems with databases supported by SQLObject, please file a bug report. engine = config['Database'].get('Engine', 'sqlite') # Default to SQLite. if engine == 'sqlite': dbpath = config['Database'].get('DatabaseName', env.find_config_server('pybridge-server.db')) # SQLObject uses a special syntax to specify path on Windows systems. # This code block is from http://simpleweb.essienitaessien.com/example if(dbpath[1] == ':'): s = re.sub('\\\\', '/', dbpath) # Change '\' to '/' s = re.sub(':', '|', s, 1) # Special for sqlite dbpath = '/' + s connection_string = "sqlite://" + dbpath else: username = config['Database'].get('Username', '') password = config['Database'].get('Password', '') host = config['Database'].get('Host', 'localhost') port = config['Database'].get('Port', '') dbname = config['Database'].get('DatabaseName', 'pybridge') # Standard URI syntax (from http://sqlobject.org/SQLObject.html): # scheme://[user[:password]@]host[:port]/database[?parameters] connection_string = engine + '://' if username: connection_string += username if password: connection_string += ':' + password connection_string += '@' connection_string += host if port: connection_string += ':' + str(port) connection_string += '/' + dbname try: connection = connectionForURI(connection_string) log.msg("Connection to %s database succeeded" % engine) except Exception, e: log.err(e) log.msg("Could not connect to %s database with URI: %s" % (engine, connection_string)) log.msg("Please check configuration file.") raise SystemExit # Database connection is required for server operation. sqlhub.processConnection = connection # Set all SQLObjects to use connection. class UserAccount(SQLObject): """A store of user information. A user account is created when a user is registered. """ username = StringCol(length=20, notNone=True, unique=True, alternateID=True) password = StringCol(length=40, notNone=True) # Store SHA-1 hex hashes. allowLogin = BoolCol(default=True) # If False, account login is disabled. email = StringCol(default=None, length=320) # See RFC 2821 section 4.5.3.1. realname = UnicodeCol(default=None, length=40) profile = UnicodeCol(default=None) created = DateTimeCol(default=datetime.now) lastLogin = DateTimeCol(default=None) # friends = MultipleJoin('UserFriend', joinColumn='from_user') def _set_username(self, value): if not isinstance(value, str) or not(1 <= len(value) <= 20): raise ValueError, "Invalid specification of username" if re.search("[^A-z0-9_]", value): raise ValueError, "Username may only be alphanumeric characters" self._SO_set_username(value) def _set_password(self, value): if not isinstance(value, str) or not(1 <= len(value) <= 40): raise ValueError, "Invalid specification of password" self._SO_set_password(value) def _set_email(self, value): # This regexp matches virtually all well-formatted email addresses. if value and not re.match("^[A-z0-9_.+-]+@([A-z0-9-]+\.)+[A-z]{2,6}$", value): raise ValueError, "Invalid or ill-formatted email address" self._SO_set_email(value) for table in [UserAccount]: table.createTable(ifNotExists=True) # The following tables are not used by PyBridge 0.3. # They will be enhanced and used in future releases. ''' class UserFriend(SQLObject): """Models the social interconnections that exist between users. Client software may use this information to provide visual clues to users that members of their "social circle" are online. Users may specify the nature of their relationships: this takes inspiration from the XFN (XHTML Friend Network) model: see http://gmpg.org/xfn/. The symmetry arising from some types of relationship is eschewed for simplicity. This relation is irreflexive: no user can form a friendship with themselves! """ fromUser = ForeignKey('UserAccount') # The creator of the relationship. toUser = ForeignKey('UserAccount') # The subject of the relationship. fromToIndex = DatabaseIndex('fromUser', 'toUser', unique=True) # XFN attributes. friendship = EnumCol(default=None, enumValues=['friend', 'acquaintance', 'contact']) physical = BoolCol(default=False) # Having met in person. professional = EnumCol(default=None, enumValues=['co-worker', 'colleague']) geographical = EnumCol(default=None, enumValues=['co-resident', 'neighbour']) family = EnumCol(default=None, enumValues=['child', 'parent', 'sibling', 'spouse', 'kin']) romantic = EnumCol(default=None, enumValues=['muse', 'crush', 'date', 'sweetheart']) class Game(InheritableSQLObject): """Captures game attributes common to all games. Implementations of specific games should inherit from this class. """ start = DateTimeCol() complete = DateTimeCol() class BridgeGame(Game): """Captures game attributes specific to bridge games. """ board = ForeignKey('BridgeBoard') declarer = EnumCol(enumValues=list(Player)) # contract = trickCount = IntCol() # Number of tricks won by score = IntCol() # Although key attributes of games are stored in fields (for searching), # the complete game is represented in PBN format. pbn = StringCol() # Players: no player may occupy more than one position. north = ForeignKey('UserAccount') east = ForeignKey('UserAccount') south = ForeignKey('UserAccount') west = ForeignKey('UserAccount') class BridgeBoard(SQLObject): """Encapsulates the attributes which may be common to multiple bridge games. Separating board attributes from . """ deal = IntCol() dealer = EnumCol(enumValues=list(Direction)) vuln = EnumCol(enumValues=list(Vulnerable)) ''' pybridge-0.3.0/pybridge/server/checker.py0000644000175000017500000000461510575514125020211 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from twisted.cred import checkers, credentials, error from twisted.internet import defer from twisted.python import failure, log from zope.interface import implements import database as db class Checker: """A database-driven implementation of ICredentialsChecker.""" implements(checkers.ICredentialsChecker) credentialInterfaces = (credentials.IUsernamePassword, credentials.IUsernameHashedPassword) def __init__(self): self.users = {} # Users online, from Server object. def requestAvatarId(self, credentials): def unauthorized(reason): log.msg("Login failed for %s: %s" % (credentials.username, reason)) return failure.Failure(error.UnauthorizedLogin(reason)) def passwordMatch(matched): if matched: return credentials.username else: return unauthorized("Incorrect password for user") if credentials.username == '': return checkers.ANONYMOUS # TODO: if allowAnonymousRegistration. users = db.UserAccount.selectBy(username=credentials.username) if users.count() is 0: return unauthorized("User not known on server") elif users[0].allowLogin is False: return unauthorized("User account is disabled") elif credentials.username in self.users: # TODO: delete old session and use this one instead? return unauthorized("User is already logged in") d = defer.maybeDeferred(credentials.checkPassword, users[0].password) d.addCallback(passwordMatch) return d pybridge-0.3.0/pybridge/server/realm.py0000644000175000017500000000302410467611131017671 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2006 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from twisted.cred import checkers, portal from twisted.spread import pb from user import User, AnonymousUser class Realm: __implements__ = portal.IRealm def requestAvatar(self, avatarId, mind, *interfaces): if pb.IPerspective not in interfaces: raise NotImplementedError if avatarId == checkers.ANONYMOUS: avatar = AnonymousUser() avatar.server = self.server # Provide reference to server. return pb.IPerspective, avatar, lambda:None else: avatar = User(avatarId) avatar.server = self.server # Provide reference to server. avatar.attached(mind) return pb.IPerspective, avatar, lambda a=avatar:a.detached(mind) pybridge-0.3.0/pybridge/interfaces/0000755000175000017500000000000010637763637017057 5ustar michaelmichaelpybridge-0.3.0/pybridge/interfaces/observer.py0000644000175000017500000000350010610463033021230 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ Provides interfaces for the Observer Pattern. For more information, see Gamma et al., "Design Patterns: Elements of Reusable Object-Oriented Software", ISBN 0-201-63361-2. """ from zope.interface import Interface class ISubject(Interface): """ISubject defines methods required for observation of an object.""" def attach(self, observer): """Add observer to list of observers. @param observer: object implementing IListener. """ def detach(self, observer): """Remove observer from list of observers. @param observer: object implementing IListener. """ def notify(self, event, *args, **kwargs): """Inform all observers that state has been changed by event. @param event: the name of the event. @type event: str """ class IListener(Interface): """IListener defines methods required by observers of an ISubject.""" def update(self, event, *args, **kwargs): """Called by ISubject being observed.""" pybridge-0.3.0/pybridge/interfaces/game.py0000644000175000017500000000632210634000206020312 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from zope.interface import Interface class IGame(Interface): """IGame defines methods common to all games. This interface makes no assumptions about the game to be played, besides that it has players. """ def start(self, initial): """Called to initialise game state. This resets any previous state. @param initial: the initial state of the game. """ def getState(self): """Returns an object representing the current state of the game. This may be used to export a game to be saved or transmitted. @return: a state object, consumable by setState(). """ def setState(self, state): """Overwrites the current game state with the specified state. This may be used to import a saved or transmitted game. @param state: a state object, as generated by getState(). """ def updateState(self, event, *args, **kwargs): """Updates game state in response to event. @param event: the name of the event. """ def addPlayer(self, position): """Provide caller with a Player object bound to position. The specified position must be vacant. @param position: position to add player. @return: a Player object. """ def removePlayer(self, position): """Removes player from specified position. @param position: position from which to remove player. """ # Methods to query game state. def inProgress(self): """Indicates whether the game is currently being played or has finished. @return: True if game is running, False otherwise. """ def isNextGameReady(self): """Indicates whether the next game is ready to start. @return: True if next game is ready to start, False otherwise. """ class ICardGame(IGame): """ICardGame defines methods specific to card games. ICardGame inherits all methods in IGame. """ def getHand(self, position): """Returns a list of the known cards in hand. For each unknown card, None is used as its placeholder. @player: a player identifier. @return: the hand of the player. """ def getTurn(self): """If game is in progress, returns the player who is next to play. @return: a player identifier, or None. """ pybridge-0.3.0/pybridge/interfaces/__init__.py0000644000175000017500000000000010461151455021136 0ustar michaelmichaelpybridge-0.3.0/pybridge/interfaces/table.py0000644000175000017500000000466610610463033020506 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from zope.interface import Interface class ITable(Interface): """ITable defines methods which are common to all table implementations, which are expected to provide the following services: - Synchronisation of game state between the server and connected clients. This should be transparent to game code. - Functionality shared by all games, such as: - Game management: players joining and leaving the game, closure of table when all observers have left. - Communication between users. A table is the abstraction of "the place where a game is played". """ def joinGame(self, user, position): """Registers a user as an active player, provided that the specified position is vacant. The interface supports a single user playing at multiple positions. Implementations may disable this feature. @param user: user identifier. @param position: position which player takes. """ def leaveGame(self, user, position): """Removes player from their position. Specification of position is required, if the user is playing at multiple positions. @param user: user identifier. @param position: position which player takes. """ def sendMessage(self, message, sender, recipients): """Issues message from sender to all named recipients, or to all observers. @param message: message text string. @param sender: user identifier of sender. @param recipients: user identifiers of recipient observers. """ pybridge-0.3.0/pybridge/ui/0000755000175000017500000000000010637763637015351 5ustar michaelmichaelpybridge-0.3.0/pybridge/ui/dialog_preferences.py0000644000175000017500000001162010637737044021534 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import gtk import os from wrapper import GladeWrapper import pybridge.environment as env from config import config from manager import wm from vocabulary import * from pybridge.bridge.symbols import Suit SUIT_LABEL_TEMPLATE = "%s" class DialogPreferences(GladeWrapper): glade_name = 'dialog_preferences' def setUp(self): # Allow user to select only PNG images for background. filter_pixbufs = gtk.FileFilter() #filter_pixbufs.add_pixbuf_formats() filter_pixbufs.add_pattern('*.png') filter_pixbufs.set_name(_('PNG images')) self.background.add_filter(filter_pixbufs) # Build a list of card decks from which the user may choose. # (The user is prevented from selecting an arbitary image.) activedeck = config['Appearance'].get('CardStyle', 'bonded.png') model = gtk.ListStore(str) self.cardstyle.set_model(model) cell = gtk.CellRendererText() self.cardstyle.pack_start(cell, True) self.cardstyle.add_attribute(cell, 'text', 0) # Populate list of card decks. path = env.find_pixmap('') for filename in os.listdir(path): if filename.endswith('.png'): iter = model.append((filename,)) if filename == activedeck: self.cardstyle.set_active_iter(iter) # Retrieve selected background. background_file = config['Appearance'].get('Background') if background_file is None or not os.path.exists(background_file): background_file = env.find_pixmap('baize.png') self.background.set_filename(background_file) # Retrieve suit colours. self.suit_colours = {} for suit in Suit: rgb = config['Appearance']['Colours'].get(suit.key, (0, 0, 0)) colour = gtk.gdk.Color(*rgb) self.suit_colours[suit] = colour # Set button label colour from self.suit_colours. hexrep = gtk.color_selection_palette_to_string([colour]) label = getattr(self, 'label_%scolour' % suit.key.lower()) label.set_markup(SUIT_LABEL_TEMPLATE % (hexrep, SUIT_SYMBOLS[suit])) use_suitsymbols = config['Appearance'].get('SuitSymbols') self.check_suitsymbols.set_active(use_suitsymbols) # Signal handlers. def on_cardstyle_changed(self, widget, *args): pass def on_background_changed(self, widget, *args): pass def on_suitcolour_clicked(self, widget, *args): # Get symbol in Suit corresponding to button clicked. suit = [s for s in Suit if s.key.lower() in widget.get_name()][0] title = _("Select colour for %s symbol" % SUIT_NAMES[suit]) dialog = gtk.ColorSelectionDialog(title) dialog.colorsel.set_current_color(self.suit_colours[suit]) def dialog_response_cb(dialog, response_id): if response_id == gtk.RESPONSE_OK: colour = dialog.colorsel.get_current_color() self.suit_colours[suit] = colour # Set button label to colour selected by user. hexrep = gtk.color_selection_palette_to_string([colour]) label = getattr(self, 'label_%scolour' % suit.key.lower()) label.set_markup(SUIT_LABEL_TEMPLATE % (hexrep, SUIT_SYMBOLS[suit])) dialog.destroy() dialog.connect('response', dialog_response_cb) dialog.run() # show() def on_cancelbutton_clicked(self, widget, *args): wm.close(self) def on_okbutton_clicked(self, widget, *args): # Save preferences to config file. for suit, colour in self.suit_colours.items(): rgb = (colour.red, colour.green, colour.blue) config['Appearance']['Colours'][suit.key] = rgb config['Appearance']['Background'] = self.background.get_filename() model = self.cardstyle.get_model() iter = self.cardstyle.get_active_iter() config['Appearance']['CardStyle'] = model.get_value(iter, 0) config['Appearance']['SuitSymbols'] = self.check_suitsymbols.get_active() wm.close(self) pybridge-0.3.0/pybridge/ui/wrapper.py0000644000175000017500000000510210610463034017354 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import gettext import gtk import gtk.glade import sys import pybridge.environment as env GLADE_PATH = env.find_glade("pybridge.glade") if sys.platform == 'win32': # Win32 should use the ICO icon. ICON_PATH = env.find_pixmap("pybridge.ico") else: # All other platforms should use the PNG icon. ICON_PATH = env.find_pixmap("pybridge.png") class GladeWrapper(object): """A superclass for Glade-based application windows. Modified from: http://www.pixelbeat.org/libs/libglade.py """ def __init__(self, parent=None): """Initialise window from Glade definition. @param parent: pointer to parent gtk.Window, or None. """ self.widgets = gtk.glade.XML(GLADE_PATH, self.glade_name, gettext.textdomain()) self.window = self.widgets.get_widget(self.glade_name) instance_attributes = {} for attribute in dir(self.__class__): instance_attributes[attribute] = getattr(self, attribute) self.widgets.signal_autoconnect(instance_attributes) self.window.set_icon_from_file(ICON_PATH) if parent is not None: self.window.set_transient_for(parent.window) self.setUp() def __getattr__(self, attribute): """Allows referencing of Glade widgets as window attributes.""" widget = self.widgets.get_widget(attribute) if widget is None: raise AttributeError("No widget named %s" % attribute) self.__dict__[attribute] = widget # Cache reference for later. return widget def setUp(self): """Override this method to run code when this window is created.""" pass def tearDown(self): """Override this method to run code when this window is destroyed.""" pass pybridge-0.3.0/pybridge/ui/vocabulary.py0000644000175000017500000001262710637736613020074 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ A repository for translatable symbols and names. """ import gtk from pybridge.bridge.symbols import * import pybridge.bridge.call as Call from config import config CALLTYPE_NAMES = { Call.Pass: _('Pass'), Call.Double: _('Double'), Call.Redouble: _('Redouble'), } CALLTYPE_SYMBOLS = { Call.Pass: '-', Call.Double: 'X', Call.Redouble: 'XX', } DIRECTION_NAMES = { Direction.North: _('North'), Direction.East: _('East'), Direction.South: _('South'), Direction.West: _('West'), } DIRECTION_SYMBOLS = { Direction.North: _('N'), Direction.East: _('E'), Direction.South: _('S'), Direction.West: _('W'), } LEVEL_SYMBOLS = { Level.One: '1', Level.Two: '2', Level.Three: '3', Level.Four: '4', Level.Five: '5', Level.Six: '6', Level.Seven: '7', } RANK_NAMES = { Rank.Two: _('Two'), Rank.Three: _('Three'), Rank.Four: _('Four'), Rank.Five: _('Five'), Rank.Six: _('Six'), Rank.Seven: _('Seven'), Rank.Eight: _('Eight'), Rank.Nine: _('Nine'), Rank.Ten: _('Ten'), Rank.Jack: _('Jack'), Rank.Queen: _('Queen'), Rank.King: _('King'), Rank.Ace: _('Ace'), } RANK_SYMBOLS = { Rank.Two: '2', Rank.Three: '3', Rank.Four: '4', Rank.Five: '5', Rank.Six: '6', Rank.Seven: '7', Rank.Eight: '8', Rank.Nine: '9', Rank.Ten: '10', Rank.Jack: 'J', Rank.Queen: 'Q', Rank.King: 'K', Rank.Ace: 'A', } SUIT_NAMES = { Suit.Club: _('Clubs'), Suit.Diamond: _('Diamonds'), Suit.Heart: _('Hearts'), Suit.Spade: _('Spades'), } if config['Appearance'].get('SuitSymbols'): SUIT_SYMBOLS = { Suit.Club: u'\N{BLACK CLUB SUIT}', Suit.Diamond: u'\N{BLACK DIAMOND SUIT}', Suit.Heart: u'\N{BLACK HEART SUIT}', Suit.Spade: u'\N{BLACK SPADE SUIT}', } else: SUIT_SYMBOLS = { Suit.Club: 'C', Suit.Diamond: 'D', Suit.Heart: 'H', Suit.Spade: 'S', } STRAIN_NAMES = { Strain.Club: _('Club'), Strain.Diamond: _('Diamond'), Strain.Heart: _('Heart'), Strain.Spade: _('Spade'), Strain.NoTrump: _('No Trump'), } if config['Appearance'].get('SuitSymbols'): STRAIN_SYMBOLS = { Strain.Club: u'\N{BLACK CLUB SUIT}', Strain.Diamond: u'\N{BLACK DIAMOND SUIT}', Strain.Heart: u'\N{BLACK HEART SUIT}', Strain.Spade: u'\N{BLACK SPADE SUIT}', Strain.NoTrump: 'NT', } else: STRAIN_SYMBOLS = { Strain.Club: 'C', Strain.Diamond: 'D', Strain.Heart: 'H', Strain.Spade: 'S', Strain.NoTrump: 'NT', } VULN_SYMBOLS = { Vulnerable.All: _('All'), Vulnerable.NorthSouth: _('N/S'), Vulnerable.EastWest: _('E/W'), Vulnerable.None: _('None'), } def render_call(call): if isinstance(call, Call.Bid): if call.strain == Strain.NoTrump: # No associated colour. return LEVEL_SYMBOLS[call.level] + STRAIN_SYMBOLS[Strain.NoTrump] else: rgb = config['Appearance']['Colours'].get(call.strain.key, (0, 0, 0)) hexrep = gtk.color_selection_palette_to_string([gtk.gdk.Color(*rgb)]) return "%s%s" % \ (LEVEL_SYMBOLS[call.level], hexrep, STRAIN_SYMBOLS[call.strain]) else: return CALLTYPE_SYMBOLS[call.__class__] def render_call_name(call): if isinstance(call, Call.Bid): return _('%(level)s %(strain)s') % {'level': LEVEL_NAMES[call.level], 'strain': STRAIN_NAMES[call.strain]} else: return CALLTYPE_NAMES[call.__class__] def render_card(card): rgb = config['Appearance']['Colours'].get(card.suit.key, (0, 0, 0)) hexrep = gtk.color_selection_palette_to_string([gtk.gdk.Color(*rgb)]) return "%s%s" % \ (RANK_SYMBOLS[card.rank], hexrep, SUIT_SYMBOLS[card.suit]) def render_card_name(card): return _('%(rank)s of %(suit)s') % {'rank': card.rank, 'suit': card.suit} def render_contract(contract): """Produce a format string representing the contract. @param contract: a contract object. @type contract: dict @return: a format string representing the contract. @rtype: str """ doubled = contract['redoubleBy'] and CALLTYPE_SYMBOLS[Call.Redouble] \ or contract['doubleBy'] and CALLTYPE_SYMBOLS[Call.Double] or '' fields = {'bid': render_call(contract['bid']), 'doubled': doubled, 'declarer': DIRECTION_NAMES[contract['declarer']]} if doubled: return _('%(bid)s %(doubled)s by %(declarer)s') % fields else: return _('%(bid)s by %(declarer)s') % fields pybridge-0.3.0/pybridge/ui/config.py0000644000175000017500000000327410637736537017175 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ Manages PyBridge client configuration file. """ from StringIO import StringIO from configobj import ConfigObj from validate import Validator import pybridge.environment as env # Config spec spec = StringIO("""# PyBridge configuration file [Connection] HostAddress = string PortNumber = integer(0, 65535) Username = string Password = string [Appearance] CardStyle = string BackgroundImage = string SuitSymbols = boolean(default=True) [[Colours]] Club = int_list(3, 3) Diamond = int_list(3, 3) Heart = int_list(3, 3) Spade = int_list(3, 3) """) config = None val = Validator() def load(): global config filename = env.find_config_client('config') config = ConfigObj(filename, create_empty=True, configspec=spec) config.validate(val, copy=True) def save(): global config config.validate(val, copy=True) config.write() pybridge-0.3.0/pybridge/ui/cardarea.py0000644000175000017500000003333110637740456017462 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import gtk import cairo import pango import pangocairo import pybridge.environment as env from canvas import CairoCanvas from config import config from vocabulary import * from pybridge.bridge.card import Card from pybridge.bridge.symbols import Direction, Rank, Suit # The order in which card graphics are expected in card mask. CARD_MASK_RANKS = [Rank.Ace, Rank.Two, Rank.Three, Rank.Four, Rank.Five, Rank.Six, Rank.Seven, Rank.Eight, Rank.Nine, Rank.Ten, Rank.Jack, Rank.Queen, Rank.King] CARD_MASK_SUITS = [Suit.Club, Suit.Diamond, Suit.Heart, Suit.Spade] # The red-black-red-black ordering convention. RED_BLACK = [Suit.Diamond, Suit.Club, Suit.Heart, Suit.Spade] class CardArea(CairoCanvas): """This widget is a graphical display of tricks and hands of cards. Requirements: Cairo (>=1.0), PyGTK (>= 2.8). """ # Load card mask. card_mask_file = config['Appearance'].get('CardStyle', 'bonded.png') card_mask_path = env.find_pixmap(card_mask_file) card_mask = cairo.ImageSurface.create_from_png(card_mask_path) font_description = pango.FontDescription('Sans Bold 10') border_x = border_y = 10 card_width = card_mask.get_width() / 13 card_height = card_mask.get_height() / 5 spacing_x = int(card_width * 0.4) spacing_y = int(card_height * 0.2) def __init__(self): super(CardArea, self).__init__() # Initialise parent. # To receive these events, override with external method. self.on_card_clicked = lambda card, position: True self.on_hand_clicked = lambda position: True self.focus = Direction.South self.hands = {} self.trick = None self.players = {} self.set_player_mapping(Direction.South, redraw=False) self.connect('button_press_event', self.button_press) self.add_events(gtk.gdk.BUTTON_PRESS_MASK) def draw_card(self, context, pos_x, pos_y, card): """Draws graphic of specified card to context at (pos_x, pos_y). @param context: a cairo.Context @param pos_x: @param pos_y: @param card: the Card to draw. """ if isinstance(card, Card): # Determine coordinates of card graphic. src_x = CARD_MASK_RANKS.index(card.rank) * self.card_width src_y = CARD_MASK_SUITS.index(card.suit) * self.card_height else: # Draw a face-down card. src_x, src_y = self.card_width*2, self.card_height*4 context.rectangle(pos_x, pos_y, self.card_width, self.card_height) context.clip() context.set_source_surface(self.card_mask, pos_x-src_x, pos_y-src_y) context.paint() context.reset_clip() def set_hand(self, hand, position, facedown=False, visible=[]): """Sets the hand of player at position. Draws cards in hand to context. The hand is buffered into an ImageSurface, since hands change infrequently and multiple calls to draw_card() are expensive. @param hand: a list of Card objects. @param position: a member of Direction. @param facedown: if True, cards are drawn face-down. @param visible: a list of elements of hand to draw. """ def get_coords_for_hand(): coords = {} if position in (self.TOP, self.BOTTOM): pos_y = 0 if facedown is True: # Draw cards in one continuous row. for index, card in enumerate(hand): pos_x = index * self.spacing_x coords[card] = (pos_x, pos_y) else: # Insert a space between each suit. spaces = len([1 for suitcards in suits.values() if len(suitcards) > 0]) - 1 for index, card in enumerate(hand): # Insert a space for each suit in hand which appears before this card's suit. insert = len([1 for suit, suitcards in suits.items() if len(suitcards) > 0 and RED_BLACK.index(card.suit) > RED_BLACK.index(suit)]) pos_x = (index + insert) * self.spacing_x coords[card] = (pos_x, pos_y) else: # LEFT or RIGHT. if facedown is True: # Wrap cards to a 4x4 grid. for index, card in enumerate(hand): adjust = position == self.RIGHT and index == 12 and 3 pos_x = ((index % 4) + adjust) * self.spacing_x pos_y = (index / 4) * self.spacing_y coords[card] = (pos_x, pos_y) else: longest = max([len(cards) for cards in suits.values()]) for index, card in enumerate(hand): adjust = position == self.RIGHT and longest - len(suits[card.suit]) pos_x = (suits[card.suit].index(card) + adjust) * self.spacing_x pos_y = RED_BLACK.index(card.suit) * self.spacing_y coords[card] = (pos_x, pos_y) return coords if facedown is False: # Split hand into suits. suits = dict([(suit, []) for suit in Suit]) for card in hand: suits[card.suit].append(card) # Sort suits. for suit in suits: suits[suit].sort(reverse=True) # High to low. # Reorder hand by sorted suits. hand = [] for suit in RED_BLACK: hand.extend(suits[suit]) saved = self.hands.get(position) if saved and saved['hand'] == hand: # If hand has been set previously, do not recalculate coords. coords = saved['coords'] else: coords = get_coords_for_hand() # Determine dimensions of hand. width = max([x for x, y in coords.values()]) + self.card_width height = max([y for x, y in coords.values()]) + self.card_height surface, context = self.new_surface(width, height) # Draw cards to surface. for i, card in enumerate(hand): if card in visible: pos_x, pos_y = coords[card] self.draw_card(context, pos_x, pos_y, card) # Save. self.hands[position] = {'hand' : hand, 'visible' : visible, 'surface' : surface, 'coords' : coords, 'facedown' : facedown} id = 'hand-%s' % position # Identifier for this item. if id in self.items: self.update_item(id, source=surface) else: xy = {self.TOP : (0.5, 0.15), self.BOTTOM : (0.5, 0.85), self.LEFT : (0.15, 0.5), self.RIGHT : (0.85, 0.5), } opacity = (self.players.get(position) is None) and 0.5 or 1 self.add_item(id, surface, xy[position], 0, opacity=opacity) def set_player_name(self, position, name=None): """ @param position: the position of the player. @param name: the name of the player, or None. """ self.players[position] = name # If no name specified, show hand at position as translucent. if ('hand-%s' % position) in self.items: opacity = (name is None) and 0.5 or 1 self.update_item('hand-%s' % position, opacity=opacity) id = 'player-%s' % position layout = pango.Layout(self.create_pango_context()) layout.set_font_description(self.font_description) if name is None: layout.set_text('%s' % DIRECTION_NAMES[position]) else: layout.set_text('%s: %s' % (DIRECTION_NAMES[position], name)) # Create an ImageSurface respective to dimensions of text. width, height = layout.get_pixel_size() width += 8; height += 4 surface, context = self.new_surface(width, height) context = pangocairo.CairoContext(context) # Draw background box, text to ImageSurface. context.set_line_width(4) context.rectangle(0, 0, width, height) context.set_source_rgb(0, 0.5, 0) context.fill_preserve() context.set_source_rgb(0, 0.25, 0) context.stroke() context.move_to(4, 2) context.set_source_rgb(1, 1, 1) context.show_layout(layout) if id in self.items: self.update_item(id, source=surface) else: xy = {self.TOP : (0.5, 0.2), self.BOTTOM : (0.5, 0.9), self.LEFT : (0.125, 0.625), self.RIGHT : (0.875, 0.625), } self.add_item(id, surface, xy[position], 2) def set_player_mapping(self, focus=Direction.South, redraw=True): """Sets the mapping between players at table and positions of hands. @param focus: the position to be drawn "closest" to the observer. @param redraw: if True, redraw the card area display immediately. """ # Assumes Direction elements are ordered clockwise from North. order = Direction[focus.index:] + Direction[:focus.index] for player, attr in zip(order, ('BOTTOM', 'LEFT', 'TOP', 'RIGHT')): setattr(self, attr, player) # Only redraw if focus has changed. if redraw and focus != self.focus: self.focus = focus self.clear() # Wipe all saved ImageSurface objects - not subtle! # Use a copy of self.hands, since it will be changed by set_hand(). hands = self.hands.copy() self.hands.clear() for position in Direction: self.set_player_name(position, self.players.get(position)) self.set_hand(hands[position]['hand'], position, facedown=hands[position]['facedown'], visible=hands[position]['visible']) trick = self.trick self.trick = None self.set_trick(trick) def set_trick(self, trick): """Sets the current trick. Draws representation of current trick to context. @param trick: a (leader, cards_played) pair, or None. """ xy = {self.TOP : (0.5, 0.425), self.BOTTOM : (0.5, 0.575), self.LEFT : (0.425, 0.5), self.RIGHT : (0.575, 0.5), } if trick: leader, cards = trick # The order of play is the leader, then clockwise around Direction. order = Direction[leader.index:] + Direction[:leader.index] for i, position in enumerate(order): id = 'trick-%s' % position old_card = self.trick and self.trick[1].get(position) or None new_card = cards.get(position) or None # If old card matches new card, take no action. if old_card is None and new_card is not None: surface, context = self.new_surface(self.card_width, self.card_height) self.draw_card(context, 0, 0, new_card) self.add_item(id, surface, xy[position], z_index=i+1) elif new_card is None and old_card is not None: self.remove_item(id) elif old_card != new_card: surface, context = self.new_surface(self.card_width, self.card_height) self.draw_card(context, 0, 0, new_card) self.update_item(id, surface, z_index=i+1) elif self.trick: # Remove all cards from previous trick. for player in self.trick[1]: self.remove_item('trick-%s' % player) self.trick = trick # Save trick. def button_press(self, widget, event): """Determines if a card was clicked: if so, calls on_card_selected.""" if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS: found_hand = False # Determine the hand which was clicked. for position in self.hands: card_coords = self.hands[position]['coords'] surface = self.hands[position]['surface'] hand_x, hand_y = self.items['hand-%s' % position]['area'][0:2] if (hand_x <= event.x <= hand_x + surface.get_width()) and \ (hand_y <= event.y <= hand_y + surface.get_height()): found_hand = True break if found_hand: self.on_hand_clicked(position) # Determine the card in hand which was clicked. pos_x, pos_y = event.x - hand_x, event.y - hand_y # Iterate through visible cards backwards. for card in reversed(self.hands[position]['hand']): if card in self.hands[position]['visible']: x, y = card_coords[card] if (x <= pos_x <= x + self.card_width) and \ (y <= pos_y <= y + self.card_height): self.on_card_clicked(card, position) break return True # Expected to return True. pybridge-0.3.0/pybridge/ui/dialog_connection.py0000644000175000017500000001111310636731170021360 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import gtk from wrapper import GladeWrapper from pybridge.network.client import client from config import config from manager import wm TCP_PORT = 5040 class DialogConnection(GladeWrapper): glade_name = 'dialog_connection' def setUp(self): # Read connection parameters from client settings. connection = config['Connection'] if connection: self.entry_hostname.set_text(connection.get('HostAddress', 'localhost')) self.entry_portnum.set_text(str(connection.get('PortNumber', TCP_PORT))) self.entry_username.set_text(connection.get('Username', '')) password = connection.get('Password', '').decode('rot13') self.entry_password.set_text(password) self.check_savepassword.set_active(password != '') else: self.entry_portnum.set_text(str(TCP_PORT)) def connectSuccess(self, avatar): """Actions to perform when connecting succeeds.""" # Save connection information. connection = config['Connection'] connection['HostAddress'] = self.entry_hostname.get_text() connection['PortNumber'] = int(self.entry_portnum.get_text()) connection['Username'] = self.entry_username.get_text() if self.check_savepassword.get_active(): # Encode password, to confuse password sniffer software. # ROT13 encoding does *not* provide security! password = self.entry_password.get_text().encode('rot13') else: password = '' # Flush password. connection['Password'] = password wm.close(self) def connectFailure(self, failure): """Actions to perform when connecting fails.""" client.disconnect() dialog = gtk.MessageDialog(parent=self.window, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK) dialog.set_title(_('Connection failed')) dialog.set_markup(_('Could not connect to server.')) dialog.format_secondary_text(_('Reason: %s') % failure.getErrorMessage()) def dialog_response_cb(dialog, response_id): dialog.destroy() self.button_connect.set_property('sensitive', True) dialog.connect('response', dialog_response_cb) dialog.show() # Signal handlers. def on_dialog_connection_delete_event(self, widget, *args): wm.close(self) def on_field_changed(self, widget, *args): """Validates entry fields, disables Connect button if invalid.""" # Host name, user name must not be blank. valid = self.entry_hostname.get_text() \ and self.entry_username.get_text() # Port number must be an integer. if valid: try: port = int(self.entry_portnum.get_text()) except ValueError: valid = False self.button_connect.set_property('sensitive', valid) def on_connect_clicked(self, widget, *args): # Prevent repeat clicks. self.button_connect.set_property('sensitive', False) hostname = self.entry_hostname.get_text() port = int(self.entry_portnum.get_text()) client.connect(hostname, port) username = self.entry_username.get_text() password = self.entry_password.get_text() register = self.check_registeruser.get_active() == True if register: # Attempt login only after registration. # TODO: can defer.waitForDeferred() be used here? d = client.register(username, password) d.addCallback(lambda _: client.login(username, password)) else: d = client.login(username, password) d.addCallbacks(self.connectSuccess, self.connectFailure) def on_cancel_clicked(self, widget, *args): wm.close(self) pybridge-0.3.0/pybridge/ui/__init__.py0000644000175000017500000000260110636755207017452 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from twisted.internet import gtk2reactor gtk2reactor.install() import gtk from twisted.internet import reactor import locale import gettext locale.setlocale(locale.LC_ALL, '') import pybridge.environment as env gettext.bindtextdomain('pybridge', env.get_localedir()) gettext.textdomain('pybridge') gettext.install('pybridge') import config config.load() def run(): """Starts the PyBridge client UI.""" from manager import wm from window_main import WindowMain wm.open(WindowMain) # Start the event loop. reactor.run() gtk.main() config.save() # Save config at exit. pybridge-0.3.0/pybridge/ui/window_bridgetable.py0000644000175000017500000005773710637705466021575 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import gtk from wrapper import GladeWrapper from pybridge.network.client import client from pybridge.network.error import GameError from cardarea import CardArea from config import config from eventhandler import SimpleEventHandler from manager import WindowManager from vocabulary import * from window_bidbox import WindowBidbox class WindowBridgetable(GladeWrapper): glade_name = 'window_bridgetable' def setUp(self): self.children = WindowManager() self.eventHandler = SimpleEventHandler(self) self.table = None # Table currently displayed in window. self.player, self.position = None, None # Set up "Take Seat" menu. self.takeseat_items = {} menu = gtk.Menu() for position in Direction: item = gtk.MenuItem(DIRECTION_NAMES[position], True) item.connect('activate', self.on_seat_activated, position) item.show() menu.append(item) self.takeseat_items[position] = item self.takeseat.set_menu(menu) # Set up CardArea widget. self.cardarea = CardArea() self.cardarea.on_card_clicked = self.on_card_clicked self.cardarea.on_hand_clicked = self.on_hand_clicked self.cardarea.set_size_request(640, 480) self.scrolled_cardarea.add_with_viewport(self.cardarea) self.cardarea.show() renderer = gtk.CellRendererText() renderer.set_property('size-points', 12) renderer.set_property('xalign', 0.5) # Set up bidding history and column display. self.call_store = gtk.ListStore(str, str, str, str) self.biddingview.set_model(self.call_store) for index, position in enumerate(Direction): title = DIRECTION_NAMES[position] column = gtk.TreeViewColumn(title, renderer, markup=index) column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) column.set_fixed_width(50) self.biddingview.append_column(column) # Set up trick history and column display. self.trick_store = gtk.ListStore(str, str, str, str) self.trickview.set_model(self.trick_store) for index, position in enumerate(Direction): title = DIRECTION_NAMES[position] column = gtk.TreeViewColumn(str(title), renderer, markup=index) column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) column.set_fixed_width(50) self.trickview.append_column(column) renderer = gtk.CellRendererText() # Set up score sheet and column display. self.score_store = gtk.ListStore(str, str, str, str) self.scoresheet.set_model(self.score_store) for index, title in enumerate([_('Contract'), _('Made'), _('N/S'), _('E/W')]): column = gtk.TreeViewColumn(title, renderer, markup=index) self.scoresheet.append_column(column) # Set up observer listing. self.observer_store = gtk.ListStore(str) self.treeview_observers.set_model(self.observer_store) column = gtk.TreeViewColumn(None, renderer, text=0) self.observer_store.set_sort_column_id(0, gtk.SORT_ASCENDING) self.treeview_observers.append_column(column) def tearDown(self): # Close all child windows. for window in self.children.values(): self.children.close(window) self.table = None # Dereference table. def errback(self, failure): print "Error: %s" % failure.getErrorMessage() #print failure.getBriefTraceback() def setTable(self, table): """Changes display to match the table specified. @param table: the (now) focal table. """ self.table = table self.table.attach(self.eventHandler) self.table.game.attach(self.eventHandler) self.player, self.position = None, None self.window.set_title(_('Table %s (Bridge)') % self.table.id) self.resetGame() for position in Direction: self.redrawHand(position) # Redraw all hands. if self.table.game.inProgress(): # If trick play in progress, redraw trick. if self.table.game.play: self.redrawTrick() self.setTurnIndicator() for call in self.table.game.bidding.calls: position = self.table.game.bidding.whoCalled(call) self.addCall(call, position) self.setDealer() self.setVulnerability() # If contract, set contract. if self.table.game.bidding.isComplete(): self.setContract() # If playing, set trick counts. if self.table.game.play: for position, cards in self.table.game.play.played.items(): for card in cards: self.addCard(card, position) self.setTrickCount() # If user is a player and bidding in progress, open bidding box. if self.player and not self.table.game.bidding.isComplete(): bidbox = self.children.open(WindowBidbox, parent=self) bidbox.setCallSelectHandler(self.on_call_selected) bidbox.setTable(self.table, self.position) # Initialise seat menu and player labels. for position in Direction: player = self.table.players.get(position) # Player name or None. available = player is None or position == self.position self.takeseat_items[position].set_property('sensitive', available) if player: self.event_joinGame(player, position) else: # Position vacant. self.event_leaveGame(None, position) # Initialise observer listing. self.observer_store.clear() for observer in self.table.observers: self.event_addObserver(observer) def resetGame(self): """Clears bidding history, contract, trick counts.""" # self.cardarea.clear() self.call_store.clear() # Reset bidding history. self.trick_store.clear() # Reset trick history. self.setContract() # Reset contract. self.setTrickCount() # Reset trick counts. def addCall(self, call, position): """Adds call from specified player, to bidding tab.""" column = position.index if column == 0 or self.call_store.get_iter_first() == None: iter = self.call_store.append() else: # Get bottom row. There must be a better way than this... iter = self.call_store.get_iter_first() while self.call_store.iter_next(iter) != None: iter = self.call_store.iter_next(iter) format = render_call(call) self.call_store.set(iter, column, format) def addCard(self, card, position): """""" position = self.table.game.play.whoPlayed(card) column = position.index row = self.table.game.play.played[position].index(card) if self.trick_store.get_iter_first() == None: self.trick_store.append() iter = self.trick_store.get_iter_first() for i in range(row): iter = self.trick_store.iter_next(iter) if iter is None: iter = self.trick_store.append() format = render_card(card) self.trick_store.set(iter, column, format) def addScore(self, contract, made, score): textContract = render_contract(contract) textMade = '%s' % made if contract['declarer'] in (Direction.North, Direction.South) and score > 0 \ or contract['declarer'] in (Direction.East, Direction.West) and score < 0: textNS, textEW = '%s' % abs(score), '' else: textNS, textEW = '', '%s' % abs(score) self.score_store.prepend([textContract, textMade, textNS, textEW]) def gameComplete(self): # Display all previously revealed hands - the server will reveal the others. for position in self.table.game.visibleHands: self.redrawHand(position, all=True) self.setTurnIndicator() dialog = gtk.MessageDialog(parent=self.window, type=gtk.MESSAGE_INFO) dialog.set_title(_('Game result')) # Determine and display score in dialog box. if self.table.game.contract: declarerWon, defenceWon = self.table.game.play.getTrickCount() required = self.table.game.contract['bid'].level.index + 7 offset = declarerWon - required score = self.table.game.getScore() self.addScore(self.table.game.contract, declarerWon, score) fields = {'contract': render_contract(self.table.game.contract), 'offset': abs(offset) } if offset > 0: if offset == 1: resultText = _('Contract %(contract)s made by 1 trick.') % fields else: resultText = _('Contract %(contract)s made by %(offset)s tricks.') % fields elif offset < 0: if offset == -1: resultText = _('Contract %(contract)s failed by 1 trick.') % fields else: resultText = _('Contract %(contract)s failed by %(offset)s tricks.') % fields else: resultText = _('Contract %(contract)s made exactly.') % fields pair = (score >= 0 and _('declarer')) or _('defence') scoreText = _('Score %(points)s points for %(pair)s.') % {'points': abs(score), 'pair': pair} dialog.set_markup(resultText + '\n' + scoreText) else: dialog.set_markup(_('Bidding passed out.')) dialog.format_secondary_text(_('No score.')) if self.player: dialog.add_button(_('Leave Seat'), gtk.RESPONSE_CANCEL) dialog.format_secondary_text(_('Click OK to start next game.')) dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) dialog.set_default_response(gtk.RESPONSE_OK) # If user leaves table (ie. closes window), close dialog as well. dialog.set_transient_for(self.window) dialog.set_destroy_with_parent(True) def dialog_response_cb(dialog, response_id): dialog.destroy() if self.player: if response_id == gtk.RESPONSE_OK and self.table.game.isNextGameReady(): d = self.player.callRemote('startNextGame') d.addErrback(self.errback) elif response_id == gtk.RESPONSE_CANCEL: self.on_leaveseat_clicked(dialog) dialog.connect('response', dialog_response_cb) dialog.show() def redrawHand(self, position, all=False): """Redraws cards making up the hand at position. Cards played are filtered out and omitted from display. Unknown cards are displayed face down. @param position: @param all: If True, do not filter out cards played. """ try: hand = self.table.game.getHand(position) facedown = False except GameError: # Unknown hand. hand = range(13) facedown = True if all is True or self.table.game.play is None: available = hand else: played = self.table.game.play.played[position] if facedown: # Draw cards face down for unknown hand. available = range(13 - len(played)) else: available = [card for card in hand if card not in played] self.cardarea.set_hand(hand, position, facedown, visible=available) def redrawTrick(self): """Redraws trick. @param table: @param trick: """ trick = None if self.table.game.play: trick = self.table.game.play.getCurrentTrick() self.cardarea.set_trick(trick) # Methods to set information displayed on side panel. def setContract(self): """Sets the contract label from contract.""" format = "%s" if self.table.game.contract: text = render_contract(self.table.game.contract) self.label_contract.set_markup(format % text) self.label_contract.set_property('sensitive', True) else: self.label_contract.set_markup(format % _('No contract')) self.label_contract.set_property('sensitive', False) def setDealer(self): format = "%s" dealer = '' if self.table.game.inProgress(): dealer = DIRECTION_NAMES[self.table.game.board['dealer']] self.label_dealer.set_markup(format % dealer) def setTrickCount(self): """Sets the trick counter labels for declarer and defence.""" format = "%s (%s)" if self.table.game.play: declarer, defence = self.table.game.play.getTrickCount() required = self.table.game.contract['bid'].level.index + 7 declarerNeeds = max(0, required - declarer) defenceNeeds = max(0, 13 + 1 - required - defence) self.label_declarer.set_markup(format % (declarer, declarerNeeds)) self.label_defence.set_markup(format % (defence, defenceNeeds)) self.frame_declarer.set_property('sensitive', True) self.frame_defence.set_property('sensitive', True) else: # Reset trick counters. self.label_declarer.set_markup(format % (0, 0)) self.label_defence.set_markup(format % (0, 0)) self.frame_declarer.set_property('sensitive', False) self.frame_defence.set_property('sensitive', False) def setTurnIndicator(self): """Sets the statusbar text to indicate which player is on turn.""" context = self.statusbar.get_context_id('turn') self.statusbar.pop(context) try: turn = self.table.game.getTurn() if self.table.game.play: declarer, dummy = self.table.game.play.declarer, self.table.game.play.dummy if self.position and self.position == turn != dummy: text = _("Play a card from your hand.") elif self.position and self.position == declarer and turn == dummy: text = _("Play a card from dummy's hand.") else: text = _("It is %s's turn to play a card.") % DIRECTION_NAMES[turn] else: # Bidding. if self.position and self.position == turn: text = _("Make a call from the bidding box.") else: text = _("It is %s's turn to make a call.") % DIRECTION_NAMES[turn] except GameError: # Game not in progress. text = _("Waiting for next game to start.") self.statusbar.push(context, text) def setVulnerability(self): """Sets the vulnerability indicators.""" format = "%s" vulnerable = '' if self.table.game.inProgress(): vulnerable = VULN_SYMBOLS[self.table.game.board['vuln']] self.label_vuln.set_markup(format % vulnerable) # Registered event handlers. def event_addObserver(self, observer): self.observer_store.append([observer]) def event_removeObserver(self, observer): def func(model, path, iter, user_data): if model.get_value(iter, 0) in user_data: model.remove(iter) return True self.observer_store.foreach(func, observer) def event_joinGame(self, player, position): self.cardarea.set_player_name(position, player) # Disable menu item corresponding to position. widget = self.takeseat_items[position] widget.set_property('sensitive', False) # If all positions occupied, disable Take Seat. if len(self.table.players.values()) == len(Direction): self.takeseat.set_property('sensitive', False) if self.player and self.table.game.isNextGameReady(): d = self.player.callRemote('startNextGame') d.addErrback(self.errback) def event_leaveGame(self, player, position): self.cardarea.set_player_name(position, None) # Enable menu item corresponding to position. widget = self.takeseat_items[position] widget.set_property('sensitive', True) # If we are not seated, ensure Take Seat is enabled. if self.position is None: self.takeseat.set_property('sensitive', True) def event_start(self, board): #self.children.close('dialog_gameresult') self.resetGame() self.redrawTrick() # Clear trick. for position in Direction: self.redrawHand(position) self.setTurnIndicator() self.setDealer() self.setVulnerability() if self.player: d = self.player.callRemote('getHand') # When player's hand is returned by server, reveal it to client-side Game. # TODO: is there a better way of synchronising hands? d.addCallbacks(self.table.game.revealHand, self.errback, callbackKeywords={'position' : self.position}) bidbox = self.children.open(WindowBidbox, parent=self) bidbox.setCallSelectHandler(self.on_call_selected) bidbox.setTable(self.table, self.position) def event_makeCall(self, call, position): self.addCall(call, position) self.setTurnIndicator() if self.table.game.bidding.isComplete(): self.setContract() if self.children.get(WindowBidbox): # If a player. self.children.close(self.children[WindowBidbox]) if not self.table.game.inProgress(): self.gameComplete() def event_playCard(self, card, position): # Determine the position of the hand from which card was played. playfrom = self.table.game.play.whoPlayed(card) self.addCard(card, playfrom) self.setTurnIndicator() self.setTrickCount() self.redrawTrick() self.redrawHand(playfrom) if not self.table.game.inProgress(): self.gameComplete() def event_revealHand(self, hand, position): all = not self.table.game.inProgress() # Show all cards if game has finished. self.redrawHand(position, all) def event_sendMessage(self, message, sender, recipients): buffer = self.chat_messagehistory.get_buffer() iter = buffer.get_end_iter() buffer.insert(iter, '\n' + _('%(sender)s: %(message)s' % {'sender': sender, 'message': message})) self.chat_messagehistory.scroll_to_iter(iter, 0) # Signal handlers. def on_call_selected(self, call): if self.player: d = self.player.callRemote('makeCall', call) d.addErrback(self.errback) def on_hand_clicked(self, position): if not self.player and not self.table.players.get(position): # Join game at position. self.on_seat_activated(self.cardarea, position) def on_card_clicked(self, card, position): if self.player: if self.table.game.inProgress() and self.table.game.play: d = self.player.callRemote('playCard', card) d.addErrback(self.errback) def on_seat_activated(self, widget, position): def success(player): self.player = player # RemoteReference to BridgePlayer object. self.position = position self.takeseat.set_property('sensitive', False) self.leaveseat.set_property('sensitive', True) self.cardarea.set_player_mapping(self.position) if self.table.game.inProgress(): d = self.player.callRemote('getHand') d.addCallbacks(self.table.game.revealHand, self.errback, callbackKeywords={'position' : self.position}) # If game is running and bidding is active, open bidding box. if not self.table.game.bidding.isComplete(): bidbox = self.children.open(WindowBidbox, parent=self) bidbox.setCallSelectHandler(self.on_call_selected) bidbox.setTable(self.table, self.position) d = self.table.joinGame(position) d.addCallbacks(success, self.errback) def on_takeseat_clicked(self, widget, *args): # TODO: match user up with preferred partner. for position in Direction: if position not in self.table.players: # Position is vacant. self.on_seat_activated(widget, position) # Take position. break def on_leaveseat_clicked(self, widget, *args): def success(r): self.player = None self.position = None self.takeseat.set_property('sensitive', True) self.leaveseat.set_property('sensitive', False) if self.children.get(WindowBidbox): self.children.close(self.children[WindowBidbox]) d = self.table.leaveGame(self.position) d.addCallbacks(success, self.errback) def on_toggle_gameinfo_clicked(self, widget, *args): visible = self.toggle_gameinfo.get_active() self.gameinfo.set_property('visible', visible) def on_toggle_chat_clicked(self, widget, *args): visible = self.toggle_chat.get_active() self.chatbox.set_property('visible', visible) def on_toggle_fullscreen_clicked(self, widget, *args): if self.toggle_fullscreen.get_active(): self.window.fullscreen() else: self.window.unfullscreen() def on_leavetable_clicked(self, widget, *args): # If user is currently playing a game, request confirmation. if self.player and self.table.game.inProgress(): dialog = gtk.MessageDialog(parent=self.window, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_QUESTION) dialog.set_title(_('Leave table?')) dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) dialog.add_button(_('Leave Table'), gtk.RESPONSE_OK) dialog.set_markup(_('Are you sure you wish to leave this table?')) dialog.format_secondary_text(_('You are currently playing a game. Leaving may forfeit the game, or incur penalties.')) def dialog_response_cb(dialog, response_id): dialog.destroy() if response_id == gtk.RESPONSE_OK: d = client.leaveTable(self.table.id) d.addErrback(self.errback) dialog.connect('response', dialog_response_cb) dialog.show() else: d = client.leaveTable(self.table.id) d.addErrback(self.errback) def on_chat_message_changed(self, widget, *args): sensitive = self.chat_message.get_text() != '' self.chat_send.set_property('sensitive', sensitive) def on_chat_send_clicked(self, widget, *args): message = self.chat_message.get_text() if message: # Don't send a null message. self.chat_send.set_property('sensitive', False) self.chat_message.set_text('') # Clear message. self.table.sendMessage(message) def on_window_delete_event(self, widget, *args): self.on_leavetable_clicked(widget, *args) return True # Stops window deletion taking place. pybridge-0.3.0/pybridge/ui/window_bidbox.py0000644000175000017500000001164210637437541020555 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import gtk from wrapper import GladeWrapper import pybridge.bridge.call as Call from config import config from eventhandler import SimpleEventHandler from vocabulary import * class WindowBidbox(object): """The bidding box is presented to a player, during bidding. Each call (bid, pass, double or redouble) is displayed as a button. When it is the player's turn to bid, a call is made by clicking the corresponding button. Unavailable calls are shown greyed-out. """ def __init__(self, parent=None): self.window = gtk.Window() if parent: self.window.set_transient_for(parent.window) self.window.set_title(_('Bidding Box')) self.window.connect('delete_event', self.on_delete_event) self.window.set_resizable(False) self.callButtons = {} self.callSelectHandler = None # A method to invoke when a call is clicked. self.eventHandler = SimpleEventHandler(self) self.table = None self.position = None def buildButtonFromCall(call, markup): button = gtk.Button() button.set_relief(gtk.RELIEF_NONE) button.connect('clicked', self.on_call_clicked, call) # A separate label is required for marked-up text. label = gtk.Label() label.set_markup(markup) label.set_use_markup(True) button.add(label) self.callButtons[call] = button return button vbox = gtk.VBox() bidtable = gtk.Table(rows=7, columns=5, homogeneous=True) vbox.pack_start(bidtable) # Build buttons for all bids. for y, level in enumerate(Call.Level): for x, strain in enumerate(Call.Strain): bid = Call.Bid(level, strain) markup = render_call(bid) xy = (x, x+1, y, y+1) bidtable.attach(buildButtonFromCall(bid, markup), *xy) vbox.pack_start(gtk.HSeparator()) otherbox = gtk.HBox() vbox.pack_start(otherbox) # Build buttons for other calls. othercalls = [(Call.Pass(), render_call_name, True), (Call.Double(), render_call, False), (Call.Redouble(), render_call, False)] for call, renderer, expand in othercalls: markup = renderer(call) otherbox.pack_start(buildButtonFromCall(call, markup), expand) self.window.add(vbox) self.window.show_all() def tearDown(self): if self.table: self.table.game.detach(self.eventHandler) self.table = None # Dereference table. def setCallSelectHandler(self, handler): """Provide a method to invoke when user selects a call. @param handler: a method accepting a call argument. @type handler: function """ self.callSelectHandler = handler def setTable(self, table, position): """Monitor the state of bidding in game at specified table. @param table: the BridgeGame for which to observe bidding session. @param: """ if self.table: self.table.game.detach(self.eventHandler) self.table = table self.table.game.attach(self.eventHandler) self.position = position self.enableCalls() # Event handlers. def event_makeCall(self, call, position): self.enableCalls() # Utility methods. def enableCalls(self): """Enables buttons representing the calls available to player.""" if self.position == self.table.game.getTurn(): self.window.set_property('sensitive', True) for call, button in self.callButtons.items(): isvalid = self.table.game.bidding.isValidCall(call) button.set_property('sensitive', isvalid) else: self.window.set_property('sensitive', False) # Signal handlers. def on_call_clicked(self, widget, call): if self.callSelectHandler: self.callSelectHandler(call) # Invoke external handler. def on_delete_event(self, widget, *args): return True # Stops window deletion taking place. pybridge-0.3.0/pybridge/ui/canvas.py0000644000175000017500000001776210636765310017200 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import gtk import cairo import pybridge.environment as env from config import config class CairoCanvas(gtk.DrawingArea): """Provides a simple canvas layer for the display of graphics.""" # TODO: enhance documentation. background_path = config['Appearance'].get('Background', env.find_pixmap('baize.png')) background = cairo.ImageSurface.create_from_png(background_path) pattern = cairo.SurfacePattern(background) pattern.set_extend(cairo.EXTEND_REPEAT) def __init__(self): super(CairoCanvas, self).__init__() # Initialise parent. self.items = {} # Set up gtk.Widget signals. self.connect('configure_event', self.configure) self.connect('expose_event', self.expose) def clear(self): """Clears all items from canvas.""" self.items = {} # Remove all item references. # Redraw background pattern on backing. width, height = self.window.get_size() context = cairo.Context(self.backing) context.rectangle(0, 0, width, height) context.set_source(self.pattern) context.paint() # Trigger a call to self.expose(). self.window.invalidate_rect((0, 0, width, height), False) def add_item(self, id, source, xy, z_index, opacity=1): """Places source item into items list. @param id: unique identifier for source. @param source: ImageSurface. @param xy: tuple providing (x, y) coords for source in backing. @param z_index: integer. @param opacity: integer in range 0 to 1. """ # Calculate and cache the on-screen area of the item. area = self.get_area(source, xy) self.items[id] = {'source': source, 'area': area, 'xy': xy, 'z-index': z_index, 'opacity' : opacity, } self.redraw(*area) def remove_item(self, id): """Removes source item with identifier from items list. @param id: unique identifier for source. """ if self.items.get(id): area = self.items[id]['area'] del self.items[id] self.redraw(*area) def update_item(self, id, source=None, xy=None, z_index=0, opacity=0): """ @param id: unique identifier for source. @param source: if specified, ImageSurface. @param xy: if specified, tuple providing (x, y) coords for source in backing. @param z_index: if specified, integer. @param opacity: if specified, integer in range 0 to 1. """ # If optional parameters are not specified, use stored values. z_index = z_index or self.items[id]['z-index'] opacity = opacity or self.items[id]['opacity'] if source or xy: # If source or xy coords changed, recalculate on-screen area. source = source or self.items[id]['source'] xy = xy or self.items[id]['xy'] area = self.get_area(source, xy) # If area of item has changed, clear item from previous area. oldarea = self.items[id]['area'] if area != oldarea: del self.items[id] self.redraw(*oldarea) else: source = self.items[id]['source'] xy = self.items[id]['xy'] area = self.items[id]['area'] self.items[id] = {'source': source, 'area': area, 'xy' : xy, 'z-index': z_index, 'opacity' : opacity, } self.redraw(*area) def redraw(self, x, y, width, height): """Redraws sources in area (x, y, width, height) to backing canvas. @param x: start x-coordinate of area to be redrawn. @param y: start y-coordinate of area to be redrawn. @param width: the width of area to be redrawn. @param height: the height of area to be redrawn. """ context = cairo.Context(self.backing) context.rectangle(x, y, width, height) context.clip() # Set clip region. # Redraw background pattern in area. context.set_source(self.pattern) context.paint() # Build list of sources to redraw in area, in order of z-index. # TODO: Find sources which intersect with area. area = gtk.gdk.Rectangle(x, y, width, height) items = self.items.values() items.sort(lambda i, j : cmp(i['z-index'], j['z-index'])) for item in items: pos_x, pos_y = item['area'][0:2] context.set_source_surface(item['source'], pos_x, pos_y) # context.paint() context.paint_with_alpha(item['opacity']) context.reset_clip() self.window.invalidate_rect((x, y, width, height), False) # Expose. def get_area(self, source, xy): """Calculates the on-screen area of the specified source centred at xy. @param source: @param xy: @return: a tuple (x, y, width, height) """ win_w, win_h = self.window.get_size() # Window width and height. width, height = source.get_width(), source.get_height() x = int((xy[0] * win_w) - width/2) # Round to integer. y = int((xy[1] * win_h) - height/2) # Ensure that source coordinates fit inside dimensions of backing. if x < self.border_x: x = self.border_x elif x + width > win_w - self.border_x: x = win_w - self.border_x - width if y < self.border_y: y = self.border_y elif y + height > win_h - self.border_y: y = win_h - self.border_y - height return x, y, width, height def new_surface(self, width, height): """Creates a new ImageSurface of dimensions (width, height) and ensures that the ImageSurface is cleared. @param width: the expected width of the ImageSurface. @param height: the expected height of the ImageSurface. @return: tuple (surface, context) """ # Create new ImageSurface for hand. surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) context = cairo.Context(surface) # Clear ImageSurface - in Cairo 1.2+, this is done automatically. if cairo.version_info < (1, 2): context.set_operator(cairo.OPERATOR_CLEAR) context.paint() context.set_operator(cairo.OPERATOR_OVER) # Restore. return surface, context def configure(self, widget, event): width, height = self.window.get_size() self.backing = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) # Recalculate position of all items. for id, item in self.items.iteritems(): self.items[id]['area'] = self.get_area(item['source'], item['xy']) self.redraw(0, 0, width, height) # Full redraw required. return True # Expected to return True. def expose(self, widget, event): context = widget.window.cairo_create() context.rectangle(*event.area) context.clip() # Only redraw the exposed area. context.set_source_surface(self.backing, 0, 0) context.paint() context.reset_clip() return False # Expected to return False. pybridge-0.3.0/pybridge/ui/manager.py0000644000175000017500000000464210610463034017316 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from wrapper import GladeWrapper class WindowManager(dict): """A dictionary with features for managing GladeWrapper window instances.""" def open(self, windowclass, id=None, parent=None): """Creates a new instance of a GladeWrapper window. @param windowclass: a subclass of GladeWrapper. @type windowclass: classobj @param id: if specified, an identifier for the window instance. @type id: str or None @param parent: if specified, a parent window to set as transient. @type parent: GladeWrapper instance or None @return: the instance variable of the created window. @rtype: GladeWrapper instance """ id = id or windowclass if self.get(id): raise KeyError, "Identifier \'%s\' already registered" % id instance = windowclass(parent) self[id] = instance return instance def close(self, instance): """Closes an existing instance of a GladeWrapper window. @param id: the window instance. @type id: instance """ if instance not in self.values(): raise ValueError, "Window instance not registered" # Identify the window instance. for id, inst in self.items(): if inst == instance: break # Since a window may close itself, it is necessary to remove the # reference before invoking tearDown(), to prevent an infinite loop. del self[id] instance.tearDown() instance.window.destroy() # An instance of WindowManager to be shared by all windows. wm = WindowManager() pybridge-0.3.0/pybridge/ui/window_main.py0000644000175000017500000002606110636265511020226 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import gtk from wrapper import GladeWrapper from twisted.internet import reactor import webbrowser from pybridge import __version__ as PYBRIDGE_VERSION import pybridge.environment as env from pybridge.network.client import client from eventhandler import SimpleEventHandler from manager import WindowManager, wm from dialog_connection import DialogConnection from dialog_newtable import DialogNewtable from dialog_preferences import DialogPreferences from window_bridgetable import WindowBridgetable TABLE_ICON = env.find_pixmap("table.png") USER_ICON = env.find_pixmap("user.png") class WindowMain(GladeWrapper): glade_name = 'window_main' tableview_icon = gtk.gdk.pixbuf_new_from_file_at_size(TABLE_ICON, 48, 48) peopleview_icon = gtk.gdk.pixbuf_new_from_file_at_size(USER_ICON, 48, 48) def setUp(self): # Use a private WindowManager for table window instances. self.tables = WindowManager() # Set up table model and icon view. self.tableview.set_text_column(0) self.tableview.set_pixbuf_column(1) self.tableview_model = gtk.ListStore(str, gtk.gdk.Pixbuf) self.tableview_model.set_sort_column_id(0, gtk.SORT_ASCENDING) self.tableview.set_model(self.tableview_model) # Set up people model and icon view. # TODO: allow users to provide their own "avatar" icons. self.peopleview.set_text_column(0) self.peopleview.set_pixbuf_column(1) self.peopleview_model = gtk.ListStore(str, gtk.gdk.Pixbuf) self.peopleview_model.set_sort_column_id(0, gtk.SORT_ASCENDING) self.peopleview.set_model(self.peopleview_model) # Attach event handler to listen for events. self.eventHandler = SimpleEventHandler(self) client.attach(self.eventHandler) if not wm.get(DialogConnection): wm.open(DialogConnection, parent=self) def tearDown(self): # TODO: detach event handler from all attached subjects. # Close all windows. for window in wm.values(): wm.close(window) client.disconnect() def quit(self): """Shut down gracefully.""" wm.close(self) reactor.stop() gtk.main_quit() def errback(self, failure): print "Error: %s" % failure.getErrorMessage() # Event handlers. def event_loggedIn(self, username): self.notebook.set_property('sensitive', True) self.menu_connect.set_property('visible', False) self.menu_disconnect.set_property('visible', True) self.menu_newtable.set_property('sensitive', True) self.newtable.set_property('sensitive', True) def event_loggedOut(self): for table in self.tables.values(): self.tables.close(table) self.notebook.set_property('sensitive', False) self.menu_connect.set_property('visible', True) self.menu_disconnect.set_property('visible', False) self.menu_newtable.set_property('sensitive', False) self.newtable.set_property('sensitive', False) self.tableview_model.clear() self.peopleview_model.clear() def event_connectionLost(self, host, port): dialog = gtk.MessageDialog(parent=self.window, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_ERROR) dialog.set_title(_('Connection to server lost')) dialog.set_markup(_('The connection to %s was lost unexpectedly.' % host)) dialog.format_secondary_text(_('Please check your computer\'s network connection status before reconnecting. If you cannot reconnect, the server may be offline.')) # If this problem persists... def dialog_response_cb(dialog, response_id): dialog.destroy() if response_id == gtk.RESPONSE_OK: wm.open(DialogConnection, parent=self) dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) dialog.add_button(gtk.STOCK_CONNECT, gtk.RESPONSE_OK) dialog.connect('response', dialog_response_cb) dialog.show() def event_gotRoster(self, name, roster): lookup = {'tables' : (self.tableview_model, self.tableview_icon), 'users' : (self.peopleview_model, self.peopleview_icon)} try: model, icon = lookup[name] for id, info in roster.items(): model.append([id, icon]) roster.attach(self.eventHandler) except KeyError: pass # Ignore an unrecognised roster. def event_joinTable(self, tableid, table): window = self.tables.open(WindowBridgetable, id=tableid) window.setTable(table) def event_leaveTable(self, tableid): self.tables.close(self.tables[tableid]) # Close window. def event_openTable(self, tableid, info): """Adds a table to the table listing.""" self.tableview_model.append([tableid, self.tableview_icon]) def event_closeTable(self, tableid): """Removes a table from the table listing.""" def func(model, path, iter, user_data): if model.get_value(iter, 0) in user_data: model.remove(iter) return True self.tableview_model.foreach(func, tableid) def event_userLogin(self, username, info): """Adds a user to the people listing.""" self.peopleview_model.append([username, self.peopleview_icon]) def event_userLogout(self, username): """Removes a user from the people listing.""" def func(model, path, iter, user_data): if model.get_value(iter, 0) in user_data: model.remove(iter) return True self.peopleview_model.foreach(func, username) # Signal handlers. def on_notebook_switch_page(self, notebook, page, page_num): pass def on_tableview_item_activated(self, iconview, path, *args): iter = self.tableview_model.get_iter(path) tableid = self.tableview_model.get_value(iter, 0) if tableid not in client.tables: d = client.joinTable(tableid) d.addErrback(self.errback) self.jointable.set_property('sensitive', False) def on_tableview_selection_changed(self, iconview, *args): cursor = self.tableview.get_cursor() if cursor: # Ensure cursor contains a path, not None. iter = self.tableview_model.get_iter(cursor[0]) # Path. tableid = self.tableview_model.get_value(iter, 0) # If client not joined to table, enable Join Table button. sensitive = tableid not in client.tables self.jointable.set_property('sensitive', sensitive) # Display information about table. self.frame_tableinfo.set_property('sensitive', True) self.label_tableid.set_text(tableid) self.label_tabletype.set_text(client.tableRoster[tableid]['game']) else: self.frame_tableinfo.set_property('sensitive', False) self.label_tableid.set_text('') self.label_tabletype.set_text('') def on_peopleview_selection_changed(self, iconview, *args): cursor = self.peopleview.get_cursor() if cursor: # Ensure cursor contains a path, not None. iter = self.peopleview_model.get_iter(cursor[0]) # Path. person = self.peopleview_model.get_value(iter, 0) # Display information about person. self.frame_personinfo.set_property('sensitive', True) self.label_personname.set_text(person) else: self.frame_personinfo.set_property('sensitive', False) self.label_personname.set_text('') def on_window_main_delete_event(self, widget, *args): self.quit() def on_newtable_clicked(self, widget, *args): if not wm.get(DialogNewtable): wm.open(DialogNewtable) def on_jointable_clicked(self, widget, *args): path = self.tableview.get_cursor()[0] self.on_tableview_item_activated(self.tableview, path) def on_connect_activate(self, widget, *args): if not wm.get(DialogConnection): wm.open(DialogConnection) def on_disconnect_activate(self, widget, *args): do_disconnect = True if len([True for table in self.tables.values() if table.player]) > 0: dialog = gtk.MessageDialog(parent=self.window, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_QUESTION) dialog.set_title(_('Disconnect from server')) dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) dialog.add_button(gtk.STOCK_DISCONNECT, gtk.RESPONSE_OK) dialog.set_markup(_('Are you sure you wish to disconnect?')) dialog.format_secondary_text(_('You are playing at a table. Disconnecting may forfeit the game, or incur penalties.')) do_disconnect = (dialog.run() == gtk.RESPONSE_OK) dialog.destroy() if do_disconnect: # Close all table windows, triggers stoppedObserving() on all tables. # TODO: should do this on_disconnected client.disconnect() def on_quit_activate(self, widget, *args): self.quit() def on_preferences_activate(self, widget, *args): if not wm.get(DialogPreferences): wm.open(DialogPreferences) def on_homepage_activate(self, widget, *args): webbrowser.open('http://pybridge.sourceforge.net/') def on_about_activate(self, widget, *args): about = gtk.AboutDialog() about.set_name('PyBridge') about.set_version(PYBRIDGE_VERSION) about.set_copyright('Copyright (C) 2004-2007 Michael Banks') about.set_comments(_('A free online bridge game.')) about.set_website('http://pybridge.sourceforge.net/') license = file(env.find_doc('COPYING')).read() about.set_license(license) authorsfile = file(env.find_doc('AUTHORS')) authors = [author.strip() for author in authorsfile] about.set_authors(authors) logo_path = env.find_pixmap('pybridge.png') logo = gtk.gdk.pixbuf_new_from_file(logo_path) about.set_logo(logo) def dialog_response_cb(dialog, response_id): dialog.destroy() about.connect('response', dialog_response_cb) about.show() pybridge-0.3.0/pybridge/ui/dialog_newtable.py0000644000175000017500000000406410633275133021030 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import gtk from wrapper import GladeWrapper from pybridge.network.client import client from manager import wm class DialogNewtable(GladeWrapper): glade_name = 'dialog_newtable' def setUp(self): pass def createSuccess(self, table): wm.close(self) def createFailure(self, reason): error = reason.getErrorMessage() dialog = gtk.MessageDialog(parent=self.window, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK) dialog.set_title(_('Could not create table')) dialog.set_markup(_('The table could not be created.')) dialog.format_secondary_text(_('Reason: %s') % error) dialog.run() dialog.destroy() # Signal handlers. def on_cancelbutton_clicked(self, widget, *args): wm.close(self) def on_okbutton_clicked(self, widget, *args): tableid = self.entry_tablename.get_text() d = client.joinTable(tableid, host=True) d.addCallbacks(self.createSuccess, self.createFailure) def on_tablename_changed(self, widget, *args): # Disable the OK button if the table name field is empty. sensitive = self.entry_tablename.get_text() != "" self.okbutton.set_property('sensitive', sensitive) pybridge-0.3.0/pybridge/ui/eventhandler.py0000644000175000017500000000256610610463034020366 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from zope.interface import implements from pybridge.interfaces.observer import IListener class SimpleEventHandler: """An implementation of IListener which redirects updates to its target.""" implements(IListener) def __init__(self, target, prefix='event_'): self.__target = target self.__prefix = prefix def update(self, event, *args, **kwargs): """Redirects named event to target's handler method, if present.""" method = getattr(self.__target, "%s%s" % (self.__prefix, event), None) if method: method(*args, **kwargs) pybridge-0.3.0/pybridge/network/0000755000175000017500000000000010637763637016425 5ustar michaelmichaelpybridge-0.3.0/pybridge/network/roster.py0000644000175000017500000000570110610463033020272 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from UserDict import IterableUserDict from twisted.internet import reactor from twisted.spread import pb from zope.interface import implements from pybridge.interfaces.observer import ISubject class Roster(IterableUserDict): """A dictionary-like object, which combines a set of available items with information associated with each item. This class implements the ISubject interface to provide notifications when an entry in the roster is added, removed or changed. """ implements(ISubject) def __init__(self): IterableUserDict.__init__(self) self.listeners = [] def attach(self, listener): self.listeners.append(listener) def detach(self, listener): self.listeners.remove(listener) def notify(self, event, *args, **kwargs): for listener in self.listeners: listener.update(event, *args, **kwargs) class LocalRoster(Roster, pb.Cacheable): """A server-side 'master copy' of a Roster. Changes to the LocalRoster are relayed to registered RemoteRoster objects as well as to all local listeners. """ def __init__(self): Roster.__init__(self) self.observers = [] def getStateToCacheAndObserveFor(self, perspective, observer): self.observers.append(observer) # Assumes that each item has an 'info' attribute. return dict([(id, item.info) for id, item in self.items()]) def stoppedObserving(self, perspective, observer): self.observers.remove(observer) def notify(self, event, *args, **kwargs): # Override to provide event notification for remote observers. Roster.notify(self, event, *args, **kwargs) for observer in self.observers: # Event handlers are called on the next iteration of the reactor, # to allow the caller of this method to return a result. reactor.callLater(0, observer.callRemote, event, *args, **kwargs) class RemoteRoster(Roster, pb.RemoteCache): """A client-side Roster, which mirrors a server-side LocalRoster object by tracking changes. """ def setCopyableState(self, state): self.update(state) pybridge-0.3.0/pybridge/network/error.py0000644000175000017500000000265010574517254020123 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from twisted.spread import pb class DeniedRequest(pb.Error): """Raised by server in response to an unsatisfiable request from client.""" class IllegalRequest(pb.Error): """Raised by server in response to an erroneous request from client. The propagation of this error from server to client suggests there is a bug in the client code (ie. sending invalid or ill-formatted data to the server) or in the server code (ie. mishandling data). Please report any bugs which you discover in PyBridge! """ class GameError(pb.Error): """Raised by game in response to an unsatisfiable or erroneous request.""" pybridge-0.3.0/pybridge/network/usermanager.py0000644000175000017500000000264710610463033021273 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from roster import LocalRoster, RemoteRoster class LocalUserManager(LocalRoster): def userLogin(self, user): self[user.name] = user self.notify('userLogin', username=user.name, info=user.info) def userLogout(self, user): del self[user.name] self.notify('userLogout', username=user.name) class RemoteUserManager(RemoteRoster): def observe_userLogin(self, username, info): self[username] = info self.notify('userLogin', username=username, info=info) def observe_userLogout(self, username): del self[username] self.notify('userLogout', username=username) pybridge-0.3.0/pybridge/network/__init__.py0000644000175000017500000000000010461147750020507 0ustar michaelmichaelpybridge-0.3.0/pybridge/network/localtable.py0000644000175000017500000001631410637437552021100 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import time from twisted.internet import reactor from twisted.spread import pb from zope.interface import implements from pybridge.interfaces.observer import ISubject, IListener from pybridge.interfaces.table import ITable from pybridge.network.error import DeniedRequest, IllegalRequest class LocalTable(pb.Cacheable): """An implementation of ITable suitable for server-side table instances. A LocalTable maintains the "master" game object and provides synchronisation services for remote tables to mirror the game state. """ implements(ITable, ISubject, IListener) info = property(lambda self: {'game': self.gametype.__name__}) def __init__(self, id, gametype, config={}): self.listeners = [] self.id = id self.gametype = gametype self.game = gametype() # Initialise game. self.game.attach(self) # Listen for game events. self.observers = {} # For each user perspective, a remote ITableEvents. self.players = {} # Positions mapped to perspectives of game players. self.view = LocalTableViewable(self) # For remote clients. # Configuration variables. self.config = {} self.config['CloseWhenEmpty'] = True self.config['MultiplePlayersPerUser'] = False self.config['TimeCreated'] = tuple(time.localtime()) self.config.update(config) def getStateToCacheAndObserveFor(self, perspective, observer): # Inform existing observers that a new user has joined. self.notify('addObserver', observer=perspective.name) self.observers[perspective] = observer # Build a dict of public information about the table. state = {} state['id'] = self.id state['gametype'] = self.gametype.__name__ state['gamestate'] = self.game.getState() state['observers'] = [p.name for p in self.observers.keys()] state['players'] = dict([(pos, p.name) for pos, p in self.players.items()]) return state # To observer. def stoppedObserving(self, perspective, observer): del self.observers[perspective] # If user was playing, then remove their player(s) from game. for position, user in self.players.items(): if perspective == user: self.leaveGame(perspective, position) self.notify('removeObserver', observer=perspective.name) # If there are no remaining observers, close table. if self.config.get('CloseWhenEmpty') and not self.observers: self.server.tables.closeTable(self) # Implementation of ISubject. def attach(self, listener): self.listeners.append(listener) def detach(self, listener): self.listeners.remove(listener) def notify(self, event, *args, **kwargs): for listener in self.listeners: listener.update(event, *args, **kwargs) # For all observers, calls event handler with provided arguments. for observer in self.observers.values(): self.notifyObserver(observer, event, *args, **kwargs) def notifyObserver(self, obs, event, *args, **kwargs): """Calls observer's event handler with provided arguments. @param obs: an observer object. @type obs: RemoteCacheObserver @param event: the name of the event. @type event: str """ # Event handlers are called on the next iteration of the reactor, # to allow the caller of this method to return a result. reactor.callLater(0, obs.callRemote, event, *args, **kwargs) # Implementation of IListener. def update(self, event, *args, **kwargs): # Expected to be called only by methods of self.game. for observer in self.observers.values(): self.notifyObserver(observer, 'gameUpdate', event, *args, **kwargs) # Implementation of ITable. def joinGame(self, user, position): if position not in self.game.positions: raise IllegalRequest, "Invalid position type" # Check that user is not already playing at table. if not self.config.get('MultiplePlayersPerUser'): if user in self.players.values(): raise DeniedRequest, "Already playing in game" player = self.game.addPlayer(position) # May raise GameError. self.players[position] = user self.notify('joinGame', player=user.name, position=position) return player def leaveGame(self, user, position): if position not in self.game.positions: raise IllegalRequest, "Invalid position type" # Ensure that user is playing at specified position. if self.players.get(position) != user: raise DeniedRequest, "Not playing at position" self.game.removePlayer(position) # May raise GameError. del self.players[position] self.notify('leaveGame', player=user.name, position=position) def sendMessage(self, message, sender, recipients): names = [perspective.name for perspective in self.observers.keys()] if recipients: # Translate user names to their observer objects. # Remove user names without a perspective object observing table. recipients = [name for name in recipients if name in names] sendTo = [o for p, o in self.observers.items() if p.name in recipients] else: # Broadcast message to all observers. recipients = names sendTo = self.observers.values() for observer in sendTo: self.notifyObserver(observer, 'sendMessage', message=message, sender=sender.name, recipients=recipients) class LocalTableViewable(pb.Viewable): """Provides a public front-end to an instantiated LocalTable. Serialization flavors are mutually exclusive and cannot be mixed, so this class is a subclass of pb.Viewable. """ def __init__(self, table): """ @param table: a instantiated LocalTable. """ self.table = table def view_joinGame(self, user, position): # TODO: return a deferred? return self.table.joinGame(user, position) def view_leaveGame(self, user, position): return self.table.leaveGame(user, position) def view_sendMessage(self, user, message, sender=None, recipients=[]): return self.table.sendMessage(message, sender=user, recipients=recipients) pybridge-0.3.0/pybridge/network/client.py0000644000175000017500000001422410635750101020234 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import sha from twisted.cred import credentials from twisted.internet import reactor from twisted.spread import pb from zope.interface import implements from pybridge.interfaces.observer import ISubject from pybridge.network.localtable import LocalTable from pybridge.network.remotetable import RemoteTable pb.setUnjellyableForClass(LocalTable, RemoteTable) from pybridge.network.tablemanager import LocalTableManager, RemoteTableManager from pybridge.network.usermanager import LocalUserManager, RemoteUserManager pb.setUnjellyableForClass(LocalTableManager, RemoteTableManager) pb.setUnjellyableForClass(LocalUserManager, RemoteUserManager) class NetworkClient(pb.Referenceable): """Provides the glue between the client code and the server.""" implements(ISubject) def __init__(self): self.listeners = [] self.avatar = None # Remote avatar reference. self.factory = pb.PBClientFactory() self.factory.clientConnectionLost = self.connectionLost self.expectLoseConnection = False # Indicates when disconnecting. self.username = None self.tables = {} # Tables observed. self.tableRoster = None self.userRoster = None def connectionLost(self, connector, reason): if self.avatar: # Reset invalidated remote references. self.avatar = None self.tables.clear() self.tableRoster.clear() self.userRoster.clear() self.username = None self.notify('loggedOut') if not self.expectLoseConnection: # Connection lost unexpectedly, so notify user. print "Lost connection: %s" % reason.getErrorMessage() self.notify('connectionLost', host=connector.host, port=connector.port) def errback(self, failure): print "Error: %s" % failure.getErrorMessage() # Implementation of ISubject. def attach(self, listener): self.listeners.append(listener) def detach(self, listener): self.listeners.remove(listener) def notify(self, event, *args, **kwargs): for listener in self.listeners: listener.update(event, *args, **kwargs) # Methods def connect(self, host, port): """Connect to server. @param host: the host name or IP address of the server. @type host: string @param port: the port number on which the server is listening. @type port: int """ connector = reactor.connectTCP(host, port, self.factory) self.expectLoseConnection = False def disconnect(self): """Drops connection to server.""" self.expectLoseConnection = True self.factory.disconnect() def login(self, username, password): """Authenticate to connected server with username and password. @param username: @param password: The SHA-1 hash of the password string is transmitted, protecting the user's password from eavesdroppers. """ def gotRoster(roster, name): if name == 'tables': self.tableRoster = roster elif name == 'users': self.userRoster = roster self.notify('gotRoster', name=name, roster=roster) def connectedAsUser(avatar): """Actions to perform when connection succeeds.""" self.avatar = avatar self.username = username self.notify('loggedIn', username=username) # Request services from server. for rostername in ['tables', 'users']: d = avatar.callRemote('getRoster', rostername) d.addCallbacks(gotRoster, self.errback, callbackArgs=[rostername]) hash = sha.new(password).hexdigest() creds = credentials.UsernamePassword(username, hash) d = self.factory.login(creds, client=self) d.addCallback(connectedAsUser) return d def register(self, username, password): """Register user account on connected server.""" def connectedAsAnonymousUser(avatar): """Register user account on server.""" hash = sha.new(password).hexdigest() d = avatar.callRemote('register', username, hash) # TODO: after registration, need to disconnect from server? return d anon = credentials.UsernamePassword('', '') d = self.factory.login(anon, client=None) d.addCallback(connectedAsAnonymousUser) return d # Client request methods. def joinTable(self, tableid, host=False): def success((table, remote)): table.master = remote # Set RemoteReference for RemoteBridgeTable. self.tables[tableid] = table self.notify('joinTable', tableid=tableid, table=table) return table if host: d = self.avatar.callRemote('hostTable', tableid=tableid, tabletype='bridge') else: d = self.avatar.callRemote('joinTable', tableid=tableid) d.addCallback(success) return d def leaveTable(self, tableid): def success(r): del self.tables[tableid] self.notify('leaveTable', tableid=tableid) d = self.avatar.callRemote('leaveTable', tableid=tableid) d.addCallback(success) return d client = NetworkClient() pybridge-0.3.0/pybridge/network/tablemanager.py0000644000175000017500000000274610610463033021404 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from roster import LocalRoster, RemoteRoster class LocalTableManager(LocalRoster): def openTable(self, table): # TODO: don't notify clients which don't recognise game type. self[table.id] = table self.notify('openTable', tableid=table.id, info=table.info) def closeTable(self, table): del self[table.id] self.notify('closeTable', tableid=table.id) class RemoteTableManager(RemoteRoster): def observe_openTable(self, tableid, info): self[tableid] = info self.notify('openTable', tableid=tableid, info=info) def observe_closeTable(self, tableid): del self[tableid] self.notify('closeTable', tableid=tableid) pybridge-0.3.0/pybridge/network/remotetable.py0000644000175000017500000001010310634254667021270 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from twisted.spread import pb from zope.interface import implements from pybridge.interfaces.observer import ISubject from pybridge.interfaces.table import ITable from pybridge.network.error import DeniedRequest, IllegalRequest # TODO: move to somewhere more appropriate. from pybridge.bridge.game import BridgeGame GAMETYPES = {'BridgeGame' : BridgeGame} class RemoteTable(pb.RemoteCache): """A client-side implementation of ITable providing a "front-end" to a remote server-side LocalTable. RemoteTable mirrors the state of LocalTable as a local cache. External code may, therefore, read the table state without network communication. Actions which change the table state are forwarded to the LocalTable. """ implements(ITable, ISubject) info = property(lambda self: {'game': self.gametype.__name__}) def __init__(self): self.master = None # Server-side ITable object. self.listeners = [] self.id = None self.game = None self.gametype = None self.observers = [] # Observers of master table. self.players = {} # Positions mapped to player identifiers. def setCopyableState(self, state): self.id = state['id'] if state['gametype'] in GAMETYPES: self.gametype = GAMETYPES[state['gametype']] self.game = self.gametype() self.game.setState(state['gamestate']) else: raise NameError, "Unknown game type %s" % state['gametype'] self.observers = state['observers'] self.players = state['players'] for position in self.players: self.game.addPlayer(position) # Implementation of ITable. def joinGame(self, position, user=None): d = self.master.callRemote('joinGame', position) return d def leaveGame(self, position, user=None): d = self.master.callRemote('leaveGame', position) return d def sendMessage(self, message, sender=None, recipients=[]): d = self.master.callRemote('sendMessage', message, recipients) return d # Implementation of ISubject. def attach(self, listener): self.listeners.append(listener) def detach(self, listener): self.listeners.remove(listener) def notify(self, event, *args, **kwargs): for listener in self.listeners: listener.update(event, *args, **kwargs) # Remote update methods. def observe_addObserver(self, observer): self.observers.append(observer) self.notify('addObserver', observer) def observe_removeObserver(self, observer): self.observers.remove(observer) self.notify('removeObserver', observer) def observe_joinGame(self, player, position): self.game.addPlayer(position) self.players[position] = player self.notify('joinGame', player, position) def observe_leaveGame(self, player, position): self.game.removePlayer(position) del self.players[position] self.notify('leaveGame', player, position) def observe_sendMessage(self, message, sender, recipients): # TODO: add to message log? self.notify('sendMessage', message, sender, recipients) def observe_gameUpdate(self, event, *args, **kwargs): self.game.updateState(event, *args, **kwargs) pybridge-0.3.0/pybridge/network/observable.py0000644000175000017500000000756010610463033021105 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from twisted.internet import reactor from twisted.spread import pb from zope.interface import implements from pybridge.interfaces.observer import ISubject, IListener class Observable(object): # object """A generic implementation of ISubject.""" implements(ISubject) def __init__(self): super(Observable, self).__init__() print "observable" self.listeners = [] def attach(self, listener): self.listeners.append(listener) def detach(self, listener): self.listeners.remove(listener) def notify(self, event, *args, **kwargs): for listener in self.listeners: listener.update(event, *args, **kwargs) class LocalObservable(Observable, pb.Cacheable): """An extension of Observable, which relays its notifications to clients by means of communication with registered RemoteObservable objects. Subclasses of LocalObservable may provide the 'master copy' of a state machine, generating notifications to reflect state changes. """ def __init__(self): super(LocalObservable, self).__init__() print "local observable" self.observers = {} # Mapping from perspectives to observers. def getState(self): """Return an object representing the state of the observable. @return: a state object. """ raise NotImplementedError, "Subclass must override getState()" def getStateToCacheAndObserveFor(self, perspective, observer): self.observers[perspective] = observer return self.getState() # Return state to RemoteObservable. def stoppedObserving(self, perspective, observer): del self.observers[perspective] def notify(self, event, *args, **kwargs): #Observable.notify(self, event, *args, **kwargs) super(LocalObservable, self).notify(event, *args, **kwargs) # Override to provide event notification for remote observers. for observer in self.observers.values(): # Event handlers are called on the next iteration of the reactor, # to allow the caller of this method to return a result. reactor.callLater(0, observer.callRemote, event, *args, **kwargs) class RemoteObservable(Observable, pb.RemoteCache): """An extension of Observable, which receives the notifications received from a LocalObservable object and relays them to its own listeners. Subclasses of RemoteObservable may provide a local 'mirror copy' of a state machine encapsulated by a remote LocalObservable. """ implements(IListener) def setState(self, state): raise NotImplementedError, "Subclass must override setState()" def setCopyableState(self, state): self.setState(state) def update(self, event, *args, **kwargs): # This method is only expected to be invoked by observe_update(). self.notify(self, event, *args, **kwargs) def observe_update(self, event, *args, **kwargs): """Receives remote notifications and relays them to local update().""" self.update(event, *args, **kwargs) pybridge-0.3.0/pybridge/environment.py0000644000175000017500000000662610571342336017646 0ustar michaelmichael# PyBridge -- online contract bridge made easy. # Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import os import sys """ This module provides path location services for PyBridge. Note to PyBridge packagers: The packaging policy of your distribution may specify a filesystem organisation standard, which conflicts with the directory structure defined in this module. This is the only module that you should need to modify to make PyBridge compliant with distribution policy. """ # Locate base directory. if hasattr(sys, 'frozen'): # If py2exe distribution. currentdir = os.path.dirname(sys.executable) basedir = os.path.abspath(currentdir) else: # Typically /usr/ (if installed) or root of source distribution. currentdir = os.path.dirname(os.path.abspath(sys.argv[0])) basedir = os.path.normpath(os.path.join(currentdir, '..')) # Locate shared resources directory, typically /usr/share/. if os.path.exists(os.path.join(basedir, 'share')): sharedir = os.path.join(basedir, 'share') else: # Root of source distribution. sharedir = basedir # Locate client configuration directory, typically ~/.pybridge/. clientconfigdir = os.path.join(os.path.expanduser('~'), '.pybridge') if not os.path.exists(clientconfigdir): os.mkdir(clientconfigdir) # Create directory. # Locate server configuration directory. serverconfigdir = clientconfigdir def find_config_client(name): """A client configuration file is located in: / """ return os.path.join(clientconfigdir, name) def find_config_server(name): """A server configuration file is located in: / """ return os.path.join(serverconfigdir, name) def find_doc(name): """A documentation file may be located in: /doc/pybridge/ (installed) / (source) """ if sharedir == basedir: return os.path.join(basedir, name) else: return os.path.join(sharedir, 'doc', 'pybridge', name) def find_glade(name): """A Glade interface file may be located in: /pybridge/glade/ (installed) /glade/ (source) """ if sharedir == basedir: return os.path.join(basedir, 'glade', name) else: return os.path.join(sharedir, 'pybridge', 'glade', name) def find_pixmap(name): """A pixmap file may be located in: /pybridge/pixmaps/ (installed) /pixmaps/ (source) """ if sharedir == basedir: return os.path.join(basedir, 'pixmaps', name) else: return os.path.join(sharedir, 'pybridge', 'pixmaps', name) def get_localedir(): """Returns the path of the locale directory.""" return os.path.join(sharedir, 'locale') pybridge-0.3.0/pybridge/enum.py0000644000175000017500000001260310550242137016231 0ustar michaelmichael# -*- encoding: utf-8 -*- # enum.py # Part of enum, a package providing enumerated types for Python. # # Copyright © 2006 Ben Finney # This is free software; you may copy, modify and/or distribute this work # under the terms of the GNU General Public License, version 2 or later # or, at your option, the terms of the Python license. """Robust enumerated type support in Python This package provides a module for robust enumerations in Python. An enumeration object is created with a sequence of string arguments to the Enum() constructor:: >>> from enum import Enum >>> Colours = Enum('red', 'blue', 'green') >>> Weekdays = Enum('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun') The return value is an immutable sequence object with a value for each of the string arguments. Each value is also available as an attribute named from the corresponding string argument:: >>> pizza_night = Weekdays[4] >>> shirt_colour = Colours.green The values are constants that can be compared only with values from the same enumeration; comparison with other values will invoke Python's fallback comparisons:: >>> pizza_night == Weekdays.fri True >>> shirt_colour > Colours.red True >>> shirt_colour == "green" False Each value from an enumeration exports its sequence index as an integer, and can be coerced to a simple string matching the original arguments used to create the enumeration:: >>> str(pizza_night) 'fri' >>> shirt_colour.index 2 """ __author_name__ = "Ben Finney" __author_email__ = "ben+python@benfinney.id.au" __author__ = "%s <%s>" % (__author_name__, __author_email__) __date__ = "2006-10-13" __copyright__ = "Copyright © %s %s" % ( __date__.split('-')[0], __author_name__ ) __license__ = "Choice of GPL or Python license" __url__ = "http://cheeseshop.python.org/pypi/enum/" __version__ = "0.4.2" class EnumException(Exception): """ Base class for all exceptions in this module """ def __init__(self): if self.__class__ is EnumException: raise NotImplementedError, \ "%s is an abstract class for subclassing" % self.__class__ class EnumEmptyError(AssertionError, EnumException): """ Raised when attempting to create an empty enumeration """ def __str__(self): return "Enumerations cannot be empty" class EnumBadKeyError(TypeError, EnumException): """ Raised when creating an Enum with non-string keys """ def __init__(self, key): self.key = key def __str__(self): return "Enumeration keys must be strings: %s" % (self.key,) class EnumImmutableError(TypeError, EnumException): """ Raised when attempting to modify an Enum """ def __init__(self, *args): self.args = args def __str__(self): return "Enumeration does not allow modification" class EnumValue(object): """ A specific value of an enumerated type """ def __init__(self, enumtype, index, key): """ Set up a new instance """ self.__enumtype = enumtype self.__index = index self.__key = key def __get_enumtype(self): return self.__enumtype enumtype = property(__get_enumtype) def __get_key(self): return self.__key key = property(__get_key) def __str__(self): return "%s" % (self.key) def __get_index(self): return self.__index index = property(__get_index) def __repr__(self): return "EnumValue(%s, %s, %s)" % ( repr(self.__enumtype), repr(self.__index), repr(self.__key), ) def __hash__(self): return hash(self.__index) def __cmp__(self, other): result = NotImplemented self_type = self.enumtype try: assert self_type == other.enumtype result = cmp(self.index, other.index) except (AssertionError, AttributeError): result = NotImplemented return result class Enum(object): """ Enumerated type """ def __init__(self, *keys, **kwargs): """ Create an enumeration instance """ value_type = kwargs.get('value_type', EnumValue) if not keys: raise EnumEmptyError() keys = tuple(keys) values = [None] * len(keys) for i, key in enumerate(keys): value = value_type(self, i, key) values[i] = value try: super(Enum, self).__setattr__(key, value) except TypeError, e: raise EnumBadKeyError(key) super(Enum, self).__setattr__('_keys', keys) super(Enum, self).__setattr__('_values', values) def __setattr__(self, name, value): raise EnumImmutableError(name) def __delattr__(self, name): raise EnumImmutableError(name) def __len__(self): return len(self._values) def __getitem__(self, index): return self._values[index] def __setitem__(self, index, value): raise EnumImmutableError(index) def __delitem__(self, index): raise EnumImmutableError(index) def __iter__(self): return iter(self._values) def __contains__(self, value): is_member = False if isinstance(value, basestring): is_member = (value in self._keys) else: try: is_member = (value in self._values) except EnumValueCompareError, e: is_member = False return is_member pybridge-0.3.0/pybridge/__init__.py0000644000175000017500000000035010610463032017014 0ustar michaelmichael""" PyBridge - a free online bridge game. """ __version__ = '0.3.0' __author__ = 'Michael Banks ' __license__ = 'GNU General Public License, Version 2 or later' __url__ = 'http://pybridge.sourceforge.net/' pybridge-0.3.0/man/0000755000175000017500000000000010637763637013702 5ustar michaelmichaelpybridge-0.3.0/man/pybridge.60000644000175000017500000000137410477323513015566 0ustar michaelmichael.TH PYBRIDGE 6 "5 September 2006" "" "" .SH NAME PyBridge \- a free online bridge game. .SH SYNOPSIS .B pybridge .SH DESCRIPTION \fBpybridge\fR is a graphical interface for playing the card game of (contract) bridge over the Internet or a local network. To actually use \fBpybridge\fR, you need to connect to a PyBridge game server. You may run a game server yourself (see \fBpybridge-server\fR) or connect to a public server. A list of public servers, along with other information, can be found at \fIhttp://pybridge.sourceforge.net/\fR. .SH OPTIONS \fBpybridge\fR accepts no options. .SH SEE ALSO \fBpybridge-server\fR(6) .SH AUTHOR This manual page was written by Michael Banks . .SH BUGS Plenty. Please report any bugs you find. pybridge-0.3.0/man/pybridge-server.60000644000175000017500000000144710477261747017105 0ustar michaelmichael.TH PYBRIDGE-SERVER 6 "5 September 2006" "" "" .SH NAME PyBridge Server \- a game server for PyBridge. .SH SYNOPSIS \fBpybridge-server\fR [options] .SH DESCRIPTION \fBpybridge-server\fR is a game server for PyBridge. It accepts incoming network connections from \fBpybridge\fR clients. For more information, visit \fIhttp://pybridge.sourceforge.net/\fR. .SH OPTIONS .TP \fB\-h\fR, \fB--help\fR Print help message and exit. .TP \fB\-p\fR \fIportnum\fR, \fB--port\fB \fIportnum\fR Listen for incoming connections on port \fIportnum\fR. (The default port is 5040.) .TP .BR \fB\-v\fR, \fB--version\fR Print version information and exit. .SH SEE ALSO \fBpybridge\fR(6) .SH AUTHOR This manual page was written by Michael Banks . .SH BUGS Plenty. Please report any bugs you find. pybridge-0.3.0/PKG-INFO0000644000175000017500000000051210637763637014222 0ustar michaelmichaelMetadata-Version: 1.0 Name: pybridge Version: 0.3.0 Summary: A free online bridge game. Home-page: http://sourceforge.net/projects/pybridge/ Author: Michael Banks Author-email: michael@banksie.co.uk License: UNKNOWN Download-URL: http://sourceforge.net/project/showfiles.php?group_id=114287 Description: UNKNOWN Platform: UNKNOWN pybridge-0.3.0/glade/0000755000175000017500000000000010637763637014203 5ustar michaelmichaelpybridge-0.3.0/glade/pybridge.glade0000644000175000017500000026645210637736435017021 0ustar michaelmichael True PyBridge True True True _Server True True _Connect to Server... True True gtk-connect 1 GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _Disconnect True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-disconnect True True False _New Table... True True gtk-new 1 True False _Open Game... True True gtk-open 1 True True gtk-quit True True True _Options True True True gtk-preferences True True True _Help True True PyBridge _Home Page True True gtk-home 1 True True _About PyBridge True True gtk-about 1 False False True False True True 8 8 320 240 True True GTK_POLICY_NEVER GTK_POLICY_AUTOMATIC GTK_SHADOW_IN True True 160 True 4 True False True True 0 0 True 2 True gtk-new False False True New Table... True False False 1 False False True False True True 0 0 True 2 True gtk-jump-to False False True Join This Table True False False 1 False False 1 True False True 4 4 4 4 True 2 2 8 2 True 0 ID: GTK_FILL True 0 1 2 GTK_FILL True 0 Type: 1 2 GTK_FILL True 0 1 2 1 2 GTK_FILL True Table Information True label_item 2 False 1 False True Available Tables tab False False True 8 8 320 240 True True GTK_POLICY_NEVER GTK_POLICY_AUTOMATIC GTK_SHADOW_IN True True 160 True 4 True False True 4 4 4 4 True 1 2 8 2 True 0 Name: GTK_FILL True 0 1 2 GTK_FILL True Person Information True label_item False 1 1 False True People Online tab 1 False False 1 True Connect to Server False GDK_WINDOW_TYPE_HINT_DIALOG GDK_GRAVITY_CENTER True True 4 4 True 0 GTK_SHADOW_NONE True 4 4 16 True 2 2 8 4 True 0 Port: 1 2 GTK_FILL True 0 Host Name: GTK_FILL 140 True True * 1 2 80 True True 5 * 1 2 1 2 True <b>Server Address</b> True label_item True 0 GTK_SHADOW_NONE True 4 4 16 True 4 True 2 2 8 4 True 0 User Name: GTK_FILL True 0 Password: 1 2 GTK_FILL 120 True True 40 * 1 2 120 True True 40 False * 1 2 1 2 True True Save Password True True False False 1 True True Register as New User True True False False 2 True <b>User Account</b> True label_item False False 1 2 True GTK_BUTTONBOX_END True True True gtk-cancel True -6 True False True True gtk-connect True -5 1 False GTK_PACK_END True New Table False True GDK_WINDOW_TYPE_HINT_DIALOG True GDK_GRAVITY_CENTER True True 4 4 4 4 True 1 2 4 2 140 True True * 1 2 True Table Name: GTK_FILL 2 True GTK_BUTTONBOX_END True True True gtk-cancel True -6 True False True True gtk-ok True -5 1 False GTK_PACK_END True Table True True GTK_TOOLBAR_BOTH True Take Seat True gtk-media-play False False True False Leave Seat True gtk-media-stop False True False False True Game Info True gtk-info True False True Chat True gtk-justify-fill True False True gtk-fullscreen False True False False True Leave Table True gtk-quit False False False True True True GTK_POLICY_NEVER GTK_POLICY_NEVER True 240 True GTK_SHADOW_OUT GTK_POS_LEFT True 4 4 True 2 <span size="x-large">No contract</span> True False False True 4 True True False True 2 <span size="x-large"><b>0</b> (0)</span> True True Declarer True label_item False True False True 2 <span size="x-large"><b>0</b> (0)</span> True True Defence True label_item 1 False 1 True 2 2 8 2 True 0 Dealer: GTK_FILL True 0 Vulnerability: 1 2 GTK_FILL True 0 True 1 2 True 0 True 1 2 1 2 GTK_FILL False 2 True True True True GTK_POLICY_NEVER GTK_POLICY_AUTOMATIC 150 True True True False False True Bidding tab False False True True GTK_POLICY_NEVER GTK_POLICY_AUTOMATIC 150 True True True False 1 False True Tricks tab 1 False False True True GTK_POLICY_NEVER GTK_POLICY_AUTOMATIC True True 2 False True Score Sheet tab 2 False False 3 True True 102 True True GTK_POLICY_NEVER GTK_POLICY_AUTOMATIC GTK_SHADOW_IN True True False True Observers label_item False 4 240 120 True GTK_SHADOW_OUT GTK_POS_LEFT 300 160 True 4 4 True True GTK_POLICY_NEVER GTK_SHADOW_IN True True False GTK_WRAP_WORD True 4 True True True False True Send True False False 1 False 1 1 False 1 1 True False False 2 True 5 PyBridge Preferences False GDK_WINDOW_TYPE_HINT_DIALOG False True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK 2 True True 8 8 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 3 2 8 4 160 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 2 1 2 160 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 2 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Background: 1 2 GTK_FILL True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Card Style: GTK_FILL True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Suit Colours: 2 3 GTK_FILL True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 4 True True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK C True False True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK D True False 1 True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK H True False 2 True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK S True False 3 1 2 2 3 False True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Display suits as symbols? True 1 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <i>Changes to these settings are effective after restarting PyBridge.</i> True True 2 False True Game Display tab False False 1 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK GTK_BUTTONBOX_END True gtk-cancel True True gtk-ok True 1 False GTK_PACK_END pybridge-0.3.0/MANIFEST.in0000644000175000017500000000034110637711335014647 0ustar michaelmichaelinclude MANIFEST.in include AUTHORS COPYING INSTALL NEWS README bin/pybridge.desktop include glade/*.glade man/* recursive-include locale */LC_MESSAGES/pybridge.mo recursive-include pixmaps *.ico *.png *.svg prune locale/src pybridge-0.3.0/AUTHORS0000644000175000017500000000023110571344163014155 0ustar michaelmichaelAuthors: Michael Banks Contributors: Sourav K. Mandal Artwork: Stephen Banks pybridge-0.3.0/setup.py0000644000175000017500000000257410637711041014627 0ustar michaelmichael#!/usr/bin/env python import glob from distutils.core import setup from pybridge import __version__ # To create a standalone Windows distribution, run "python setup.py py2exe" # and then copy the GTK/etc and GTK/lib directories into dist/ try: import py2exe except ImportError: pass setup( name = 'pybridge', version = __version__, author = 'Michael Banks', author_email = 'michael@banksie.co.uk', url = 'http://sourceforge.net/projects/pybridge/', description = 'A free online bridge game.', download_url = 'http://sourceforge.net/project/showfiles.php?group_id=114287', packages = ['pybridge', 'pybridge.bridge', 'pybridge.interfaces', 'pybridge.network', 'pybridge.server', 'pybridge.ui'], scripts = ['bin/pybridge', 'bin/pybridge-server'], data_files = [('share/applications', ['bin/pybridge.desktop']), ('share/doc/pybridge', ['AUTHORS', 'COPYING', 'INSTALL', 'NEWS', 'README']), ('share/pybridge/glade', glob.glob('glade/*.glade')), ('share/pybridge/pixmaps', glob.glob('pixmaps/*')), ], # py2exe console = ['bin/pybridge-server'], windows = [{'script' : 'bin/pybridge', 'icon_resources' : [(1, 'pixmaps/pybridge.ico')]}], options = {'py2exe': {'packages' : 'encodings', 'includes' : 'cairo, pango, pangocairo, atk, gobject, gtk.glade' } }, ) pybridge-0.3.0/COPYING0000644000175000017500000004313310423140124014133 0ustar michaelmichael GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. pybridge-0.3.0/README0000644000175000017500000000311510637515540013773 0ustar michaelmichaelPyBridge 0.3.0 - a free online bridge game http://pybridge.sourceforge.net/ http://sourceforge.net/projects/pybridge/ About PyBridge ============== PyBridge allows you to play the card game of (contract) bridge, with human players, over the Internet or a local network. There are many Web resources for learning to play bridge. A good starting point is the Wikipedia: see http://en.wikipedia.org/wiki/Contract_bridge. Release Notes ============= To use PyBridge, you need to connect to a game server. You have two options: - Connect to a public server. A list of known public servers can be found on the project website. - Run a PyBridge game server yourself, using the pybridge-server script. When connecting to a PyBridge server for the first time, you need to ensure that the "Register as New User" checkbox is ticked. Installation instructions can be found in the INSTALL file. Details of new features, bug fixes and other changes can be found in the NEWS file. Your feedback, bug reports and ideas for new features are especially welcome. - Forums: http://sourceforge.net/forum/?group_id=114287 - Bug reports: http://sourceforge.net/tracker/?group_id=114287&atid=667822 - Feature ideas: http://sourceforge.net/tracker/?group_id=114287&atid=667825 If you are a competent (or aspiring!) Python programmer and would like to contribute code to PyBridge, then please get in touch with the developers! Copyright Information ===================== PyBridge is released under the GNU General Public License, version 2 or later. A copy of the license is provided in the COPYING file. pybridge-0.3.0/NEWS0000644000175000017500000000563710637740044013624 0ustar michaelmichael===================== 0.3.0 (25 June 2007) ===================== New features ------------ - Architecture: - Separation of bridge game logic from 'table' code in [Local|Remote]Table and Game classes. This enables other games to be implemented as separate packages and to be supported by the existing network code. - Bridge facilities: - Implementation of Thomas Andrews' "impossible bridge book" algorithm, which provides a one-to-one correspondence between all possible hand deals and numerical index values. See http://bridge.thomasoandrews.com/impossible/ for details. - Miscellaneous: - Use ConfigObj to provide settings management for the graphical client. This introduces a dependency on ConfigObj. - Manual pages included in source distribution. - Server: - Use SQLObject (an object relational mapper, see http://www.sqlobject.org/) to provide user account database; replaces the flat-file username/password structure used previously. This introduces a dependency on SQLObject and a compatible database backend - see the INSTALL file for more information. - User interface: - Card area display reimplemented in Cairo, with enhancements: - Rotation of table view when user selects a position to play. - Separation of display canvas from card and hand logic, for reusability. The canvas module may be useful for other projects. - Introduction of a preferences dialog, allowing user to customise the appearance of the bridge game display. - Error notification to user when connection is dropped unexpectedly; require confirmation when user requests disconnection whilst playing game. ====================== 0.2.1 (16 August 2006) ====================== Bug fixes --------- - Fixed setup.py script to install source package and supporting files in the standard /usr/ directories. - Updated pybridge.environment and bin/* scripts to work with both the source and installation directory layouts. ====================== 0.2.0 (14 August 2006) ====================== New features ------------ - Substantial rewrite of network code. Focus on splitting discrete services into separate components and making full use of Twisted's pb.Cacheable. (Support for other games can be added simply by writing new table modules for server and client subclassing LocalTable and RemoteTable respectively.) - Introduction of an event handler and callback mechanism, which provides an indirect interface between network code and user interface components. - Facelift for user interface: tables now display as separate windows, enhancements to CardArea display widget, new icons and logo artwork. - Basic table chat support. - Initial support for internationalization and localization via gettext. ==================== 0.1.0 (19 July 2006) ==================== - Initial release. pybridge-0.3.0/INSTALL0000644000175000017500000000426710637515403014153 0ustar michaelmichaelInstalling PyBridge =================== If you want to install PyBridge, change to the directory where you unpacked the source tarball, then type (as root): python setup.py install Alternatively, you can run PyBridge from the extracted tarball archive. Change to the PyBridge 'bin/' directory, then type './pybridge' to run the graphical client, or './pybridge-server' to run the standalone server. PyBridge packages are available for Debian GNU/Linux and Ubuntu systems, thanks to David Watson . Please visit: - For Debian: http://packages.debian.org/pybridge/ - For Ubuntu: http://packages.ubuntu.com/pybridge/ Users of Microsoft Windows may wish to use the 'py2exe' PyBridge distribution, which provides native executable (.exe) versions of the 'pybridge' and 'pybridge-server' scripts, and bundles all the library software (see below) required to run PyBridge. Requirements ============ You will need the following software installed to run PyBridge: - Python (>= 2.4) - http://www.python.org/ - Twisted Core (>= 2.0) - http://twistedmatrix.com/trac/wiki/TwistedCore - Zope Interface (>= 3.0) - http://www.zope.org/Products/ZopeInterface To run the PyBridge client, the following software is also required: - GTK+ (>= 2.8) - http://www.gtk.org/ - PyGTK (>= 2.8) - http://www.pygtk.org/ - Cairo (>= 1.0) - http://cairographics.org/ - PyCairo (>= 1.0) - http://cairographics.org/pycairo/ - ConfigObj (>= 4.0) -http://www.voidspace.org.uk/python/configobj.html To run the standalone PyBridge server, the following software is also required: - SQLObject (>= 0.7) - http://www.sqlobject.org/ - A database server compatible with SQLObject: see "Configuring the Server". Configuring the Server ====================== By default, the standalone server creates and uses a SQLite database in the ~/.pybridge/ directory. This is suitable for hosting LAN games, but if you wish to operate a high-load PyBridge server on the Internet, it may be necessary to switch to an external database server, such as MySQL or PostgreSQL. To change the database settings, edit the ~/.pybridge/server.cfg file. The fields to change are documented in the pybridge/server/config.py module.