From e3ae35e70ea1a0f126af6caf149cb612fa7d1c04 Mon Sep 17 00:00:00 2001 From: bitcraft Date: Thu, 19 Jan 2012 19:31:57 -0600 Subject: [PATCH] rewrite for version 3 --- 16x16-overworld.png | Bin 0 -> 39286 bytes README | 87 ++- TODO | 2 - formosa.tmx | 16 + npc/pirate/.actions.py.swp | Bin 0 -> 20480 bytes npc/pirate/actions.csv | 15 - npc/pirate/actions.py | 235 ++++++- npc/pirate/actions.pyc | Bin 0 -> 9124 bytes npc/pirate/precept.csv | 0 pirate.py | 190 +++--- pygoap.py | 1083 ------------------------------- pygoap/.README.swp | Bin 0 -> 16384 bytes pygoap/.actions.py.swp | Bin 0 -> 20480 bytes pygoap/.agent.py.swp | Bin 0 -> 16384 bytes pygoap/.blackboard.py.swp | Bin 0 -> 20480 bytes pygoap/.environment.py.swp | Bin 0 -> 16384 bytes pygoap/.environment2d.py.swp | Bin 0 -> 20480 bytes pygoap/.goals.py.swp | Bin 0 -> 20480 bytes pygoap/.goaltests.py.swp | Bin 0 -> 12288 bytes pygoap/.planning.py.swp | Bin 0 -> 24576 bytes pygoap/.tiledenvironment.py.swp | Bin 0 -> 12288 bytes pygoap/README | 102 +++ pygoap/__init__.py | 1 + pygoap/actions.py | 194 ++++++ pygoap/actionstates.py | 6 + pygoap/agent.py | 177 +++++ pygoap/blackboard.py | 169 +++++ pygoap/environment.py | 186 ++++++ pygoap/environment2d.py | 137 ++++ pygoap/goals.py | 290 +++++++++ pygoap/goaltests.py | 2 + pygoap/objectflags.py | 20 + pygoap/planning.py | 263 ++++++++ pygoap/tiledenvironment.py | 42 ++ pygoap/tmxloader.py | 625 ++++++++++++++++++ pygoap/util.py | 10 + 36 files changed, 2597 insertions(+), 1255 deletions(-) create mode 100644 16x16-overworld.png delete mode 100644 TODO create mode 100644 formosa.tmx create mode 100644 npc/pirate/.actions.py.swp delete mode 100644 npc/pirate/actions.csv create mode 100644 npc/pirate/actions.pyc delete mode 100644 npc/pirate/precept.csv delete mode 100644 pygoap.py create mode 100644 pygoap/.README.swp create mode 100644 pygoap/.actions.py.swp create mode 100644 pygoap/.agent.py.swp create mode 100644 pygoap/.blackboard.py.swp create mode 100644 pygoap/.environment.py.swp create mode 100644 pygoap/.environment2d.py.swp create mode 100644 pygoap/.goals.py.swp create mode 100644 pygoap/.goaltests.py.swp create mode 100644 pygoap/.planning.py.swp create mode 100644 pygoap/.tiledenvironment.py.swp create mode 100644 pygoap/README create mode 100644 pygoap/__init__.py create mode 100644 pygoap/actions.py create mode 100644 pygoap/actionstates.py create mode 100644 pygoap/agent.py create mode 100644 pygoap/blackboard.py create mode 100644 pygoap/environment.py create mode 100644 pygoap/environment2d.py create mode 100644 pygoap/goals.py create mode 100644 pygoap/goaltests.py create mode 100644 pygoap/objectflags.py create mode 100644 pygoap/planning.py create mode 100644 pygoap/tiledenvironment.py create mode 100644 pygoap/tmxloader.py create mode 100644 pygoap/util.py diff --git a/16x16-overworld.png b/16x16-overworld.png new file mode 100644 index 0000000000000000000000000000000000000000..46075feccfad2ae84f7ca3a40401334fc9889e10 GIT binary patch literal 39286 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�Daf|k|D=^5SW)9@a9lBRQ6 zgfYBDcm^6HqX4y0RDv{7ree$GsgSn~S?KuA99#j~14va}HQ%T4r}r<|%ir`+(_G+s zdmRgo`Fw+K@sf;!_ntW5hQPVLP1Zr3D?=nR$e&YHC4Hj3Uan48VXY9pOHCFBS@lTey8@KMPfbZbl`eYGXNiCN=R`Xka1qdKPVBR|yX@Yd&dRE!h#ua#Yp8A_Y2C*3 z&4p6$`|V~!aY;nJ9>bms&_TGjvyjqC)iFYYK zxh9K!y-lxmM_SM6PzczBey(Wh;uU9!qafOdY-*#Pseek?`C&FdC z`MCFTw>F2eSHjtulbjjDIQmqppa)NU5PNm zykvxh>MOkw_IG*ybmHRq->azlclz9jHcXpTad*60nt;>oVh1ieaKf)&lUTJ2_B~TL z`=En<<$dgV?s5q)dLFt6UR6ZV4+~ngpB$9!B&#JC_^Kr$Yvf_6JR^i+pU(Yv7G2>5 zn@EA>4MfjH7e3T;0_i%1R;Y8c-!b{FzAgp5ej?NQo&TmrPdvlpGJiF8BjM{IzVB$a zDOQ}2Ds)gKZyZfWAz%^fbK;k9_&PS9z!?{`q-%CJlGuGh^#!FSr^ zTvWsLFHN55R(yChKg^dN&-7h|w3*E>a54=*eiB(s6MvEB9UcyRzko8?vn{eAtXEi< z`$T9PXg_Op29SK1KH_)!L9u1`)T!Bj$1Lvg=htM&=QQ{C{o&=hsELGu1$V(Kw+dj1 zgxj85*e-3QO=y#~&7FARxWJf-Lx-XBH%4bfYu$e3&ChY8Xbfc&xk(Gl6tMhs%Fz4Y z&ySVX%RWuSo@oc#hBPOSp55fegV=7}4a&u&cXi9CJNRf80!odXrO3tv<;nXu%k*w` zK6-Dyu7nJhhju3og)RZi9u*zuVR|39EO?2RR6;FO7h^GHSJY)wC}G>Mv|+=%mLpeK z-Z`a*ph$88thdKirGaGu$&n_|tec(lwLzQq)D-uwq0T=pIC>>#*~E?-ci06-^x>v4 zjSl(*ya*qPL&*^0$`*V<%9-wp$D*T1jviPxE}}5Iswy=kL>^Ao0}fs~y8AqmH%qAL zmhr|67BjlicOzab^FM!rm`)mM^0}chcpvvT0_LOn5C5RKF(iDat1MJ=!jOpgwjG~u zvQcojGCNLW)0AnsNaw4Cr~bwRxX8z4*<5XX-$)J2-5li85CPo+d!#6Qksp+#Ho$H@ z_+^bk5U3LNoE+3uJ2W)9`Z;%)(vJkavOo7|JC*{(&Axn%g;WFcBn3$gc3D6i+;vTV zw3S|E2_rj5s*YP9`PAq5R)lqfr%n0=Y@BRU?K(1>o`_pW3NEvhtkdgae~b@s=XKpR zF&`hn7(1SQSI)N@3?}rrUNkNU3Q)c{7wW`{2ya&N0&I;0uY=}K`t2opw3r8)^AT*^ zCb4rrC5+i995nmwSq4ZalCG#+1+UzK#7j!40%eY_6dzm_%YU1f%2CgitumNkfSy)^ zcR_v@5hfzfcpvw{MECKu608S15RKej4X62TM2u2mkpRe!$(Ms?mtSlpEm z>2Wf#Un+Zec|+xR{ov@BrcsMuH*4w#R%L0!l>{0tUxpfKG_aQYWpR#^3fZiIg9E8^ z4fmN&R}novl)e&ON;rrX(w^q-D=EVXNcn|A-!fv_=W z<52P#0t7(@PLm0+Orn%iBUjc&9q?p6Y-6PW>Za_ z!Sk_aWjXB&&?g*IewfQSTj?rnzg&l%EUxj#B+=czGnavq*qvO)YYFEx`H2)vYg~i= zz|^k)qr3e@HE{M*apQUFTQ#&7X&QOyIJOq)KewP)=V&k}C%F58v_?_|8rf!PHCu)Cz&^7wgf&5N7 z`SSXvo5qHWx_$b;V7Xb2wa(Y;ZQcTZkE`rpv`8m-&og@D&u;mhOKeEyL{ES8tPBVR zA6?7J{^eLc`Va$69ebfXT=DEoM0VD%J?2H}7Pc@z5iAEcZ2Tkx7e(0#?Z<|#8#qSF z4#l$F|485{^iR#Rx9j_!%zn!@IBJUkr3)IJmNrgT7=MCs_V!J>n7qgcGg>Wrs)Tk9 znoOJr1BBmSbf!ei-bX@#7`V7kWn>zNH4luPq|0uv{(O!~~Ud>8ce2cO>e zEp_JyMI`(k(MaJpmh=9743WBD44TnN{K%YN56YEfqvt})7x%dhsy%kKk$P?)(QZ!q z#-xD82*SM!dV3x}HReCoM0B{|jiq4JmJeR`mY})=tV)rnFcXQe2EQ;b;?dcLe$tcs z@H<(~vo`_N{wt8RDf)aI39g8qGN0Y!Y8sFgJvmC0cw5+#Zvf2;H-bXYt2URYAfY%? z2G5U!vjKA=@?fAyp+Gtbek|E`9IGZkBe+;Xw;KziT%X?`+YhF#Lm@#?M|L;LNEnl#GB7e-drd|Uv&x}RDpc~n(oK5B z<6;TE^+Go8^^|K-k!DVp-qN%8vtirifF&Le?e-Rs2GNGaF6%F6*oY8&m@-2Ep^B9i zg5Tt~sf)kauK~=Y3OF#j9BSYy6!bcE2pWy3Kkxws36LT<;HM3J$f(X5<8)FHSNZ5$ zb^#+fk>2S!<`EQFAp>J43kSLbE2xg>0PF?$VsfQ0$m$KHfYm=HTtB_ZG2>4{hZfvA zmxFw65Ea_=5(*UFS9JF^B-3Q$9_FBET2VnCS2s-st-@YvLp^ciZe}l3C=7PYFkl1zz1fhG?qh^(cVreHKS*)DgLN^`^c1A9Li+)r@jlAU$MD z8OHR;)y8RZNN{l+bs9luOK-Ya8{oinwvW zCmxE_dI!Pgtc?!t2hbE)cafk8n;(N$MeimmnMe9mHr>WK`=i_WP9FP**KN)R&O@!+# zSXoy2)F3oNR^r8cH!zLm_uFIOz+Mf|fjeh|J2%|-Zo#K?6yMd`30SaGDJ?oXl)NkW z@OEdz4|+C=(|Ts(NK)MKpN-wm71C21r109?Iz{n}c#1X5!ewYE!Fm4+I6Z_v7Naq6 zPAX-wQMQ-e)Umr@uJB^1N5Yq~xp90e27AwTk!c4nV}!o<@jZ*y`=7KB9*XY(R*+n@ z>$J4a>D@_Eza0i1m5A9JMpUH|AuYpfy5%S1v7YljrPuc2jA=(7n`9t8ciBEIo_=6l zkcjabC=<u%L>e7dxoFe$P^1-F_BGGG;8e|{V963R6SVf z+KnzpK|0R%A8Cj(9{S?Kt`^^sMipQZOr4PoQdrLQ7 zIj76Z)y1b%b&rpeiwSQVdxf1g=tXCoitE4Zn7lMK*N`ECsr6TY6;#2$<$3?$VU)XHqZq?p^ggDoG2m~7LtdtO(c zjs~j(kX15}YWM7%^pFEcLW4A&)ZMKqWSz^(e9HJZBWcJaRc3n^!C>q0&=)2QlnB9vdcKiF~2 zuwS7hLSM{Wl*TG_c?3+@Dm)$=c8&-X0tC_$LXBT-Yzr5x9sjuDVgA&s$fIWU zKUt(@LphbaWdF?VCIv*k0~7$Vg@IukCX>5#VL5Q%wgl?y+xpF@u)3}FqvMb)>d3R< ztprH_altj7huMV*=D)4?0aF)%$ryPWQLOEwOuvL=e}6hE)E!qQz}Z$B@fe}^a220+ zG(ZFJ(FnCZh9WtxdF>!fM;-9gvrgk|ncN=bY0>dp7$lwz7qmgI+kVB<%I8);)h5p_ zpmQO;o3Vq>-zC%)AJGov7M8>ES4Igiz5}Cf$n)dQK&)@{+V zTPPvQ)w>i~Z8Mlm`V~T$)vsuweqROp2sBH)RH|hSQAd}$?$SZ-^bhSqXa)%dmA}j# z;FsHr>xp|JB5x1NnCT5vEY6}bl2mcyZ-KARM%m)2oYog_vr?VbjzAvQ=8!tmr~xDj zBfCTz=jcyR8XK8tKQT6rH}5k9ow<3(_il)&u0FH(y6+4YPxKjl6}RX!f01(Qf@_rK z5fM@e=eBR|7w`0mGDICEvUrQrY|aD9I$MS)8ofaA0uGm7CmmCJs!kJ}7L1SrLNjCd zB*!?fk~AL;Jbb+>zVW0wDPFclxCJHHEwOq8id{LDY7U?XQ}`GDyRVJ06QS=|K$v0v z={FmaA4N}C)q?71N)SnX&Dz>YyXo@@vpdoZ)j!}Ot9*J;8M$(2-%hEp^UW|`h*DGk z?}%*C2lIV#gJD4@4V`#X_D{uKm}rrmRw~IFVL+zzjyla;6YzBqwtaTz4L)8V2>5w2 zs)nq9B8x;Bl+YV+;FMkyGcCih$k5QA5lFtTj+GMEH8Dv=E7Z=O!xRHQNtnLW9FA%G zQR4zC#~m@@h87!$Q~vaH2B6uAmjzszGk?vFoHo~@D_tCLE4EU&WwSiPz8Zon$zo#{ zp864eQ*(z4(o1s?j-8+)j6NEOduh4=Fjb+gNi9+~i3WhCRkM)ctMS(n3^Y{TH30ut z69=gWqS?khlX~+UKRt%&-sm8Jb(5Bj*)2~D zeafS?wz;C>Ed7H!@l*ybjek!f(-GAs&_TI}b8X{*+>@9OB#lEJsU=qhh}Di3&c z1WLcKb){JT29~(<&(!_Z1354T{|p;^r&ZI1^O~r0@@27XgQ7fIEae1v2hg8v zyR_hbDvqu%`D5IQXGmw<02#yI%3?{(r@eVqlktF|@*JgQguVJFW64F!a;l5}pYS^4 zxJ!eJ;sDqE02N^(7o?<K{mL z7vAW&;j*ajqd`#mPxWdd{Zvj>A*{>5`k4#>E~S*yGjgHkbPag$1eUC76Bx+fY&YCK zpazSkxfV=QfNs?`{m$AkH2dD4{opNnr#Dy|@yyX6?;m7fbBFwsrn+b)ux-_GCmwI@ zq(KH;yM9z&EX_Pe1~lJ``2J!O;qPY~zoa*Jxg2^ZKK5J@(tpF8P#J=}Q_4t75CMv{(VJ}N`-tHd5@HINhL=pa>eTsN?4d@L) zwe>eVbYrBV-ZAi@GDyeJQ=cvUnybN6JQ8|HfLvE@u;ltXoz&5u8b+fmaH|yqQUu_K zLz31SnZLa(<&E{w&~8ll%ew>fxDFl|OhoM_KV?Z0r88qXrl3=ey_b~zU#4QTf~?0lb?fS<4;$VhYp#wi;ZDmCKprv+CpL%WroO)O=`ghIzRw(3V&nXxHGFKarF}l zGA@Gir4r)os&YGn9xqv%6dtUs!>4$PViMt&se%k!(w>t(h4_Otc)DKHoENB~2i4$u zT6fEAGXNwbE~~}p%RjDD63MsJI4-cK+?=;ObUo=o$a+%H(DW&5M%g=IOmg%RR=2U= z2m3y+P4up3V`L4p(fIG*(tp=!D`hzbuARPd7V}{Iw-0wH3}EP|toPV0=#XDz`Y(!F z_HKX@2Qcl69C*H0)2Hjd)%3m4&ex^*o9E2m8a9wZi$3^i=npZe;q{yJx+doOc~%D?buSF?(?Eoo=2aDMhR>$P znn}x$dAt~`@;^J}9a6uCM%YX|)ysM$?)?1i#0q%AE9>?In{Lh76a3nd?Qzg=5t>_T z^q{lX17|zA;)QXiz#~rbsVN{#&DK0K`wgsg6UhcpMu-kyi#%~COcil&?T6s@H1(#DzTk6VkyT0$X9bt6aZ*TNFY9U^Vv zQ9{783ARjV3Tl*1*B0`G2sx10_a~NSAApD>sPZLP>nS`(4v=`qlMtkNn>fv5>jtm) z;UVz>i!xTe?7lN>NEIk;Dv^41=E*U{Bn_VkxdvsM+Vu}Eh6CUZ)%xb-x3ntJrQvcz z;X_byp>AbMVK|y1)PAwxoOH3d; zNPG)Nx#1?5Qltq?Ve6bAHd%@Qv)4bVS)P}E~ce6s5NAO zaldcenzpMYY)SC{@buk*Q2+7&?_0L)S=oDJWUoUig(%ruLR2KO&)HH|D%(*R5sIv= zv&qbitjON`jyv~z`+UFO-yd&(xZ_^)`FuPd&*$@T3hpEBXwK(G4;XBz4D-7DKXYpd z4ZYUQZ^hzX;J}UtFo|3v21NX+5sq=+BB_+QgC$=#rr|;0_LuwiU+p9onqW@le98Vp17*vs)wYE z0jozoEQ5t5oTS&fMCoQ+oV+~bN0jI3JO74EKAO4YP#Lz)!D9JWT)`8s`@R0^RvD=d zzoh5X3BRznDC$g)9C|fl$&XTc!-B!kaqjw-8A5zj3I_J+LRKqF$Rx|RR-Zsd*Y{2r zBa%C^3gkyNTL*tthCK^Bf+Jwh*U?w22$@YncAd!N1w1d!9Zuiu7e@#e$BC9Y_=58f zI=*{In)9`pt+hMejj4V4ZE=|(518-F(F84@8FYDbw}p|r%>At!j~&QACbBxef4%+g zk@7-MixYr7?+tC3&5Di^kER$_XWwRyQ>MxHs~?z6fkio;4f}L|bl}z8>%IXyKLSAk zy9@CJmk{-`^&G~^4ND_c(lfIuM^6?E!0jFzy~;@I6L6Qd8ot5K4qVt&4$J-M{%R&A zGDd=(zz8*yN-b;H-P`sAhYkg51hE`h)QJ9n`)pFp<&nND4FjZ~VWA;6KB3ruIx~27 z9P6DHv!l@F@bkPm>cQ4JS-%rEQO3dS6E~XB{HqdMB zOfHGHx#%k|%MhIZut1w0_^6+EN`Y;u$|tm~dD2K1=+&!M?F3o=cy^BV)6=2r&LzT_ zS284Ux&4k0`CAyJhgZbX7q<_$);nV(UcTcIWl`FfvhrhlYT=jg6@LG9G1Nk&w=S5~ z?WTb!qmXIXr!TLiuGYok`(i^fYZ%q629+yj6ded{ZU0xEz*m%caeVT1fCP=?Iz*#6r2Ml-$IA7( zdqwYTxY~M^xE+bT46w)APfkk5-b| z3E@R4tRckBiao@2AmE5tRx_{defXMr49ziI8srq`7e`HH@)&z(0U2~`Npn$yk(_R0 z|K_@tOk>vjl#Gy_WkSfPOBZW&9Q`04#5Iz?280_ij`R3eDRb{5k7LG5n;hJdUSZ343I5dsEhWcyWGG{~1VaGjxM_*oeX(WJC$3qVQhcfWIqG?6N5mg~@`9jFkOBkLd zVFG!hB4q|DnCU`6Q<8a=BeMc&ZGUCeB5AHqB^-Z4AR)?N8xz}poklYs(->|{q2h4n zJ5$uE>M@#|`xFz(abMp<5U%twi5iQbXxMt^A7lE_Ir@p`8zgDvlU5XIwN^sn-+*6; z9Vh>d8|}|NSWT){ok5qamBw@Mw<-qeg5Te3O=kHz*US-2CHH$=Z|+w+-T7m`$nUq8-ogT`*jja3g6|!+dfZ+Y{c`8@#sc(oy3)7M8{0aO8i%5dx|t?B>{ClyTT| z{XJ-9H+QL0F((AP?>BLTv;WcfX!W^-fhTf+QVybDHoF4YfJTG##i60I!NgR0 zs3l5_%Z7md3hjjki5;Ws;H8m(i$bYSmg~kntrswy%O3;}q_rNYacl^Ns0l4M%XOD8 zu$(v?thsD|CTrUhHh&}kQTa7!IDgM)Omhz=!1bC7WnA0}FYbrCSsjZpU{?H*;cK6{Vo&+9!&MBCp{s?G8n^2G5$;PmK z<`!bljsV8z`h47Xdr{VTQp(N5=Xs~u-O_fv4#1poueWow-lnTnwHn}8y zaxe={r||{TstDJ};4xNxkIa>VNi7g|9-l5$R^j%D#?5p_Ow#e$zTt+4X#na8|_d9yre!Lwb2A@SGKA|$0K{59JWq9J z72|s<5k4#B?p~3@efi>zwdlY2WjnE0TAj7%m%q`OY?|Lf9a>M*6Fqs2-ZV*Fb*W9A z?Y-;?fPx|OQU-l5qoQKHCsm)4N_MIbQS9ehB3g46khH(1xS~^cu-6D|)S%RQdhRIk zOfVy@_3I&`)ceUtX0>EbBe1#ZH`^#R%?5M_pW{Ua^`%qWsWW9;Kx?j4DJjAAbS!3= z)X~$_dQpmW;oR;a3^K(Xp-^HttnRRo%Zw*#P;nP z3tD7f()IJPturP+?`7od``jaR7ya zzPa5}U78tPxm)UdMHPJe4%UrF`XhqTds(kVX%JLKc>0L*9PgZC^N70fGihXB7pQ_`GY{oMV9Bfd|f4>^g_Ko|Aw`>`4jNtqpnC>(no^MI` zI^WeM34M{~=rc1)Zqz*oXWqi0i0<@f3pbh09ccYE;0q=VWQo=hmCCk3yNJrPWn*Lizpv?ZWWi3)t`0TA7 zg=gO_y6Pm?QGC-o`md_P1>=1?O8qfOcavz^%A^EqADzt$@}=IKKcl9StAR9&Ez2Qx(aCkd&!12y;!g$vRSd#mY>$C9sW&URjzAK3LD`#3>Kh9ujVV3LS+ zFu&}Z{%iKzAzRISEcG*|Z6PfZ=y1R8CCSH((208?VkS}~>-8vJp8Ez6n$3e#Z(&Mk ziUQ%~L$2ml-!c~HJox($sy2K??s?W}(?c9OJ9hm$*DSS0H4v4+UPAqdoCpL}0-S3{ zl*5?Rj(CQji_jW`Bdeoxc5R{TbT?tS_rk#{>zXBrH1ouXJMNM zEj5L+u-@N2>JI8^G-e+O!h(6Fq*PRM_|>=2$UBfr!yYL|f9#&3#zYo`%v~9u{(;}bo*O)w*LI^% zUo0U*y7ZlvsS**bX`N!xp&A)3#Ams}S5*wTt4`EZC_UXJKd+dLLg`x-3~T;77ZbPk zhu6!ku*1=-S4B<+ro*jF~@<6#HY+uL)X({dJB%O$&*q#tcR1KJPDr!V#cP~wE~5ZvX= zYyvR;onYW-yAF%r@!dwzJ?O}c@f3*w0~J1wX#&Qp|G_y@nNKbmxCoCiGvb0Z?wek= zlsb8}Acky$q1!bIZLt+wtJOA$p)cQ?&TQOXh4~k~wLP2KF#l#9dbhv#QX!nZrYEtZ z`1b1J0KOZFU^UYyzhS{P$BDc9N0Wk}OriO+Pf6tW`&fBu5;1h!AWS3uTSbnHPgLJW z266?ADB%!1^vydA9~odf-4)&jcW7vVeM@d^_$=xk4#$a}>kJV&f$&VEm?ictVta!6 zP9SQL-Di|QKOtzLr@7RN5?YEkX~}t;3gjVrx;2Zo^NR2x$A=F-T%#X6PFaQ&S_|4T zN*rJIrt+vKsP=Zg;d9Uwh6HN^*NH&PY9-gCo*sVQe$(R0o|n zC1oZIfytbBFkrg!Lp^hC*cM^TsZyYhU=ppi_X-#W{`C_+w`9~aajSn6x3{hgt&#To zF>Qwt75s;b=cpV46jT!o!iIHv%ht$wj%jw<5$4ASRsWsUcPIDm83|4SS4ko3&dZY+ zj5A0OzM&V$I5r*ciMxJ(y>*{`N*Y-}>Y6d|TUeZBS%jaX>D$(}JOaxIFfMq%gJ106 z`*&d{6$p-haPp9BeVZMf^{PcKM0%D26i~~(4=axpxY#U!nAHH)4TXjti*&O5TsJEF z3jxkgg5H(PsPiWQ^MdNdHmZyw`gX$cR)Ct8lJ_)25dSlHz=UTm1^_z-y@i`FN||G` z`_qWTiuoA)Lt4u#`xtK68@%Dz%i%k!_3v}bFxT{8(7Jk<8`J;?;?@K>io#8ycF^hM{!9s_5(cK!pMhvv8EKLypjvt^{ z8TD36${+dpBkD(RRRK_x3^+P+(f@kjZD6XsgCKIn!3W zYEsC3%5}ErP6sM|Xv9q#%i=w|Q!S^@sEUA~y>>m&(Y=xiPeuWaRKU-= z6qxm*97(yg+a_Ge2E>e_wG;V&s%*t7vK2WhE1(24f1u(^H_ckf)Qy6S>$~`Fz(e*Y z;^j}$awpoBuntM1<_QJeS>E1z%Ykh&%skZl12%UA1usPaG8fCp4vMK0S@wH^5kw_K z4!{#kF074};L&{w-IjA271Zs2D3Vu8nFXgGE{Z^JPYrS9mCNwh> zZscB4nw{ha!iXL+Uaz7G3+elU68BBtaGRp_n~GEud*`P3avrGHMgw$kuk}!>S0Zeh zg1u@M5SS=gjtFH{ai_4Pt($t2RnSv}(ki(TD`f7|#<*1Sc+oeZ=J(5VDW48MzZ8`x z`{qTiWZYvDvB`<~sT~TxCc28vJOfV-(>WxJ(<+qRi50&h*S?sqQB7YHRj>>aDA$?U zf1SQ%@FUV1%RI*EegUJ;IurooV5Zgjo*mWrQhoOhJbsf!zu-DFN-N|4YrH+X?;1_I zeHv6hq%q5YTEI*Fl~j9;jLg;q58MycyEQX3O^>gjlQpy0cyn4zzIkGLNYJWdyMM15KY^lP8*BheKC6%W1lf3obM} zX?2BaD{FNY}xRrN%ES;Ma{V3;VCtUuOmlJkUPa1R*(H0n$uqgu_qlsV>K zYZzE=&38)3+?^JHJUdzB3Vlm2UUWARJffDX0w>0*-mcl8*9_2;Q$&U z%)}W|AyhV*=ZPr0h|BTr3;X(~nooH>O*I~d(xR}9<6J*)s~IG!wa{?w?54fTt_ z^dE`-ioQvFi5Csu0wcx0L+)0U_ZaVXZ3r9uw0pjGVQ1TAi8IybHlO%1-R8ubrAvii z0U+Y;eLFHo)C{HhBBv(_V+{qscs7#dJiyHwmO7m%O0p$36NKzd2L}2)YPn_&kbsGL z69(5v4Q#FN_PGdA1RTL>m*?-?h};eEKbr)@lrTKTf29eQx5VXnLzuTCBjRVT2P^Y^ zN`+BT^jaX}LJDK&*AJMhJ0LCQ0LId9CEHBK!rU~_L9&`W)$sk^)J0}!&`@G5Ua~jD zL_I`fBBp_4k#Js~1XBMW)@xhmEx{}o?z3j?PLCsGC9nDql-B1o_6v~x)p$yDJjkmw zNL`-z*2=9rwCY|wx2UrwO{pX5BzuZf&{S@!xXyrGh80G@%P73})q7V`D%B0?KO{re zaIIKAOi++8AT3l=C|Us{3LQj)=ZaD(JcGoE0j)h8+Z%G%f;)t|Fx5< z9}y&8g3W}_48T63BZ7XyhVPfs&Kc4dHa}A$017#X65*M1fwoun*)=gCkA1VB`pC}7 z-O&^xjrDnu3+Zdv@C(h6>JS_$$24-!5xVq)=F(z_s{PGPXiVN~8na&m+jmvnZYLS> zoY)2VtAY`};ay0Q#oWMa)LBODhuO#4iEfi+-1&`JM~b_oZLovrr}{Inm-C+TRxQ}W z(oY;tAYH$8d-+j_h@-70vYdam!OZo~KS5$icU=&=z!2gSKQJu}OuUFl$)vx(YdpM~r*eT1DbV>tR8 z?h-1_OK8t!Y2fz14D?c_6`%GiGI{gufW1~P-BU`~1?4KH41sVTj5@3A5fjK3mOIGd z{lW%3e@)3o3 zf7qvsbwPk#R(ksENQQHc@IMNZlGS3t*i-ubZ9evGr*N$xM!@-^<_t~!U7;spj*qX< zcO6v9y6DLy5bgPdpGL8hwz+14o5{xi?Q^PB*biA_jZz9Bg$$nk>A z;Wj@gR#$7zru~^LLSly-HzP0tS7EHgdChMjF8l5Lb=Kj(#)8|%Uxb0>U?+XDUl}=_ zpTU*q%pK;gfQ3(y_Km;n&z^4@zSa|LkWKyT0hY06gdF-8?qKvczGQ-;G-7W@Wu+h3 z<9$Ybi1&lq*7rY-(B-^f4xn}n{Z3l4D;X>xcR49dA*KH{_H|@wG@~%}wPPWBO~14M zgDMovAc1xEOdweOoJVRP2GSfz`FWCkub=t!0L1K`j>9XOE*@na zfdt5jHE{K$%vRbCcAy5S?e#s6#zW95kbN<2P9n!PBgjQ;~MiaAx0$6eNJhvi20Z4Sg?xai+ zi$;^}JKC;ot#<9agGpnSP3wRs#m*)JQmwZZ`iK39;Zv9H--*q2;H!7lJtSr_%k|mS z!?znz`ro(GK@#iy-5s5wJ%MTB#^=6dk$@n0Li`Q34vtnhP|P=xabA#8WP zVrg1K?>qVS#^$KlPm40QHz zHlm=AC2}XwWCqi7`M7L2SX+WoQ1oDUYC7Oi_07cZPN)^dUTXP| zU;eEPU-mY})f*%=H{1Hw@U@Rh$JGzehHUTCQi~8T^AqlH3Z!OrQm&l#*o2-Zoz8QH z4t=RUI~MpZP8bm^iI6`yRMeLr3^@$LPnOwPMn1oiDb8+n1)AREN6D(DbnQEJ$nN51 z*o1#38s4Tpj&xt4k_*{ogb`Q#JtQFI5$lhRWQ#@ht>dp^*d~!6#=N)lYuYbyyWs^6-PG3)0v%?O1q3B zaT@@t$@ovpb#F#uQNS5UNr`>1dpgHEY*^AdaK7WXy%#X9YQBF1Z{?AcLQfxpX^(sL zH}_$&N95yOpDKHPu?FQFT!u@mPds9Q)<8Mt ze9;&UR7LD>hP6Dl{Q3o7+aYXyMIvUm7AQ)%K?@D56CLz=@Jg;JWdUh&2^}&6T$E=m zTo-dZ1J$ARb*iC$aEzQ9e(w{WQe)yoZL5Y@eV;FA3)Dm6m4LP+?*_8~MZ9ckR0ftt zRb>B!a=|VUF(uIfY9krh5DjndCwIe{-u2|9!$fYL;u(0FvhDQ7qJ#8xvqC8_R_q{z zzk7dtI|5bOuv!=&7F^z=X$teWz-LAAmTsqQp^U>81D?DKTr2K3Z`^!I7W8zhXS0@Y zfnrYsfa$V34bwwZ(&?~i<30B$K#7QSs#-u74;+l>+QL+4nT28J_c?y< z;(1aqmqx-Xs5RIzc%hltwNU+K*%z8(`=`jn?P-pibI$k;J~cyhwba#h58c;yo&hLr z`$#ErE@4|4`1ZD5U_PdQ5gg6=5p9>rEu!P0q|!w0HyzH1@?|8>T-BpoiqnDE|9fe* zQ&vr+ilY?{{$V$|opg-Z8u+8SN}lMn+5pzlfn}SMbUZ6M`!vkbuOK*tDUr5ezIdJk z1t`wG#D6}O3bA9^U0%R?H2B{B)eBruyUYQc9ah=pQqlM7b*q7WH-B|mSEqRYM~yy- z?giglH)-i6wxHQCG>qfE`VsM~+P`_83U>G-50!fY22Z2PJ|xspI)6h{V_yZYxF(RH z_7>XSC47;oq5RZ+t71Qwr)U_~Qee+h#-TJ5nyT<-b&x!O3@NfUf3XKM==(N>@-%j~ z44O0app{5g(VSiM4+Gj{Cnf(xLzt7K6!*P@3ZQcK>ltRzykMMn(v; zoOB7Df&6=H5yB)n)TyIjAh+DXi^)`@oD7U>uYu~P@`g`jXqmZuCuj4sF*hPpK(U?+ zv*Hm%r~cdRz=4DgNyF^v-8-wl#BMByYOqus%TzL84AnWGMK(d7gV3gBLpW;bOf7^6 zT0>?Q#BP{?m*~NAJEYl)btar!GPuy~m@TJFd>gfR3KowS$=i+6y7mbk!oYT0%`f&( zM0Q++E#>0=p2tQ7Y{+;}C;A2R@ador{0%G=C5^m(V1lZhD%NZ#L>T(RKvq2YwT8;0 zL5vahjw*FL>Q9nP|Y~LYP2wi`VU+vsi{GVE}=;6 zPm^$9FJHuCA&x3~ewi>#cXvqnT=6>NQ1;*rC zRakS8WDX;OH%dpdMJz(|eNrO5E9+G!)h1-W+IIbN*45LrM6BAlyjg#5=5cSDz%g-S zpoc0vXx{w5IE;leD?TfWJV)qg47MjEtiA;O?It`nY3NZ3RzOu4m@NQ74?n73wacW_ zY-~uc<9Q)J`6FL-=$Ci4O)}lm?z>R8GZiEqy8okom}TDF7dw5Uwo7fjXv4SV)t~kF zk{kaRUYYjs;nK9f`3{PUA6qYgKftRit6zj4IP)X|*HBky#lBR=Sfx(fUVKTsz=y-L zhfQvG{E}w&b(-e(y}GHf&c-AieBvaI9(tb5gTvJOXiBy?3fU9It<2?f8qQ#tE^Srn zeR)EEKMeSHp~cAN-LyYI2*ApQZNz#)Ca=t?Vu7>Jg>I#ZQrQv%P_OQ)It;AD-<8r z$E+7u+j*^?$vubEZ<6h#C&yRf^aF_mK8D@7#XdjF0SrGYVt0Et4FCxMDNk!aBfoLM{?F^-fHjSvb&on}QY1^^WF4)+=4LD}ug2XJLGCS6;9d!4c18^%yWM`M z78g;p{cj*bC2*UN z;!-U5`CArHPhYpMl5+)4a2`Q^O&OX9VTeZ?$72H)zOahXfL?b$tJ$=fkGHi;n#e2n zVzpN_I3YroWNKQ4hZlCnTG!f1j_)FXot+Ofi?o(a|E-3@J!y@?s zV!&wn@{xkitpm?asCnff`_eLpmJQmlPu{W**2QHO0}d(Tnug204o-b`yRNaw1&cBD znnwGQT)V-FsLTwz*05PF>`7Q$&E;N(J(^xap^`5o!3%&e%6djV>RqMh9A z%ILH`wG|hlW7uenXJZ178ZVd)lNF%=cS$0ZJ!vHFn0jr9MR9~_I#g}KPl@&MQjyXv zw|IweWmh)4$_Ah4C#@0RbT>WB7N?OO61#&7budp3R1U4Z; zWCxEP94MzHo!ucB`ftQ=Avr@w{`OHBxRhzku>7qQphHRSuUL^R@-NCdeuzllW?52Z z2)k+;y23?IIT&CS47F^NUO6i;#C(w+7+0&HI7oM8NHks6i}pW8>w?3+sl^KPLUiH@ zxU(Qs&r=Wj(348_Dz#6Vmj#|vU3sq0`m^Wu;;e&K7~z}qj&Fveh36IAo9@-nOBMS| z9An^6|1i6E)#Bxy;=5bG)qd?X#`Z;CE^S77L-dVRcwN{!Ede_)FN^>6B=-7E<zR zXFylTLpgxm`!b_xlQmFon#l__utqQ3;=&Zrb^UntUqaVGp{yWCA__SJ{ zUdKH}v%cyEOzYDU`_d~Ib}i-RHwc&8G6U`j%c{uPm2VGd;3&(ckKP)4b{ui9HbZ2Y zQw%!6cjeaBVzC3q$;%MND zXvo#^NrNP40oP~P?X|}R^w5pTO8O%c{y_bz;n@!}{t1OGyG!b&3Up|0#XH*hP1C2< zl@0}Crszo*H#t`8jn0D5Oqm~;i(Mlxs<}f*vn`iP#{;r~L+*n@geJ)^KFTqs?AI40 z!@nS`;ncHEThBYx#+Gm-_r*EkBbA~}Dbf0_7kAf0|89raJC}YKPQY;dcAVPu>jvLeK>Cq1}vf92apW_Vw>N_A~ z^Uy!ZQC#L1ZH`TdP09SnCqIw2l=Ig`CB?}0M!wgJ?*sM0vsGJ|pg>NhjugZz>@sy= zK6uYODKE`5Niq()c>pt`gNd#qC%ZnE8DE4Px+~`?2=#2D2qX@6^rslkvh&YFUQzE~ zb;H^s$p?iscJpJ12ST~rn4uR^jc5GrKd|v)aCrA!zmhRUlst!p#YYBime}dj;?P`D zRWCpvG4`kU+R8IFtgyza`@BA~(Hy(rhHMgFU-&3dN$T%~?c5K7&m|YzZX7P`=YPLS z#ASaWM_0soX_ozc95=iVfUh!P{20l<4kyrc<<;~bEd-WlR|6jw7Snvs|6B(e1Zwtn zO((p5$9K)~m%@QsRENdcJ#p@{F*e`T>qDEq7tUg|boDY?QVJ;4SP+|MV(*`TQWupF zh?b^H-HY`+}j(z&7!FmCD8+KUf2T`Je=1Ryz6I?(s2^ z+K-Olg!oRYp0E@m&r6JAKguRWIAin%sh!U4G+YY5Z-_jRKiZONjvvCCzVr7+wPq*v zf*L1Qt??B%yVEZL2PZm!rlp&#?a6)}75A*vrh!v}xXx0lgdB@Ud)2@T?J`3h@MXS= zf)-P186UGQ6IQ;GqFjcDGf2^#grfH*lj~@=Q+wb;x%Dfwcn#l zkouz$!m>L4tdS>&E$_*S|6C#?LBOVtkDU_N=AS05(Vn~h-F<*EMYr+~Gc^*pR{;+J zvI@-}_ieKAd9)i4aR7agC@Z?%yWk*;;^g~?(^1eL7gdH!3?=4QPhSetu2YY3zK$k2 zYo2hM)9^^jb4cQnA)ZQf*qgw$8~Sg7T6v3}=Qi&W6gGxGTR;UupRfRE$@cE^NnWeC zILDpBRp9sJH##?Vc{|u*V@N)4iG3x9klang#dS5BWDh2*m(mBua=ja%uKK=k0ud2a z&UwnPK`zamv56w8b(U9q1&VX%>={fBSy;_U;P!mHMq|7?PS#V*(Utm zDb4$7rX+N!C6pcXh~IfUQ~;hP;ElR{omXkyT=h-p<=k$H$Hki9?Y`1<3A?UgR44W@ zdQBRzNQFF7{qkrOnr!Ko3p%o9hOcB zr=qf*PQ0XdzkVClvokPVV-ZzPwF-a@6Mk94X*fTT13EOi_k!*i<@CGp+zNC@CjhMf z3_6c43vT>o{Ypt7)FvRevedU;!d2qE{|;w$_8ogoMcdL6bJc%dvHQ; zbv)h8J8Kk=Cu~||#p|zwOL75Rnaswy3pFeNTIyAwLk?b6MEe49V| zMkq32>{l(x`b>N^RM2DPy_#2<&ax}z8He#z&{jB}#xv@t35;8XlpBn#d|FJ%F1$-% z9yoMX%Ta{?Jej5sg@iA@UtHm31YWrhnRS6}0nE3!rK(NF6}zsP&wptBeHtAATQ@T_&x8ERg1*-T(harkX~pT~t(Jfk+7X+a0CTUYo z8zdBOx+yj_o{zj>iFCuVa;eOer1bsHCeHj*_GNc1XgxO_5H0>1xGk+FNSRT!)gISP znJOzv{Q&{I73n9I|Dyo+`KFJLq=METKTQvyan(s2hP<`bc4aH=QrXJpY(c&fjmZbCtbDg6*>k%ka(IUA-h6f)z#p9Ij)?Wdr3+RuB-;y<2e~n4e|KpBK zrST``&C&DxJ_;~wFJeyZ4akOfaRO-a?bDZVfSY8T&F%Ym{O*n^i~-ht_WmrLaIG*D zQw`URzf@*K(AP7lx|N4%^2g4NJ+e%BE-Hde5$06<)Gv9KBprBH~CAGb* zF!<2D4P{Xts>)p2e4C4r|Bl=|!#J_GQfUH5Q`yrBclPcBgknM}Zw*xp!r z@_vpC#pvd`o57)NXQ?ah^i09D;L$c3AEW|)m;X6q{k<@>aBj6k z8I9JLg>xs+)2KaD+tPlRXFg_j_%NLTC?hW1h#582c~w8?&O5t1y{?1zWqF+~Y5|fRIu}UHfV9dmned$%oW>pRJF7Dy>kJ zgK<0}CIZL^8tQ8S9U%{!o|&`j3qMRuJn|?Uk>TUn**A%iUrlQ0U_7_H2E$FIxh&6q zIg}3!oRaQz+_>}*fr-v) z=tOeAS}sct;Z1u|U~(&WBhDvA#Y8B4(Hj=1t|0bCU6?Tkn^PyiLq&&r18&b$6}JZrx=C<0m{USt>GYOh1PI#C<%1#7i9jV zDm>f=Lpuk82xH)J%6T+;jY!TmQCr#zo2!7%SeA)GE-44Z=Ix4sA(_wYdl_nbZ>Lng z3p*o(S>1d3tf-TWWkD($MlFe+z9gZQ*JR~@8q3T$TX^#MRAxQ!)+{(TMxP>@haO*A zY1t%YV8g3qsdp54@0z0ECodfz-(?;#j!ih0W}tG-Jk93*ow zQwZ#1tkKlBBsZ2LKe6i2B_(l8P$|NqDa7^4VRj9 zY3e(KcV6uRR!`BT*R=bDh3-Fzj~KZWUeuRVh;RG!^-@P(XYLY{a*0``GGj*q--Jrw zJ^-Fj-z7?=iWy^vN7%Ivg{5L+h5_<+a&JJ|-KogCypp!Z>-i`Csd@LjiW&u3#_495 z)*+S2z!p5mqzWGv6(G z@70d~F4DD#YS+j5yt^qwv#Eq{ctgV({qtM=^EiXp4QyEbYVV0B8$rOBJC&cjG#0kV z_Wnxy&xbwMWWAt0`Q#}{eIy!Y;Vh08hJ0g1qot_m&e9KbX|yZMnla3GTQ!g7N+x7o z=|-_5qOj7^TocIINfb9TdO6_<1oH*h^|@}^{G0CC_vTuL4P5R!JICN=%WM7?s|hY; zguPC4zvB5|6%Dp$EmZZ*Df%_cYV?poM^1IQLMp6sMaR45sW@|!ow{<{tI)43G|Y<) zeWWpUzgfdiZJ$D2-IT&(=qIds#dF$Sb^MA2*9_bC=CT%cw00p0PZh5Ca;{Z9abN{T zF9Dj9JGrUn_d@#?XjvUu+1BFB&&G<)72Pj@E~{^z18uF#JUnat=tXWM0-Cid<=H`C zBl_e!WC=iqP86q|A?jtT*Jrr_H{^mNG<+2HHAvbBjs!Am6JsJht_^wFLW0?bjzAcL z_O=M5yC}`9n_?%fdY`=L%owJsuITgP4;=Xps#-)5hHMl>Y5xjvy5Mn6yo@v){K)LaSxr<-$ccJZRdU>|3iSY#Xq4cUx~ed}0`m~LoV~ZsH>YlxYI&?*&aQh>K%RZrzI>- zM^@6oE6AalnhXKixo2e@`Ql@e1=ZWXuG0@Ds6A4gASQhM>(R%ek@*qpY6i(9EQ? z;(39gDW^BSi1A{w71upF`vx=YM!kO&NNO0_n!2&eTxm73cD7j|1rfk;=_F?&H`Xj-iPe0@50iQq8#1V?;7z+xIJf_W_NxP+XJzAAehS?#Sx8N*iv zTW-?m=AYXaY678I1%P8mr&0-cR_JUdIty&xa|;1%pFz$|DbycyzxBrtzkNtf!0)Yo zg;o_F;*UG?I{yxS3()ULg4~GHj<*I3+d(e2@ty}NJIR`gbHyPiD-^Il>_Gr%;cUX# zSCEzDy)@tm+SS(dF!6K5;+Tqz`{qe_gX(gKj3h#FgqEtiIy9 z)>f>j+FNzka6{Yu^K0Vz^osO|-nMj~$NUNY(dMf%5=CDhB8p)_Him*Ge++2XfVr)! z^P#~paf&;+Qrx!ZfVh0B%8yM#i(AfQKc2k{ZO~u1ulGi9%4G_GxhtG$7#n^ z|F&*+lxuR^(IjW@+GW-!qOawRi(&gRPVS%SrDGz-32zzZuEOTs!%xP=xU$~>vz-Iv z`)xVd8XB1vaKg2boqw$KGKuYpsB1KVb`lr73T}0o>=?@=DADB@yzwPZ&r%%VL9Eda z4GC>u_Y{rt+5cQJdshV$2hZ?ZKLsPRh?2u}b3lz!$@qt!y>Q&RuJtNh&Dz1q=Vasy z46yPwlb&tx8Ms2zO<-8kIFjSjo_q#k_s284l?3u81szC`7l&U?S%%=vrFEI~2}=NA z&$^|fb-z&(mZv)5tGbuvw)QKd#q2Kw<<8Pm5QwE+W!_ z!!OO}jQOZkLiwjX6= z`Q<+mkrSp723E)Oq&>D{c8Iyk-jPEG)4pIi^bMbzy~@~^mCfbgpxxgLkiyVxUuwT~ z%|07X`5iV!o{Y10;S5#qawzRL}vQWfPXRds7%0^f2yG zYxIYrLW)D+NRJXYc@WlL7=zm%jba84+78BBsD6JBpv@5X+fALC4cRrnil!KiY0@R* zqrCt4$-~A9idYb+6n(-bN2=<8L-iue|3m1}Cw_0Duyuj?^!7$uEZFH{!RX_9R89ew zPAOaX+(0gN##FX;LMTVsXxhf$#}0O5X+Jeq7>SUVPwA31|>H$!ivuBwk;Z$!th@ zo0tyR+>zaCOmoO!AMQ+X65 zyFZHFjy;++IjQ{ayf9m0J8OnwesGhfvvKcZ;b$ejT5WjzJiN(T!IDwS@Qs?hei9`; zEV494l?c>}mXV=kr2Y2@q6nO*Uw1g>3&jtGtZU3yyU$9qAsA`i~bEB7kj2J_xa7(yd zpZr{;Fw9B=Nq6v$E~NM)xd%dtpuVA;TdeU16ibs-^8Ui*LtD~&^Z#{p=HXCwZybMS z491qR3n5D?q+*H;hREL7A~Nw>dTlNCosn1BmuzJlAk#1g7+djGrd$u17^<`1|jv`wS)m>VdFMI&1s zJY0kc<6FmKo(5@SOR(M7>AH^oLsO{>TE3HFFc8AgGZkyCjCNr$7O`BB6yMHyoT11k z!tpnG+?>~vS%DkDVtr5jC_e4;7}QiR9=X)2@5)z{)i zb#R@1Q&s{|RwtRD1Nd-~C(YFXec)%P4)>q(Hv7M)DE8jZKYZ2ph# zm&QMu<(sE=wfxs#6(xaY4E3MU`m~l=pK3>|Gg&|Q7opB{6LWNkvW-F(v%VxV8>Bqb z1A@-9bpTe|S#2o8h_1Qx5fm0L1E0=XYJZFy8M}UvE6Oq@oNnHOuybLvO`{7w z)yKFs;v$TF%y;$@2_9a~XKZ!7Qzofz!9+X2Mc7wcMRJGr|Bm6?B*uF`< zasQo#2b8qTchvmvMA41`k)(F@N1=sCtGC$Y*|!gVvmf_3=1UG!58QDvR@%ZgYe160 zSh9j}s41am`<&)EulNZ9>M;z@{r4ga6tKW6C)KE24Ju2kXRcSmkpyYbpWfSwE)t}0 zD_OgN@T`8x7Y$dG)Vbp1UO=CqE=h3aD)ba+!i&$szNcq~9Up$AMY6Gt`ZEU!e>tGF ziv70o+KX3fy>nM~ZZJ7_Y2oi@UZwl#aBrtglYY9y4ExI=dCel2(VcnD*W>7F8h(Hxh@TWgqF+eBF(og$0SCpz5okPwTHW6O6?>tH-{N9tHEPSG@SJ0M#@h zNVv^6&8qY;Lu-FIc1nLdq@`b_ymno#yd?mMjX())Z0H9HldKI#O|?{lY!ek!TCTaw z-%T(GC2n1U=Z1u|RuUnZF}kk3P`JCr6O{uwONBP6H8NVLF@WiWvdQv7dmABBZ<%S? z(0b^FXRtAI=}P4_6-fd_CIW)zZgVn%QtH#cgIYMX6wQi5#N7@$_b&susgMnz&DBhs z8oy+(Co7qDcMHVr78|Wk+r+h1hL&jK5q}OA^!ZjWmZ9%A$R|Y4?hcY0@Vh!(;H%h*KM19zsVp z2VEj9q@EQ&r#HP^Pt|3&gftO=Si-uRL^z899ZwnZ04ZV8MfrrZ1=+S;W7#OPPi{?_K+IlotFMt~mMaznYS9PKxG$j@aL^!nDP{ zk(0{m33He0%o&>z$EaResc3<`x}`NJ=v)w)u?-MI@lI(cYv6{4c@OlyfCxSw{n>>z z{N6W1N}61l2=$N<{pfPA@UB;L(2Cd@;_w`~K6N#JY|2}?qJtl5b8JXktf{Z)@wHyv zj=Foe+SJ0lC4o4KUFR1)&pCB;NE!3yfJE=KUcj?CP|Pko(TzG(fnNyLrnDUPzcSJN z45L0!ZCTWbR1CUApf6BpR|K%kpQT+v#_*=kPJ``&hLFITQ+Dsa=i-1q z$+OgIN)UncV%Cyi(4zB4w>zH2Cjz!?sr;2|> zMZ<`scfs0B%;uLmRxQY%hPcqt!EE5p%n6_2)ePvDV9*VFmve%IMsPjS>xD1ZC0w)D z)i?Ya3u%r7gTV2Ib_U1_&!{T*=F@hyBuTeER1G5745sQMeDDOonuknJL-!Z^GK)s? z&M)h6{3ows{vbrSz_Vjbxz3EyggMD5z;c0aKG9uVzQ)Dx}4h&z#oQ! z@1N_dk{+C}l^JI7XGIb-Pa-(Lofd#9zj!M&r$3~Ecfx`0|EoU~i`oI3wz$$P+mUK&dT$ z8Qw*tOQ~YJQ_BiBF-nq&qxT=-;U(cm9yDe#p=wpbu7Mm}(p=eN!@ywpncOB#@$z_;XvezB(GzVgOd;yOYR3oaF6yK{)(ajLSWcxun1{C-Uv!5#r@d}jDsUf-hxrS7 z!3!m^7R8ZgF-Lw$os=TiS*YX7a;!z3~p6 zDYKJxZ*K7b&h6mP`9u>c^&{Az_?dFSTdRUXY`j_A7+})<*3*JCiAY~bbNF!UvjZAv zc5G#(5n^Ij5pteB=>+@x%BH!Ab=qSS$9%GouR6ehS^KBXo4-(RYrnv2 z^ul~G?6d2`{#dE{v%VQ<9dP(R{Yy1-o=&ZZPF~Lp7?_P6f%{p4w(kV1iVioQKafl7 zb?nU0>E~Ce_{RYL>Hr800N#TFomfjON(Z^cy(h($Rn4d=q`l9w9DHA1``7a%s#v={=(zYTz`L zj&SMc{p^W+xueh$PKalAYd$WL_cjy&Zrn8&>L)-0bg& z@i6ndtiY*6T&|k+UkIBo^D?`icOI8DNfa&5KIM@RWnd`Dz@Zb^D;l(s0>9F3+QhHA zYCtOZNYDLj`8R%?gj~nPLni7?86B!*|jj+`4{40P~Dr?_qQbn8UHG4Bu z&9O8U%*K%z0mM8^X?)oT&COth>+KbB4&4?wVaF#<=-Ef-DRE6U)zrQ)|!v7 zs#*P2PC^8!@;I$|x}n!Gez^G6rSn&zr+fWHOW)M??%4GQy1}Si{?>Q@q&B(|DTtqg)m zF3%U1;t4BWbniI3+V~td{*7y_hpptk(%F?EsPwesU)zfF2pB6>$sQB2az1M%=;cKJ z0UZLP3K6bw4;kEIQmHKq;UYG7_-u;CI_z&ew~DCsQ0bZ-yjtrd_k;<{wWgKQRA||0 z=f{FgO{`@96qtEjUIjfP-=I4l->$uLg(E52{0)hlYWfZ#WGUd{*k*UxXq40GarNrb zQbMJVJlTc_NhNpX3J zhTSms%U83vG)?&3_fuUHVWXP*e1xtDUszpm9TylvirY$SIjSuL>c4LGKYyRuz~OQ2s zNW0i1+d57NmOw7=k6MRtq8Oh-wiCq7BXJ{TPXo!uS#+%^Y?>bR4(e}!R9EQLNo8e2 zt?ELLBJciF@TNHRVE7*qZ2Y|1l};0G1;-bX4ce|iPcO(`Q7L~OhMFG6AEBEW>RTT0?C^yyHFG(?H? zgN#6(_F4R?5Hpy0;ODzZzBl>r&oK0K`heW|nj8JuK`dZ7t?@r)$UGZPu<8a}31NF0 zX;;cTDAm$SKd47{pNY%=JAGBXkJMsJ5VChbEhn<@m%kgb3uq^09qI8SCdor)s_cU1 z4p(~!l|4KE^@f1R(9GxM-B^v7_XO1yaRgSNO0N6FMNLf+vW?T+sL^ya3;|JO<42xb zfCAbsU072Wk@dUjjnuJnss6C@s9g|BdHB&xKezT6x1+zK@lpNE=iWQ+!(gY_-YhQo z6@(ehwa{QC-Q!9HipgCTxv(2TZFUr~s)rMe>Gbp`{)vA<98Afnc;PbY{>f_MtQVs2 zQugei4WD4b=0J9B;52xi>QkyYD~ZeB>9<|UjGG~G^r)E!^1LDUckJ|nKDvy|l8zXsrn@5mx7WMRqd@y&!> z&B-y$VPC?P-9kC&i&Bk5MNC|0_w%-G`geida3`>ZY;|McS{?q%h@bW7e7ZP;2E``v z`Wfx`9JuHvcfqywWFLlK=XRx)4S>k+_$WG`pR_NxIg)*}*a;DSTTQY)DRJgo#sm&~ zDGpUg&Tz%aLM|%{Xv#v)#j;eMCj)23Bja%%6U@1rHYRyzNWH3!ULz01!QTQneF*)z z!{*mZ)dt>YnB39Z>DPrj&yH~wdJ7b%6a5bIpbQza+K#NE8~2Gqu4B!|P9IgzR1M55 z^(^fbXd|)fEmEkhZGI?;q~hx!?r)^oqkhTbPtF^dDv&2DRaWVf5`=EE*> zkxqXui5wr*J?u$}k#{6*wv{0^PB!~lW&WIQcwobu5X#{?R*{O_YCgUG;|3J1q$uAT zE^-7T&G_f7$bH={&DLLvv`l0_Hg(&fRdhU)UIpiFNTb4pCXv(9Z*H#-cO*y+l8@)_ zIuE|QWaEa_XCV*<_FJv?Nd>|Fw&&QR)55*ds*?GinJ9fNjVN6gjv;^k^(uhj_q(_u zW-tw4E0QL>lNq%>CVwcMzBq60cf%>=U>DR}I_hhfv>nscKTxF-EG0#VIx)yL{Oe$Q zFA9^MPto}y&$M2rL-Ck$wMk#In{z)-jxsUI``a|8U;~ijP`;oEG7EyV-;EK)u)(K6 z@O8o8sOECD{)LlJs>GEeY_M1TF)}zb3E?!HgtL}EOyK*>i#5tKK*XDb{{9CZ+uBRu zLxSDeH&k`SssaQMLBK+Fvs#K6thJBiMPBA_-`{#KnS{{OJ4L!~_6f}tpHhpq1nHND zCQNn^)_nUB+L|ElR33g(_g_Vw&mCF}$(Khb$mfILyFWsf8miQ$%JR(+hcfPATpe^x z^-j^qYGW(8qITDr3A*Y#C5xC<@7YAnGwlWnM5@}5J@ + + + + + + + eJwTYmBgEBrFwxJnU6CPHL3ZZOrNJlMvur6higEQaSSv + + + + + eJxjYCANfAbiL0D8FUnsOxD/AOKfBPQyMzIwsAAxKyNCjB3I5gBiTkb85ggD5UWAWBRJrziQLQHEkoyo5pAKkM0ZTsCJAn3KQ1SfEB4MUq8ExdjsU8KDyXEfsQAAzu0N1Q== + + + diff --git a/npc/pirate/.actions.py.swp b/npc/pirate/.actions.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..46e0cd7572ffc5767988006b426f5855bd88762d GIT binary patch literal 20480 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 literal 0 HcmV?d00001 diff --git a/npc/pirate/actions.csv b/npc/pirate/actions.csv deleted file mode 100644 index b9b6b94..0000000 --- a/npc/pirate/actions.csv +++ /dev/null @@ -1,15 +0,0 @@ -idle; ; idle -do_dance; ; money = money + 25 -drink_rum; has_rum; is_drunk, charisma = charisma + 10 -buy_rum; money >= 25; has_rum, money = money - 25 -steal_rum; is_evil; has_rum -steal_money; is_evil; money = money + 25 -sell_loot; has_loot; money = money + 25 -get_loot; is_sailing; has_loot -go_sailing; has_boat; is_sailing -beg_money; is_weak; money = money + 25 -buy_boat; money >= 25; has_boat, money = money - 25 -woo_lady; money >= 100, sees_lady, charisma >= 5; has_lady, money = money - 100 -get_laid; has_lady, is_drunk; is_laid -make_rum; has_sugar; has_rum -pilage_lady; is_evil, is_drunk, sees_lady; is_laid diff --git a/npc/pirate/actions.py b/npc/pirate/actions.py index d7ae60b..b71d52a 100644 --- a/npc/pirate/actions.py +++ b/npc/pirate/actions.py @@ -1,5 +1,71 @@ -from pygoap import CallableAction, CalledOnceAction, ACTIONSTATE_FINISHED +""" +This is an example module for programming actions for a pyGOAP agent. +The module must contain a list called "exported_actions". This list should +contain any classes that you would like to add to the planner. + +To make it convienent, I have chosen to add the class to the list after each +declaration, although you may choose another way. +""" + +from pygoap.actions import * +from pygoap.goals import * + + +DEBUG = 0 + +def get_position(thing, bb): + """ + get the position of the caller according to the blackboard + """ + + pos = None + a = [] + + tags = bb.read("position") + tags.reverse() + for tag in tags: + if tag['obj'] == thing: + pos = tag['position'] + a.append(tag) + break + + if pos == None: + raise Exception, "function cannot find position" + + return pos + +exported_actions = [] + +class move(ActionBuilder): + """ + you MUST have a mechanism that depletes a counter when moving, otherwise + the planner will loop lessly moving the agent around to different places. + """ + + def get_actions(self, caller, bb): + """ + return a list of action that this caller is able to move with + """ + + if not SimpleGoal(is_tired=True).test(bb): + pos = caller.environment.can_move_from(caller, dist=30) + return [ self.build_action(caller, p) for p in pos ] + else: + return [] + + def build_action(self, caller, pos): + a = move_action(caller) + a.effects.append(PositionGoal(target=caller, position=pos)) + a.effects.append(SimpleGoal(is_tired=True)) + return a + + + +class move_action(CallableAction): + pass + +exported_actions.append(move) class look(CalledOnceAction): @@ -7,74 +73,181 @@ def start(self): self.caller.environment.look(self.caller) super(look, self).start() -class pickup_object(CalledOnceAction): - def start(self): - print "pickup" +#exported_actions.append(look) -class drink_rum(CallableAction): +class pickup(ActionBuilder): + def get_actions(self, caller, bb): + """ + return list of actions that will pickup an item at caller's position + """ + + caller_pos = get_position(caller, bb) + + a = [] + for tag in bb.read("position"): + if caller_pos == tag['position']: + if not tag['obj'] == caller: + if DEBUG: print "[pickup] add {}".format(tag['obj']) + a.append(self.build_action(caller, tag['obj'])) + + return a + + def build_action(self, caller, item): + a = pickup_action(caller) + a.effects.append(HasItemGoal(caller, item)) + return a + +class pickup_action(CalledOnceAction): + """ + take an object from the environment and place it into your inventory + """ + pass + +exported_actions.append(pickup) + + + +class drink_rum(ActionBuilder): + """ + drink rum that is in caller's inventory + """ + + def make_action(self, caller, tag, bb): + a = drink_rum_action(caller) + a.effects.append(SimpleGoal(is_drunk=True)) + a.effects.append(EvalGoal("charisma = charisma + 10")) + + return a + + def get_actions(self, caller, bb): + """ + return list of actions that will drink rum from player's inv + """ + + a = [] + + for tag in bb.read("position"): + if tag['position'][0] == caller: + if DEBUG: print "[drink rum] 1 {}".format(tag) + if tag['obj'].name=="rum": + if DEBUG: print "[drink rum] 2 {}".format(tag) + a.append(self.make_action(caller, tag, bb)) + + return a + + +class drink_rum_action(CallableAction): def start(self): self.caller.drunkness = 1 super(drink_rum, self).start() - print self.caller, "is drinking some rum" def update(self, time): if self.valid(): - print self.caller, "is still drinking..." self.caller.drunkness += 1 if self.caller.drunkness == 5: self.finish() else: - self.fail() + self.fail() def finish(self): - print self.caller, "says \"give me more #$*@$#@ rum!\"" super(drink_rum, self).finish() -class idle(CallableAction): - def ok_finish(self): - return True +exported_actions.append(drink_rum) + + + +class idle(ActionBuilder): + def get_actions(self, caller, bb): + a = idle_action(caller) + a.effects = [SimpleGoal(is_idle=True)] + return [a] + +class idle_action(CalledOnceAction): + builder = idle + +exported_actions.append(idle) - def finish(self): - self.state = ACTIONSTATE_FINISHED - print self.caller, "finished idling" - #CallableAction.finish(self) - #del self.caller.blackboard.tagDB['idle'] class buy_rum(CalledOnceAction): - pass + def setup(self): + self.prereqs.append(NeverValidGoal()) + +#exported_actions.append(buy_rum) + + class steal_rum(CalledOnceAction): - pass + def setup(self): + self.effects.append(HasItemGoal(name="rum")) + +#exported_actions.append(steal_rum) + + class steal_money(CalledOnceAction): - pass + def setup(self): + self.effects.append(EvalGoal("money = money + 25")) + +#exported_actions.append(steal_money) + + class sell_loot(CalledOnceAction): - pass + def setup(self): + self.prereqs.append(HasItemGoal(name="loot")) + self.effects.append(EvalGoal("money = money + 100")) + +#exported_actions.append(sell_loot) + + class get_loot(CalledOnceAction): - pass + def setup(self): + self.effects.append(HasItemGoal(name="loot")) + +#exported_actions.append(get_loot) + + class go_sailing(CalledOnceAction): pass +#exported_actions.append(go_sailing) + + + class beg_money(CalledOnceAction): - pass + def setup(self): + self.effects.append(EvalGoal("money = money + 5")) + +#exported_actions.append(beg_money) + -class buy_boat(CalledOnceAction): - pass class woo_lady(CalledOnceAction): - pass + def setup(self): + self.prereqs = [ + EvalGoal("money >= 100"), + EvalGoal("charisma >= 5"), + PositionGoal(max_dist=3, name="wench")] + + self.effects = [ + EvalGoal("money = money - 100")] + +#exported_actions.append(woo_lady) + + class get_laid(CalledOnceAction): pass -class pilage_lady(CalledOnceAction): - pass +#exported_actions.append(get_laid) + -class make_rum(CalledOnceAction): - pass class do_dance(CalledOnceAction): - pass + def setup(self): + self.effects.append(EvalGoal("money = money + 25")) + +#exported_actions.append(do_dance) + diff --git a/npc/pirate/actions.pyc b/npc/pirate/actions.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b449ad58e4fae07034e42b2912a5faeaf6511c8 GIT binary patch literal 9124 zcmc&)&vP6{74DgpR?=Fs+PCslC1=zXT_eN+V(ir%TR_lydjEqb3TdmmH5MKwQliIAALl&aln zhN*);FLLyT*Y32m(~g5Kz82%e=_K(|;UBtUyMk&A8hNYUQ)hrsKD~ zG}Wn-HNDK)jJwV{hd}6xcCy&K&1w&R@FXc)vPgVT zw!e;&I;b=V6~;vkgIM%N(yc9x0p-q5QDNz)s6?3r`rGf!-!X{woVNCxUKFNnSOk6$ zXt-$xukhe!-H2FSZvxYpA8NSd2pHGHR2$3moWofUTP>#*#~r7o)3mjjKO%+}S9ZJv z62+;4aB)#5a2>jquhYvS&F4y~a+Yb7^aE6xgcWFfTUmfD-~|+?^8#w%5^$EF1N?1U z7Qhb31Fti8Cj{zBEd$vWVZLpvZ`*41Wi^i>^T2Kn(_5HC<|kAWopqCvTYytzIinmv zAvJuH2(K-G$^co~2HVe?r4&JIm^QL7(Sg`609A!gV5T{PtQwm0Avtk2hU*igcap9F zDb$~3N6@3AwJ?dJHWZbO;TkA!aI(f?61N3N3*E3)FkTPtsctO_vW`F%!+FY)10l zZ6Oc%8VsD@n4hBLIoU(zC0K_`%)8^LjQ+ShQH5Eb__PM7{490Fb?IiebIzcO-4hgW zk^(5(rN_CGtf&~5^7f+__Yn+y1;r@C8nvFZCM~0Ss;AM2ytZyMGD>a0T8wwNB3Oy(LMHf9%62yq{M`>>Xxu& zPg^br^dw1Z4{==8EJ~p=TB{TKE8UKdmVBrT;qFJ#*rBwNzM(xTU3^R|j*DrO^hcuu4HH*ZV4Z_M{&cY5d{h}H?F@kE9`+xv+ZR<3C889&OYZS zHHxpA=^2%U3taMT&+vQLoZie3l9FK5;4!-j7>(m!Rh_mj^mvks6B|B@#)Ej0kxC3p zErTHo^vlk;B(wLNm0boDLwDq_n_l`Rlu5SR!ExfdRP@1kt^uLKI0`%mmg6e=eB%&2 z_6#JzgH!nI@ub2qUSY0Y&7hrqgSmfSV^IC#i zuD#Da33-#g>^DG5;>0srk-0!Z8LKC)x^YNOLnaUv{R9oNXhm)OKv+~VPV9c>?pA(Y zB8AG~NSHIJGm}bYg*g$jpu-+(BC(tVo#G7#IrkG8xytt(OkVp&n%uSIS6Bv)&mifZlqAQc7rUHXlCAywGqaNj#pGEPfN~yZ7>(|akE1vtU1NN3nLt~+^5mya@{6Z;n;)aKi|g_dHyqM`J9oZ zf-=oJq+GMkZ5*55khY};fM#sqWxF9Mn_&)k2r>Vbi~*F9QLVnEqMBUyWptEnb`{bQ zP4>(dSRf2cmiJ{`ng|}^blMbLEqY#@S!Rz-h*kITi85kKBRfR$8b6z1F$H*NxE^>G7klxMjX_QnP9B3#`c+d_o0PG@! zEJ0Hh)sSN!+9rXWlXDLa!kk;u2cc(P6%nC4mPs)w|MLe(G&Oc zn?hKDA>C><@Y*9gl$J^i5gR+S)WZ;O$jdVDbRa2UsXhuY$Py0cLei7y+o{Z}SFV^0 zf%(44y@)ztqaLpyrC^Qoipt%6$Db zoWZZ8JUW-ha`e3d&F zI4~PWwe#nF`9bWO{Q7&HKZ5B*ek2je)A%m|9)6rNx8;Wt^Olr?cvA. """ -__version__ = ".004" - -import csv, sys, os, imp - -try: - import psyco -except ImportError: - pass - -from pygoap import * - - """ lets make a drunk pirate. scenerio: the pirate begins by idling - after 5 seconds of the simulation, he sees a girl + soon....he spyies a woman he should attempt to get drunk and sleep with her... ...any way he knows how. """ -global_actions = {} - -def load_commands(agent, path): - def is_expression(string): - if "=" in string: - return True - else: - return False - - def parse_line(p, e): - prereqs = [] - effects = [] - - if p == "": - prereqs = None - else: - for x in p.split(","): - x = x.strip() - if is_expression(x): - p2 = ExtendedActionPrereq(x) - else: - p2 = BasicActionPrereq(x) +__version__ = ".008" - prereqs.append(p2) +from pygoap.agent import GoapAgent +from pygoap.environment import ObjectBase +from pygoap.tiledenvironment import TiledEnvironment +from pygoap.goals import * +import os, imp, sys - for x in e.split(","): - x = x.strip() - if is_expression(x): - e2 = ExtendedActionEffect(x) - else: - e2 = BasicActionEffect(x) +from pygame.locals import * - effects.append(e2) - return prereqs, effects +stdout = sys.stdout +global_actions = {} - # more hackery +def load_commands(agent, path): mod = imp.load_source("actions", os.path.join(path, "actions.py")) + global_actions = dict([ (c.__name__, c()) for c in mod.exported_actions ]) - csvfile = open(os.path.join(path, "actions.csv")) - sample = csvfile.read(1024) - dialect = csv.Sniffer().sniff(sample) - has_header = csv.Sniffer().has_header(sample) - csvfile.seek(0) - - r = csv.reader(csvfile, delimiter=';') - - for n, p, e in r: - prereqs, effects = parse_line(p, e) - action = SimpleActionNode(n, prereqs, effects) - action.set_action_class(mod.__dict__[n]) - agent.add_action(action) + #for k, v in global_actions.items(): + # print "testing action {}..." + # v.self_test() - global_actions[n] = action + [ agent.add_action(a) for a in global_actions.values() ] def is_female(precept): try: thing = precept.thing except AttributeError: - pass + return False else: if isinstance(thing, Human): return thing.gender == "Female" + class Human(GoapAgent): - def __init__(self, gender): + def __init__(self, gender, name="welp"): super(Human, self).__init__() self.gender = gender - - def handle_precept(self, precept): - if is_female(precept): - self.blackboard.post("sees_lady", True) - - return super(Human, self).handle_precept(precept) + self.name = name def __repr__(self): return "" % self.gender + def run_once(): - pirate = Human("Male") + import pygame - # lets load some pirate commands - load_commands(pirate, os.path.join("npc", "pirate")) + pygame.init() + screen = pygame.display.set_mode((480, 480)) + pygame.display.set_caption('Pirate Island') - pirate.current_action = global_actions["idle"].action_class(pirate, global_actions["idle"]) - pirate.current_action.start() + screen_buf = pygame.Surface((240, 240)) + + # make our little cove + formosa = TiledEnvironment("formosa.tmx") - # the idle goal will always be run when it has nothing better to do - pirate.add_goal(SimpleGoal("idle", value=.1)) + time = 0 + interactive = 1 - # he has high aspirations in life - # NOTE: he needs to be drunk to get laid (see action map: actions.csv) - pirate.add_goal(SimpleGoal("is_drunk")) - pirate.add_goal(SimpleGoal("is_laid")) - #pirate.add_goal(EvalGoal("money >= 50")) + run = True + while run: + stdout.write("=============== STEP {} ===============\n".format(time)) - # make our little cove - formosa = XYEnvironment() + formosa.run(1) + + if time == 1: + pirate = Human("Male", "jack") + load_commands(pirate, os.path.join("npc", "pirate")) + #pirate.add_goal(SimpleGoal(is_idle=True)) + pirate.add_goal(SimpleGoal(is_drunk=True)) + formosa.add_thing(pirate) + + elif time == 3: + rum = ObjectBase("rum") + #pirate.add_goal(HasItemGoal(pirate, rum)) + formosa.add_thing(rum) + + elif time == 5: + formosa.move(rum, pirate.position) + pass + + elif time == 6: + wench = Human("Female", "wench") + formosa.add_thing(wench) + + screen_buf.fill((0,128,255)) + formosa.render(screen_buf) + pygame.transform.scale2x(screen_buf, screen) + pygame.display.flip() + + stdout.write("\nPRESS ANY KEY TO CONTINUE".format(time)) + stdout.flush() + + # wait for a keypress + try: + if interactive: + event = pygame.event.wait() + else: + event = pygame.event.poll() + while event: + if event.type == QUIT: + run = False + break + + if not interactive: break + + if event.type == KEYDOWN: + if event.key == K_ESCAPE: + run = False + break - # add the pirate - formosa.add_thing(pirate) + if event.type == KEYUP: break + + if interactive: + event = pygame.event.wait() + else: + event = pygame.event.poll() - # simulate the pirate idling - formosa.run(15) + except KeyboardInterrupt: + run = False - # add a female - print "=== wench appears" - wench = Human("Female") - formosa.add_thing(wench) + stdout.write("\n\n"); + time += 1 + + if time == 8: run = False - # simulate with the pirate and female - formosa.run(15) if __name__ == "__main__": import cProfile import pstats - cProfile.run('run_once()', "pirate.prof") + + try: + cProfile.run('run_once()', "pirate.prof") + except KeyboardInterrupt: + pass p = pstats.Stats("pirate.prof") p.strip_dirs() diff --git a/pygoap.py b/pygoap.py deleted file mode 100644 index c02ffa1..0000000 --- a/pygoap.py +++ /dev/null @@ -1,1083 +0,0 @@ -""" -Copyright 2010, Leif Theden - -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 3 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, see . -""" - -""" -this is a rewrite of my old goap library in an attempt to make -it less of a mess of classes and try to consolidate everything -into one class and to make it more event-based - -the current plan is to have a subprocess monitoring the -environment and doing all the pygoap stuff in a seperate -process. - -how to handle ai? multiprocessing to get around the GIL in CPython. -why not threads? because we are CPU restricted, not IO. - -memory managers and blackboards should be related. -this will allow for expectations, since a memory can be simulated in the future - -memory: -should be a heap -memory added will be wrapped with a counter -everytime a memory is fetched, the counter will be added -eventually, memories not being used will be removed -and the counters will be reset - -memories should be a tree -if a memory is being added that is similiar to an existing memory, -then the existing memory will be updated, rather than replaced. - -to keep memory usage down, goals and effects are only instanced once. -so, do not do anything crazy, like attempting to change a goal at runtime, -since it will effect avery agent that relies on it. - -to handle idle actions: - an idle action should have a very low cost associated with it - the planner will then always choose this when there is nothing better to do - - -""" - -from heapq import heappop, heappush -from heapq import heappushpop -from collections import deque - -import random -import sys -import traceback - -ACTIONSTATE_NOT_STARTED = 0 -ACTIONSTATE_FINISHED = 1 -ACTIONSTATE_RUNNING = 2 -ACTIONSTATE_PAUSED = 3 -ACTIONSTATE_BAILED = 4 -ACTIONSTATE_FAILED = 5 - - -DEBUG = False - -def dlog(text): - print text - -class Precept(object): - def __init__(self, *arg, **kwargs): - self.__dict__.update(kwargs) - - def __repr__(self): - return "" % self.__dict__ - -def distance((ax, ay), (bx, by)): - "The distance between two (x, y) points." - return math.hypot((ax - bx), (ay - by)) - -def distance2((ax, ay), (bx, by)): - "The square of the distance between two (x, y) points." - return (ax - bx)**2 + (ay - by)**2 - -def clip(vector, lowest, highest): - """Return vector, except if any element is less than the corresponding - value of lowest or more than the corresponding value of highest, clip to - those values. - >>> clip((-1, 10), (0, 0), (9, 9)) - (0, 9) - """ - return type(vector)(map(min, map(max, vector, lowest), highest)) - -class Environment(object): - """Abstract class representing an Environment. 'Real' Environment classes - inherit from this. - The environment keeps a list of .objects and .agents (which is a subset - of .objects). Each agent has a .performance slot, initialized to 0. - """ - - def __init__(self, things=[], agents=[], time=0): - self.time = time - self.things = things - self.agents = agents - - # TODO, if agents are passed, we need to init them, possibly - # by sending the relivant precepts, (time, location, etc) - - self.action_que = [] - - def default_location(self, object): - """ - Default location to place a new object with unspecified location. - """ - raise NotImplementedError - - def run(self, steps=1000): - """ - Run the Environment for given number of time steps. - """ - [ self.update(1) for step in xrange(steps) ] - - def add_thing(self, thing, location=None): - """ - Add an object to the environment, setting its location. Also keep - track of objects that are agents. Shouldn't need to override this. - """ - thing.location = location or self.default_location(thing) - self.things.append(thing) - - # add the agent - if isinstance(thing, GoapAgent): - thing.performance = 0 - thing.environment = self - self.agents.append(thing) - - # should update vision for all interested agents (correctly, that is) - [ self.look(a) for a in self.agents if a != thing ] - - def update(self, time_passed): - """ - * Update our time - * Update actions that may be running - * Update all of our agents - * Add actions to the que - """ - - #for a in self.agents: - # print "agent:", a.blackboard.__dict__ - - new_actions = [] - - # update time - self.time += time_passed - - self.action_que = [ a for a in self.action_que if a != None ] - - # update all the actions that may be running - precepts = [ a.update(time_passed) for a in self.action_que ] - precepts = [ p for p in precepts if p != None ] - - """ - # let agents know that they have finished an action - new_actions.extend([ action.caller.handle_precept( - Precept(sense="self", time=self.time, action=action)) - for action in self.action_que - if action.state == ACTIONSTATE_FINISHED ]) - """ - - - # add the new actions - self.action_que.extend(new_actions) - self.action_que = [ a for a in self.action_que if a != None ] - - # remove actions that are completed - self.action_que = [ a for a in self.action_que - if a.state != ACTIONSTATE_FINISHED ] - - # send precepts of each action to the agents - for p in precepts: - actions = [ a.handle_precept(p) for a in self.agents ] - self.action_que.extend([ a for a in actions if a != None ]) - - # let all the agents know that time has passed - t = Precept(sense="time", time=self.time) - self.action_que.extend([ a.handle_precept(t) for a in self.agents ]) - - # this is a band-aid until there is a fixed way to - # manage actions returned from agents - self.action_que = [ a for a in self.action_que if a != None ] - - # start any actions that are not started - [ action.start() for action in self.action_que if action.state == ACTIONSTATE_NOT_STARTED ] - -class Pathfinding2D(object): - def get_surrounding(self, location): - """ - Return all locations around this one. - """ - - x, y = location - return ((x-1, y-1), (x-1, y), (x-1, y+1), (x, y-1), (x, y+1), - (x+1, y-1), (x+1, y), (x+1, y+1)) - - def calc_h(self, location1, location2): - return distance(location1, location2) - -class XYEnvironment(Environment, Pathfinding2D): - """ - This class is for environments on a 2D plane, with locations - labelled by (x, y) points, either discrete or continuous. Agents - perceive objects within a radius. Each agent in the environment - has a .location slot which should be a location such as (0, 1), - and a .holding slot, which should be a list of objects that are - held - """ - - def __init__(self, width=10, height=10): - super(XYEnvironment, self).__init__() - self.width = width - self.height = height - - def look(self, caller, direction=None, distance=None): - """ - Simulate vision. - - In normal circumstances, all kinds of things would happen here, - like ray traces. For now, assume all objects can see every - other object - """ - a = [ caller.handle_precept( - Precept(sense="sight", thing=t, location=t.location)) - for t in self.things if t != caller ] - - for action in a: - if isinstance(action, list): - self.action_que.extend(action) - else: - self.action_que.append(action) - - def objects_at(self, location): - """ - Return all objects exactly at a given location. - """ - return [ obj for obj in self.things if obj.location == location ] - - def objects_near(self, location, radius): - """ - Return all objects within radius of location. - """ - radius2 = radius * radius - return [ obj for obj in self.things - if distance2(location, obj.location) <= radius2 ] - - def default_location(self, thing): - return (random.randint(0, self.width), random.randint(0, self.height)) - -def get_exception(): - cla, exc, trbk = sys.exc_info() - return traceback.format_exception(cla, exc, trbk) - -class PyEval(object): - """ - A safer way to evaluate strings. - - probably should do some preprocessing to make sure its really safe - NOTE: might modify the dict bassed to it. (not really tested) - """ - - def make_dict(self, bb=None): - safe_dict = {} - - # clear out builtins - safe_dict["__builtins__"] = None - - if bb != None: - # copy the dictionaries - safe_dict.update(bb) - - return safe_dict - - # mostly for prereq's - def do_eval(self, expr, bb): - d = self.make_dict(bb) - - #print "EVAL:", expr - try: - result = eval(expr, d) - return result - except NameError: - # we are missing a value we need to evaluate the expression - return 0 - - # mostly for effect's - def do_exec(self, expr, bb): - d = self.make_dict(bb) - - #print "EXEC:", expr - - try: - # a little less secure - exec expr in d - - #except NameError as detail: - # missing a value needed for the statement - # we make a default value here, but really we should ask the agent - # if it knows that to do with this name, maybe it knows.... - - # get name of missing variable - # name = detail[0].split()[1].strip('\'') - # d[name] = 0 - # exec self.expr in d - - except NameError: - detail = get_exception() - name = detail[3].split('\'')[1] - d[name] = 0 - exec expr in d - - # the bb was modified - for key, value in d.items(): - if key[:2] != "__": - bb[key] = value - - return True - - def measured_eval(self, expr_list, bb, goal_expr): - """ - do a normal exec, but compare the results - of the expr and return a fractional value - that represents how effective the expr is - """ - - # prepare our working dict - d0 = self.make_dict(bb) - - # build our test dict by evaluating each expression - for expr in exec_list: - # exec the expression we are testing - finished = False - while finished == False: - finished = True - try: - exec exec_expr in d0 - except NameError as detail: - finished = False - name = detail[0].split()[1].strip('\'') - d0[name] = 0 - - return self.cmp_dict(self, d0, goal_expr) - - def cmp_bb(self, d, goal_expr): - - # this only works for simple expressions - cmpop = (">", "<", ">=", "<=", "==") - - i = 0 - index = 0 - expr = goal_expr.split() - while index == 0: - try: - index = expr.index(cmpop[i]) - except: - i += 1 - if i > 5: break - - try: - side0 = float(eval(" ".join(expr[:index]), d)) - side1 = float(eval(" ".join(expr[index+1:]), d)) - except NameError: - return float(0) - - cmpop = cmpop[i] - - if (cmpop == ">") or (cmpop == ">="): - if side0 == side1: - v = 1.0 - elif side0 > side1: - v = side0 / side1 - elif side0 < side1: - if side0 == 0: - v = 0 - else: - v = 1 - ((side1 - side0) / side1) - - if v > 1: v = 1.0 - if v < 0: v = 0.0 - - return v - - def cmp_bb2(self, d, goal_expr): - - # this only works for simple expressions - cmpop = (">", "<", ">=", "<=", "==") - - i = 0 - index = 0 - expr = goal_expr.split() - while index == 0: - try: - index = expr.index(cmpop[i]) - except: - i += 1 - if i > 5: break - - try: - side0 = float(eval(" ".join(expr[:index]), d)) - side1 = float(eval(" ".join(expr[index+1:]), d)) - except NameError: - return float(0) - - cmpop = cmpop[i] - - if (cmpop == ">") or (cmpop == ">="): - if side0 == 0: return side1 - v = 1 - ((side1 - side0) / side1) - elif (cmpop == "<") or (cmpop == "<="): - if side1 == 0: return side0 - v = (side0 - side1) / side0 - - return v - - - def __str__(self): - return "" % self.expr - return v0 >= v1 - -def calcG(node): - cost = node.cost - while node.parent != None: - node = node.parent - cost += node.cost - return cost - -class PlanningNode(object): - """ - each node has a copy of a bb (self.bb_delta) in order to simulate a plan. - """ - - def __init__(self, parent, obj, cost, h, bb=None, touch=True): - self.parent = parent - self.obj = obj - self.cost = cost - self.g = calcG(self) - self.h = h - - self.bb_delta = {} - - if parent != None: - self.bb_delta.update(parent.bb_delta) - elif bb != None: - self.bb_delta.update(bb) - - if touch: self.obj.touch(self.bb_delta) - - def __repr__(self): - try: - return "" % \ - (self.obj, self.cost, self.parent.obj, self.bb_delta.tags()) - except AttributeError: - return "" % \ - (self.obj, self.cost, self.bb_delta.tags()) - - -class BasicActionPrereq(object): - """ - Basic - just look for the presence of a tag on the bb. - """ - - def __init__(self, prereq): - self.prereq = prereq - - def valid(self, bb): - """ - Given the bb, can we run this action? - """ - - if (self.prereq == None) or (self.prereq == ""): - return 1.0 - elif self.prereq in bb.keys(): - return 1.0 - else: - return 0.0 - - def __repr__(self): - return "" % self.prereq - -class ExtendedActionPrereq(object): - """ - These classes can use strings that evaluate variables on the blackboard. - """ - - def __init__(self, prereq): - self.prereq = prereq - - def valid(self, bb): - #e = PyEval() - #return e.do_eval(self.prereq, bb) - - e = PyEval() - d = {} - d.update(bb) - return e.cmp_bb(d, self.prereq) - - def __repr__(self): - return "" % self.prereq - -class BasicActionEffect(object): - """ - Basic - Simply post a tag with True as the value. - """ - - def __init__(self, effect): - self.effect = effect - - def touch(self, bb): - bb[self.effect] = True - - def __repr__(self): - return "" % self.effect - -class ExtendedActionEffect(object): - """ - Extended - Use PyEval. - """ - - def __init__(self, effect): - self.effect = effect - - def touch(self, bb): - e = PyEval() - bb = e.do_exec(self.effect, bb) - - def __repr__(self): - return "" % self.effect - - -class GoalBase(object): - """ - Goals: - can be satisfied. - can be valid - """ - - def __init__(self, s=None, r=None, value=1): - self.satisfies = s - self.relevancy = r - self.value = value - - def get_relevancy(self, bb): - """ - Return a float 0-1 on how "relevent" this goal is. - Should be subclassed =) - """ - raise NotImplementedError - - def satisfied(self, bb): - """ - Test whether or not this goal has been satisfied - """ - raise NotImplementedError - - def __repr__(self): - return "" % self.satisfies - -class AlwaysSatisfiedGoal(GoalBase): - """ - goal will never be satisfied. - - use for an idle condition - """ - - def get_relevancy(self, bb): - return self.value - - def satisfied(self, bb): - return 1.0 - -class SimpleGoal(GoalBase): - """ - Uses flags on a blackboard to test goals. - """ - - def get_relevancy(self, bb): - if self.satisfies in bb.tags(): - return 0.0 - else: - return self.value - - def satisfied(self, bb): - try: - bb[self.satisfies] - except KeyError: - return 0.0 - else: - return 1.0 - -class EvalGoal(GoalBase): - """ - This goal will use PyEval objects to return - a fractional value on how satisfied it is. - - These will enable the planner to execute - a plan, even if it is not the best one. - """ - - def __init__(self, expr): - self.expr = expr - self.satisfies = "non" - self.relevancy = 0 - self.value = 1 - - def get_relevancy(self, bb): - return .5 - - def satisfied(self, bb): - e = PyEval() - d = {} - d.update(bb) - return e.cmp_bb(d, self.expr) - -class SimpleActionNode(object): - """ - action: - has a prereuisite - has a effect - has a reference to a class to "do" the action - - this is like a singleton class, to cut down on memory usage - - TODO: - use XML to store the action's data. - names matched as locals inside the bb passed - """ - - def __init__(self, name, p=None, e=None): - self.name = name - self.prereqs = [] - self.effects = [] - - # costs. - self.time_cost = 0 - self.move_cost = 0 - - self.start_func = None - - try: - self.effects.extend(e) - except: - self.effects.append(e) - - try: - self.prereqs.extend(p) - except: - self.prereqs.append(p) - - def set_action_class(self, klass): - self.action_class = klass - - def valid(self, bb): - """ - return a float from 0-1 that describes how valid this action is. - - validity of an action is a measurement of how effective the action - will be if it is completed successfully. - - if any of the prereqs are not partially valid ( >0 ) then will - return 0 - - this value will be used in planning. - - for many actions a simple 0 or 1 will work. for actions which - modify numerical values, it may be useful to return a fractional - value. - """ - - total = [ i.valid(bb) for i in self.prereqs ] - if 0 in total: return 0 - return float(sum(total)) / len(self.prereqs) - - # this is run when the action is succesful - # do something on the blackboard (varies by subclass) - def touch(self, bb): - [ i.touch(bb) for i in self.effects ] - - def __repr__(self): - return "" % self.name - -class CallableAction(object): - """ - callable action class. - - subclass this class to implement the code side of actions. - for the most part, "start" and "update" will be the most - important methods to use - """ - - def __init__(self, caller, validator): - self.caller = caller - self.validator = validator - self.state = ACTIONSTATE_NOT_STARTED - - def touch(self): - """ - mark the parent's blackboard to reflect changes - of a successful execution - """ - self.validator.touch(self.caller.blackboard.tagDB) - - def valid(self, do=False): - """ - make sure the action is able to be started - """ - return self.validator.valid(self.caller.blackboard.tagDB) - - def start(self): - """ - start running the action - """ - self.state = ACTIONSTATE_RUNNING - print self.caller, "is starting to", self.__class__.__name__ - - def update(self, time): - """ - actions which occur over time should implement - this method. - - if the action does not need more that one cycle, then - you should use the calledonce class - """ - - def fail(self, reason=None): - """ - maybe what we planned to do didn't work for whatever reason - """ - self.state = ACTIONSTATE_FAILED - - def bail(self): - """ - stop the action without the ability to complete or continue - """ - self.state = ACTIONSTATE_BAILED - - def finish(self): - """ - the planned action was completed and the result - is correct - """ - if self.state == ACTIONSTATE_RUNNING: - self.state = ACTIONSTATE_FINISHED - self.touch() - print self.caller, "is finshed", self.__class__.__name__ - - def ok_finish(self): - """ - determine if the action can finish now - if cannot finish now, then the action - will bail if it is forced to finish. - """ - - return self.state == ACTIONSTATE_FINISHED - -class CalledOnceAction(CallableAction): - """ - Is finished imediatly when started. - """ - def start(self): - # valid might return a value less than 1 - # this means that some of the prereqs are not - # completely satisfied. - # since we want everything to be completely - # satisfied, we require valid == 1. - if self.valid() == 1: - CallableAction.start(self) - CallableAction.finish(self) - else: - self.fail() - - def update(self, time): - pass - -class PausableAction(CallableAction): - def pause(self): - self.state = ACTIONSTATE_PAUSED - -class Blackboard(object): - """ - Memory device meant to be shared among Agents - used for planning. - data should not contain references to other objects - references will cause the planning phase to use inconsistant data - only store copies of objects - - blackboards that will be shared amongst agents will have to - store the creating agent in the memories metadata - - class attribute access may be emulated and use a blackboard - as the dict, instead of (self.__dict__) - - this will allow game object programming in a way that does not - have to deal with details of the AI subsystem - - planning may introduce it's own variables onto the blackboard - they will always have a "_goap_" prefix - """ - - def __init__(self): - self.memory = [] - self.tagDB = {} - - def tags(self): - return self.tagDB.keys() - - def add(self, precept, tags=[]): - self.memory.append(precept) - for tag in tags: - self.tagDB[tag] = precept - - def post(self, tag, value): - self.tagDB[tag] = value - - def read(self, tag): - return self.tagDB[tag] - - def search(self): - return self.memory[:] - -def time_filter(precept): - if precept.sense == "time": - return None - else: - return precept - -def get_children(node, actions, duplicate_parent=False): - - # do some stuff to determine the children of this node - if duplicate_parent: - skip = [] - else: - skip = [node.obj] - - node0 = node - while node0.parent != None: - node0 = node0.parent - skip.append(node0.obj) - - children = [] - for a in [ i for i in actions if i not in skip]: - score = a.valid((node.bb_delta)) - if score > 0: children.append((score, PlanningNode(node, a, 1, 1))) - - children.sort() - - return children - -class GoapAgent(object): - """ - AI thingy - - every agent should have at least one goal (otherwise, why use it?) - """ - - # this will set this class to listen for this type of precept - interested = [] - - def __init__(self): - self.idle_timeout = 30 - self.blackboard = Blackboard() - - self.current_action = None # what action is being carried out now. - # This must be a real action. - # reccommened to use a idle action if nothin else - - self.goals = [] # all goals this instance will use - self.invalid_goals = [] # goals that cannot be satisfied now - self.filters = [] # filter precepts. see the method of same name. - self.actions = [] # all actions this npc can perform - self.plan = [] - self.current_goal = None - - # handle time precepts intelligently - self.filters.append(time_filter) - - def add_goal(self, goal): - self.goals.append(goal) - - def remove_goal(self, goal): - self.goals.remove(goal) - - def invalidate_goal(self, goal): - self.invalid_goals.append(goal) - - def add_action(self, action): - self.actions.append(action) - - def remove_action(self, action): - self.actions.remove(action) - - def filter_precept(self, precept): - """ - precepts can be put through filters to change them. - this can be used to simulate errors in judgement by the agent. - """ - - for f in self.filters: precept = f(precept) - return precept - - def handle_precept(self, precept): - """ - do something with the precept - the goals will be re-evaulated based on our new precept, if any - - also managed the plan, so this should be called occasionally - # use time precepts - - """ - - # give our filters a chance to change the precept - precept = self.filter_precept(precept) - - # our filters may have caused us to ignore the precept - if precept != None: - self.blackboard.add(precept) - self.invalid_goals = [] - - return self.replan() - - def replan(self): - if self.current_action == None: return None - - # use this oppurtunity to manage our plan - if (self.plan == []) and (self.current_action.ok_finish()): - self.plan = self.get_plan() - - if self.plan != []: - - if self.current_action.state == ACTIONSTATE_FINISHED: - action = self.plan.pop() - self.current_action = action.action_class(self, action) - return self.current_action - - elif self.current_action.state == ACTIONSTATE_FAILED: - self.plan = self.get_plan() - - elif self.current_action.state == ACTIONSTATE_RUNNING: - if self.current_action.__class__ == self.plan[-1].action_class: - self.plan.pop() - else: - if self.current_action.ok_finish(): - self.current_action.finish() - - - - def get_plan(self): - """ - pick a goal and plan if able to - - consolidated to remove function calls.... - """ - - # UPDATE THE GOALS - s = [(goal.get_relevancy(self.blackboard), goal) - for goal in self.goals - if goal not in self.invalid_goals] - - # SORT BY RELEVANCY - s.sort(reverse=True) - goals = [ i[1] for i in s ] - - for goal in goals: - ok, plan = self.search_actions(self.actions, self.current_action.validator, self.blackboard, goal) - if ok == False: - print self, "cannot", goal - self.invalidate_goal(goal) - else: - print self, "wants to", goal - if len(plan) > 1: plan = plan[:-1] - return plan - - return [] - - def search_actions(self, actions, start_action, start_blackboard, goal): - """ - actions must be a list of actions that can be used to satisfy the goal. - start must be an action, blackboard represents current state - goal must be a goal - - differs slightly from normal astar in that: - there are no connections between nodes - the state of the "map" changes as the nodes are traversed - there is no closed list (behaves like a tree search) - - because of the state being changed as the algorithm progresses, - state has be be saved with each node. also, there will be several - copies of the nodes, since they will have different state. - - sometime, i will implement different factors that will adjust the data - given to reflect different situations with the agent. - - for example, it would be nice sometimes to search for a action set that - is very short and the time required is also short. this would be good - for escapes. - - in other situations, if time is not a consideration, then maybe the best - action set would be different. - - the tree thats built here will always have the same shape for each - action map. we just need to priotize it. - """ - - pushback = None # the pushback is used to limit node access in the heap - success = False - - keyNode = PlanningNode(None, start_action, 0, 0, start_blackboard.tagDB, False) - - heap = [(0, keyNode)] - - # the root can return a copy of itself, the others cannot - return_parent = 1 - - while len(heap) != 0: - - # get the best node. if we have a pushback, then push it and pop the best - if pushback == None: - keyNode = heappop(heap)[1] - else: - keyNode = heappushpop(heap, (pushback.g + pushback.h, pushback))[1] - pushback = None - - # if our goal is satisfied, then stop - #if (goal.satisfied(keyNode.bb_delta)) and (return_parent == 0): - if goal.satisfied(keyNode.bb_delta): - success = True - break - - # go through each child and determine the best one - for score, child in get_children(keyNode, actions, return_parent): - if child in heap: - possG = keyNode.g + child.cost - if (possG < child.g): - child.parent = keyNode - child.g = calcG(child) - # TODO: update the h score - else: - # add node to our heap, using pushpack if needed - if pushback == None: - heappush(heap, (child.g + child.h, child)) - else: - heappush(heap, (pushback.g + pushback.h, pushback)) - pushpack = child - - return_parent = 0 - - if success: - #if keyNode.parent == None: return True, [] - path0 = [keyNode.obj] - path1 = [keyNode] - while keyNode.parent != None: - keyNode = keyNode.parent - path0.append(keyNode.obj) - path1.append(keyNode) - - # determine if we have suboptimal goals - for a in path1: - if a.parent != None: - prereqs = [ (p.valid(a.parent.bb_delta), p) for p in a.obj.prereqs ] - failed = [ p[1] for p in prereqs if p[0] < 1.0 ] - if len(failed) > 0: - new_goal = EvalGoal(failed[0].prereq) - new_plan = self.search_actions(actions, start_action, start_blackboard, new_goal) - return new_plan - - return True, path0 - else: - return False, [] - - diff --git a/pygoap/.README.swp b/pygoap/.README.swp new file mode 100644 index 0000000000000000000000000000000000000000..d155450c92cd45e5d768714401db46bbba25c7c8 GIT binary patch literal 16384 zcmeI2--~2N701iOm_$wd5k)XQl*xlx>FwDctO={AtM9GuuFUOQ zH}}W%bkz8)J_zcw;Dh=iismi8M)1W4!9PGy`~wsONkBj6)a{;$VuZW}Z^M`AA9btF z`JNwj>eNgwd~xS#`}p{>;q_i);_gGcKlttk&BI?Zrto1ZU;nRH>6ho{v1!kL;>sHi zeNiUds#fhhmgjSS%-dOX)#d*0*8cVF&p&92giFcry%i?ezQ;517lBE?#)_ zk&oJk-hcRJW*MFt7#J8B7#J8B7#J8B7#J8B7#J9M3p3DGcbR`8|6O1oysgLG{QUs9 z-GAoK@ERBx7#J8B7#J8B7#J8B7#J8B7#J8B7#J8B7k6qWBv+$1sZS#+z0Ljzj?rzZ-N)WH^A4y*T4(l3V7|^#=HuC3r@fi zMDQ%Q1pe_Z<^VqdKL+0cPl5Zuz2MjHH0Jx@d*B-Q`~AlJ6nq)%f?MEj@X~$8WZ=W# zclR3e3-ELBBk%=q4|w?|N zMOgTvS}UD z=ho>A&B%t{t!iKU=XvAG z-1_O%XZ$Sh>sa)UEm)GdE^b}CvUTy&Rb$)?CDJG!Pctq9dQMbl+qi|i3FXYVNg;yB zFHIY3mufur5T$8)m1-OS|Nnx}uH@1K; zddlbtZ1=1QR`>*`tvNSffXrMeytgxE5m>=qL!gwbs7 zvm;3=xfh<0(^m6U6EdXn<#DKEDXIBX9bSYeGsX1NBS7~EMg?_E#|9Z zH=C15No@>>$HZ8EcFs6?PIs=2qPlr%g!vN~gcx5*-a4DfSwNEoPIsEadZv0}C7R4B zkzX0+-;A^7cr7Nm0Tqa6(#V?fQvuf*EcvG7OkutlIgp}qNaLx9yu2#BAinjdqj$3;|llp3Gg2>lDq83m_a`8RYn(xS4p$Oy(*ud%k zZK09Tr-MjEMrJ3%NGbi|R+17h2^*<|k;G7{p~&aHHU1d;b>y5C_7$;ZmbCYX7@BfU zpIA`p9uVsn>8PUW8Zmh^{~XI9%R=0u$&CdI1iww`rayQ=ErOsQ8n%~?(6-c|j> zBr>hr^l8M#>twI>Cl!%N+GqP;7seam`fUUf`&zJ15!0^i@aSFzc1>Iff`qZt((Ej! zea*#wyqvF;C31#$dGi?y<(Y#mVn9o&hFSqm|#RwS595v0#71U z8%Q@9Ri|*y&ogqNBOCR$yRmaqHj`_6`v1=?FE3X_2KVJVKEe#M+AWB(ovFwM4aCi- z=51SD-P&3%m*ZL4jblCADgq@IwcOUH$L3D#)=s0frBvGN{;8O$Hf6$%TZi_BRBq(S zy^e}Avp3T$#{Bo+)HGU0VdM;2XQM`*Od37NveW-o@Cq2iPt(XDdT#dn8&J5HstHIi(=PlSB#8#j6}5} zIqJ?E%X9e3igZX^Nye2@gPoLMdr~T&+i}{mI-kUl>YF3$DVk14+so@L*0U*R4k=G~ zpx>yNd!a`unfDE-c6fjorXdE|NB=|DsTO0sNLNJ*-54nqHY7-4hhJ`C6E+0)56;Z7 zTGe4TZ|$WE7cY$LP5uqv9Cb3Rpli$@?W!_^a zqt&_fHOE@?o|~mytMMIeb$?>CCzUQZ#0)C)TENs;)dT67i3RI zwVDp3qZ;(mKy25y_l4Fb)~ygM_FfO^Pxx)eTz${O7|ZK1nwN>FBnS7E>u1jC0CZTl zem5-l{V7-A3(_`)l{9mlUZuY%-3J=5{lr@vTIpX&ujaHpP9L$sZpVJiOSO~V`$o^A z%jT9gXpm+=q=%YTsw+1>d{BuOG972;W0WK>#n`fk86j<{0kP)$R_{* literal 0 HcmV?d00001 diff --git a/pygoap/.actions.py.swp b/pygoap/.actions.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..a79a12ac412da13bcdf6e4b3f649b1a70b99971e GIT binary patch literal 20480 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 literal 0 HcmV?d00001 diff --git a/pygoap/.agent.py.swp b/pygoap/.agent.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..4621199a98b3f1254c77c473aa8168106a53e32c GIT binary patch literal 16384 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 YGaMwjGfLaF!m;+4n3_|S1G+Z*FJzo+T>t<8 literal 0 HcmV?d00001 diff --git a/pygoap/.blackboard.py.swp b/pygoap/.blackboard.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..b1a490a96e5cb746b3e0f94f00c9bedeef658155 GIT binary patch literal 20480 zcmeI2U5sQ!6~_w@L_S0y1`~o7!^8AUdU_Yu1za7YtB9ZrMwT_QS=#FE>gmhekKTK$ zr?&+}ebB_js3;mF5s+6jm>3ejh(;5AF-A?0kf`v01{3wk&!Fq?RNXs$cV`CO#23iD zng86rx2jH^`q!ybb?R1c_n{jO=xaKA6xVB%`sp`ccXN8yCF;`Kl!|RMl-K|5O6_!I zlof8}>b)_E-_?svf4!HPe9#%sUQ|+(*f7vAa7qkJ z+;HXE^VOcKuH1da!sYtni*7xo;!Wm;frf#Gfrf#Gfrf#Gfrf#Gfrf#XJ_BxihB{6& zP6tQ8X(e94U4w4RIp%_%3)DJOmsVflI+NuTtu_;B%k{4uM<2 z5;z~62mXAvQqO^>!1usI;Im)^ZUEPStHA}}*;mqD@BsKSxF6gDrr_P+dLY~{bAO)u zBj6CY8tetXr*G~DHv$EI$HvTa-~n(GI2W7)zQYF1I_QHQxDi|lc7uz-^K9^Z8$1d= z5AFpsa1fjgbnQx37j91DnU2!JnY3>Uoei}!YdY)QVf(JoeUs{*)rHG)JJ4CGjh>&~ zSlE;m4RxA19Tk(q{(kg5`Y)cg+Y!(-da_c5}s^4Ye!fqy5 z9)bYLB3GGB#B>syWcf_5uIkKkbxajjDdlHJHlh*D=$7tw28 z1FD~ClnT6?bfBl%BpyKN(MgnBx)wSuk0y?g-mlaV0honIi{og`VkbeR9*P$ojr6F{ zan#EVy}{4Kn$+oGmg{`HRIj1wBn*9bhlTrXQOmY7bb$ZNRP~h z)qTzzSz3^hzi@ffcPBKK_ePIxzG6_!g{DQxn2LmP5>O*CmBzGQn_i4EneJzCob~cd z_$F(SCDv<>2E$3Lr)GxcP?=2}RTR-$8Vw`HgY(^C=IYmY^8LCNZ7`6ev&XTSNzd{V zozTCicZT*V7harHvo>;&g=Y2`{WkE$66?%(M7Kb5Z{kQsmKkiI2BrOI74rEjjm%36 zws!(g8&T#h%lEF65g$rma4s6dNKi)mqP~{G&@(|CzG1*wEgcSY=>eTUL+I^b2c{^p zeguzLRB3w&`icQy(c?JkM-DRUt^@YP^sxR(L9sYmM+0A3u*o5IFG=)-7#JQYMV&&- zH^>y9l+#tbBB!?$qc==EDXqzn(qm{~PToW`mx-OtQ{(8Lc4_NbZ&En=KAw2P43(pm z(Z>=7C#Te;CuqpPID@@cgQv!=kdK!%JrokNE4>mUGpfu^d_q5lrkeX{1s5xB!?#-$ zlnbx*&g=Hydi_2b%a_uPCXOY2?k39@dQ}&};yOnsHlMW)cz3zf zA-%PvM3gwO!kWB4n)~Ip&Zeo&S8vKvTlqA}D_3r!nsb&nRSh|VN!Tg{OOjkCH)g@+ zd6w^6fPUpO@XsFW+p(LgFYha{{VYi$caqR5(F_KyBs4_IxO`RpJ zy|Az}XS|d}o?8M}wYthTa~>13Zu3?T(_FRbS_D6HL6zH=D!rpk%*@YtSb92S2srU< zQR>~1*jgAmlKr5J;KcSvCXI?jx;-&Dld?UA8!mi*GZJz?H{*!>yi&J6?_TGc1pCN+ zed4?DJ3?^jAw(^istDJcv&$Q@X5-}o6^ zfp1n~(wVpyNNoulZiHVPF!2m6>`a#eC4MAFcpX1~6}af&vZ@)xbJ%A%A(<((zaGzU zR>7CmI;b7`MhNoj6)MNs=8J_MPjVJTOb&Dov%f5|HoGgRF!vn6h7W|Yb?31kuf)>F z8Sw(0J`HiUD`UK}(Qt>$GA%r}7K@{CMrTMV7DSo-I{a!WX^w3GIgtImbDu%g|k@4Wl13!N~K zy;XPCr*nVW?dEozpR_0x8FG0F*QeCGJ~jDTnENGR>q2+Otdf0T<5I%m#r5f8SdvNt z4p&~tybx;W(Eg%DTO@UxA+)?4@>Q*^29Te)F$Et7ggFQE6dxvLc5)=(rO#~@mTDF% zWYXe+WjyA7_U+0{ilA9m#QDz=!~YU|5_|#-!6o1)=yfk} zpa8dn1K_=27ZAPA1V5tX5*wSqV_3C zkicg$9+;}oHuAQsxGLuCPTvN~z%W;pSXN>T~j z2xGCf&h2%wImiax{2L8hH<{nADkfte4Tjyy zPNy@oZvOd6vzg9|~YdfVm@qYOcO*Es`MR=p+vAVW?=TlV@wYKJMT=L_jRqxGe zFGC7c`x){($ds*0`M@O~Pd2|~5IHN!B3IRoY!Ujs4*z8(F|HrJs?l51EFZ8zQGL-% z!&fcY2~=JJ|EhY0c-dPl9mujM-qy8Um#V;K$=YzKyv1NnxomZLLz#B^*?2bJuHGy! z0=EvTL`nSrW@2QCpC$g++>R6DKL+jqmw|r~!@mH&4n71f1V1Kb{~C~Zz6V|lenGtc zbMSF+6<7vK;6m_sV)Va(r@>?3V?g5h+rS6G+rXv3>+v$4n46|G3^WWh3^WWh3^WWh z3^WY}cmKs)4okkgjxiS2?_2m2Gv$dc`QX3K ap(PG#&-}s7@=O4~Id&{36Z;$oQ0hOkTXD1i literal 0 HcmV?d00001 diff --git a/pygoap/.environment.py.swp b/pygoap/.environment.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..2a0087bc50b75f5a61afdd68a2b6036de61532d2 GIT binary patch literal 16384 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 Y7?6qJ<*ZT_O5~;JcKFRsUCprn0!?B(Q2+n{ literal 0 HcmV?d00001 diff --git a/pygoap/.environment2d.py.swp b/pygoap/.environment2d.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..73b6ab1cae3c9205284cc6875e215b1502b0c7fb GIT binary patch literal 20480 zcmeI2O^h5z6~`MJ69aL8An_?sWlfSEdw1+zV>@x!6^0-yD2^=>zOq=Oo|&4Nj(fU? z?w;8lksw6~PF%6BWUj5&zS6#2Geee_aoK#1fhXRf_2f>jaz2nq-cE3G%_k%%@=(s1h|I?A` zjio_WluJhrzhcqPi@*b;X*tN!rLLZE+0UZUA$LBY(@C6X=}@QTO1C+hzM?Sy&@mNa4XmWp5GG$=fN}J zC*a5636O#hf#cx4;DsA$3;Ym#72FFhy)g)01kZt|!S_H4h9Ci_z-{1N;4g0og5QJR zfuDm%!6~o?ZU%pReGvQsya1j8-v%3C0`33@!2z%XT-+T5KLAgFuY)c)3GM}N1D9?H zf`5T$!S}(#U<5J{fH~xD~t) z>;X4|%MRs^T$kfKRij*Y^r$2eGMD;1I_?k3hLt92foPnRtx;CQB^tXH%R!v>=M|Ec<$$ejT$EAT z(JNt9i4L?MYF7Um>I18^-;Kw`iaHaRYW%n^txH+e%W`v$Q>(UVuTZKk?dn&O)IKM& zHp#-m!mRkmtlgs|5wW^c7HT6Fp{|z7u(Io7b5^U8AVj!otyD*OWgb^S*p%MP^KmV` zfvb65#IY(J;N>zc6vq|%p*N#WnM_qwsz~+YiN?LJsV7mntsnFH<9oP`T3Bc6(ixm{ zqSfafQDw&OLKEu@scnpcmu5wy*4Esd*&0vevFXYc`(p%0sbg1R4?JCMWO>rPth^m1 z6Fu*Ow)MGz0Y|EXnRVVA;QEQ44Jk9g!qK98?0j6x%P~UJ(>5v~pJBdwd&T1IiztP= zuSTXdGYnRvSr_Y(Cr%qlAEG`Ry$UI=YQ`(e( z)URS2+GR$dGg=_k#S~zEfQofmIS2L`{<+mw?W7Jop)q65UYqUqOBuI zxHVzC8U@z+#Hu{*$0xD-GNB zwsb1o(!tYFIq1d4eOB%km1lf7BUer^n|PM%5xY|Jfe&?BgUU`WR&FU;58aCKSyR=G zaC1LZrc29ArtZ4F+P^#(UYv4axVbb}!(5vh=8763rK)-yx~eJTp|8tUG0sg% zv%C=wqfy8xT2$sDq8IkbB7=HE7bU$O@X8@q)ZX^)0O=l`8R=1i4HLkyT0OvgS$uXJ5xD87%%0UKAjX#)66VW|%(_+s_aV9$%>|=IRd6K?#W2xS zh;4I){3;?iHJI)P&B1h(l~UGfMBz#sXFAcP5_-+8U#0ONAh=|C$-9-j$ch7%gP@mZ z!}(yMT%oOT!oA&_!geQi(@daM=@abDJ}&bcs~Q3i78{G1)ifK+X&9Yl7A)1c(7kce z4CaED4S7>$uWS-`nOj{*Y)3`hVdk`NMnpo9BgSwOH~)C4Ionj~o-*j*Z)4=Uc{xqZ z`zVd``XHmmM989(8`~K%ab5*kqA-3=6vm25NuUirIl#u9%*?7rNIx3l46`a|L+qey z9pm+JdbaYE`Hn^6u{sD$Oy_Mil| z?`NNTIp!T?UNroUqC6TBhDxXOw&%h9F$)H1bb8{{@za(yRs=~5#gN%yNGoGb;dEa1 zhIC#gl9Z^$Ac`iOja6m+|6A7PvZj~yKTG}KSNuE&9tJ1CUEmmaC)f>c0KcL9Igo)h zunI&*1Ng%;;2H1?cm_NJo&nE*XTUSy8So5v2CioYB)cPr%XN@BtFvqNiT&k4lY}@4 z?9k=M!c9rbku9+akom$}K7>`}T#Bz@K7mQ5WNV^PJ|Ik|@&#d&tVdsmlI)RUlKb0l zuE@(&tw`z3&AHN&m$9Wc>(aC0Xixrdz5Zq(s&mI=C}v+X)|apHrMB$dbZY)=i8oDRak<#y+?jCs^5(I o|FRi%-R^}m+W@16NfelUmS%^X+hHNQaMUfUY+{hzg(NZl4XUkAp8x;= literal 0 HcmV?d00001 diff --git a/pygoap/.goals.py.swp b/pygoap/.goals.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..b9c0baa087b4890f1c4de623016939b91f458737 GIT binary patch literal 20480 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 M_pt5H{9n!g18zGYbN~PV literal 0 HcmV?d00001 diff --git a/pygoap/.goaltests.py.swp b/pygoap/.goaltests.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..e04e46c0ad311cadfd402d9097517bda20137e4b GIT binary patch literal 12288 zcmeI&Jx;?g6u|Kpm@5zrMd}h@&IgU!o`C@|m7!Zu42@J0i@2?_^cFn>2jU=b18PfQ zO{Hvv|C63<#~;%BjgFt=r~3zg7A9ijTx9=o^%$KFWwa1ksrPx# literal 0 HcmV?d00001 diff --git a/pygoap/.planning.py.swp b/pygoap/.planning.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..d81239c675c49fc933c7e6bed614a4b5095d7ad9 GIT binary patch literal 24576 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 literal 0 HcmV?d00001 diff --git a/pygoap/.tiledenvironment.py.swp b/pygoap/.tiledenvironment.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..645a8e9e42834b12926bce65eb03f4d21c22a326 GIT binary patch literal 12288 zcmeI2J#P~+7{^@(BnkyY1q+Kw2a+mDla`mNQbq(Vs*s={UQ`{|#Fw*l@ty9RHpfsT zW+qrE6ALdNfe{I@GO_SIn1H{1xim>j>4t>Y5g-CYfCvzQ|Ac_sV(j@O zV>gbYApHMtegpV+im}ho2k1TY4tfhcgPuUQp%rKux(FSGzMN$26Z8^#0VU8)Xa$;v zzE3gs25Lf8=*J1hzCt_DD`*>9hb}>r&@t#E*7O?Mfl{akJ%*Mcv)=0vH6lO+hyW2F z0z`la5CJ0aPYCdSi-qpSiWk-@*VJZ#nYS(BamR^86gy5a5rJ1)Vs^qznwE59&uxn8 zMj&-DDf42l%+pe7%&6+gASf2=^NZzLxrS-$+y6*-gWt)*6m5>Gy-nM3BRklVE$x@N zNj^H^3)%Mdc&?PsHvVWllDD_NrOh{Lh?1~ITKR(S*i|sBjeoH=FE&}8&Gg18J;lC!z4POV~Q+ zui=~z2WuW=HJ0VUe7@{LAv^Iy>*d;aHE+6#cN4*FHGI{L5*yOkHv>0GvWX$L;{69h lPbNNNUL1CKRsffsD2#P}^D2@nc4sV literal 0 HcmV?d00001 diff --git a/pygoap/README b/pygoap/README new file mode 100644 index 0000000..5ff67eb --- /dev/null +++ b/pygoap/README @@ -0,0 +1,102 @@ +Copyright 2010, Leif Theden + +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 3 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, see . + + + + + +how to handle ai? multiprocessing to get around the GIL in CPython. +why not threads? because we are CPU restricted, not IO. + +memory managers and blackboards should be related. +this will allow for expectations, since a memory can be simulated in the future + +memory: +should be a heap +memory added will be wrapped with a counter +everytime a memory is fetched, the counter will be added +eventually, memories not being used will be removed +and the counters will be reset + +memories should be a tree +if a memory is being added that is similiar to an existing memory, +then the existing memory will be updated, rather than replaced. + +since goals and prereqs share a common function, "valid", it makes sence to +make them subclasses of a common class. + +looking at actions.csv, it is easy to see that the behaviour of the agent will +be largely dependent on how well the action map is defined. with the little +pirate demo, it is not difficult to model his behaviour, but with larger, more +complex agents, it could quickly become a huge task to write and verify the +action map. + +with some extra steps, i would like to make it possible that the agent can +infer the prereq's to a goal through clues provided within the objects that the +agent interacts with, rather than difining them within the class. i can forsee +a performace penality for this, but that could be offset by constructing +training environments for the agent and then storing the action map that the +agent creats during training. + +the planner: +GOAP calls for a hurrstic to be used to find the optimal solution and to reduce +the number of checks made. in a physical environment where a star is used, +it makes sence to just find a vector from the current searched node to the +goal, but in action planning, there is no spatial dimension where a simple +solution like that can be used. + +without a huerstic, a* is just a tree search. the heurstic will increase the +effeciency of the planner and possibly give more consitsent results. it can +also be used to guide an agents behavoiur by manipulating some values. + +for now, while testing and building the library, the h value will not be used. +when the library is more complete, it would make sence to build a complete +agent, then construct a set of artificial scenereos to train the agent. +based on data from the scenerios, it could be possible to hardcode the h +values. the planner could then be optimised for certain scenereos. + +This module contains the most commonly used parts of the system. Classes that +have many related sibling classes are in other modules. + + +since planning is done on the blackboard and i would like agents to be able to +make guesses, or plans about other agents, then agents will have to somehow be +able to be stored on and maipulated on a blackboard. this may meant that +agents and precepts will be the same thing + +1/15/12: +overhauled the concepts of goals, prereqs, and effects and rolled them into +one class. with the new system of instanced actions, it makes design sense to +consolidate them, since they all have complimentary functionality. from a +performace standpoint, it may make sense to keep the seperate, but this way is +much easier to conceptualize in your mind, and i am not making an system that +is performace sensative....this is python. + +simplify holding/inventory: + an objects location should always be a tuple of: + ( holding object, position ) + +this will make position very simple to sort. the position in the tuple should +be a value the that holding object can make sence of, for example, an +environment might expect a zone, and (x,y), while an agent would want an index +number in their inventory. + +a side effect will be that location goals and holding goals can be consolidated +into one function. + +new precepts may render a current plan invalid. to account for this, an agent +will replan everytime it receives a precept. a better way would be to tag a +type of precept and then if a new one that directly relates to the plan arrives +then replan. diff --git a/pygoap/__init__.py b/pygoap/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pygoap/__init__.py @@ -0,0 +1 @@ + diff --git a/pygoap/actions.py b/pygoap/actions.py new file mode 100644 index 0000000..e7f1075 --- /dev/null +++ b/pygoap/actions.py @@ -0,0 +1,194 @@ +""" +These are the building blocks for creating pyGOAP agents that are able to +interact with their environment in a meaningful way. + +When actions are updated, they can return a precept for the environment to +process. The action can emit a sound, sight, or anything else for other +objects to consume. + +These classes will be known to an agent, and chosen by the planner as a means +to satisfy the current goal. They will be instanced and the the agent will +execute the action in some way, one after another. + +There is a delicate balance between the actions here and the "ActionEffects" +and "ActionPrereqs" that you will have to master. A simple way to mentally +distinguish them is that the prereqs and effects are clues to the planner to +behave in a certain way and will never change anything except a blackboard. +The action classes here are the 'guts' for the action and will modify the game +environment in some meaningful way. + + +Actions need to be split into ActionInstances and ActionBuilders. + +An ActionInstance's job is to work in a planner and to carry out actions. +A ActionBuilder's job is to query the caller and return a list of suitable +actions for the bb. +""" + +from planning import * +from actionstates import * +import sys + + +test_fail_msg = "some goal is returning None on a test, this is a bug." + +class ActionBuilder(object): + """ + ActionBuilders examine a blackboard and return a list of actions + that can be succesfully completed at the the time. + """ + + def get_actions(self, caller, bb): + raise NotImplementedError + + def __init__(self, **kwargs): + self.prereqs = [] + self.effects = [] + self.costs = {} + + self.__dict__.update(kwargs) + + self.setup() + + def setup(self): + """ + add the prereqs, effects, and costs here + override this + """ + pass + + def __repr__(self): + return "".format(self.__class__.__name__) + + +class CallableAction(InstancedAction): + """ + callable action class. + + subclass this class to implement the code side of actions. + for the most part, "start" and "update" will be the most + important methods to overload. + """ + + def __init__(self, caller, **kwargs): + self.caller = caller + self.state = ACTIONSTATE_NOT_STARTED + + self.prereqs = [] + self.effects = [] + self.costs = {} + + self.__dict__.update(kwargs) + + def test(self, bb=None): + """ + make sure the action is able to be started + return a float from 0-1 that describes how valid this action is. + + validity of an action is a measurement of how effective the action + will be if it is completed successfully. + + if any of the prereqs are not partially valid ( >0 ) then will + return 0 + + this value will be used in planning. + + for many actions a simple 0 or 1 will work. for actions which + modify numerical values, it may be useful to return a fractional + value. + """ + + # NOTE: may be better written with itertools + + if len(self.prereqs) == 0: return 1.0 + if bb == None: raise Exception + total = [ i.test(bb) for i in self.prereqs ] + print "[goal] {} test {}".format(self, total) + #if 0 in total: return 0 + try: + return float(sum(total)) / len(self.prereqs) + except TypeError: + print zip(total, self.prereqs) + print test_fail_msg + sys.exit(1) + + + def touch(self, bb=None): + """ + call when the planning phase is complete + """ + if bb == None: bb = self.caller.bb + [ i.touch(bb) for i in self.effects ] + + + def start(self): + """ + start running the action + """ + self.state = ACTIONSTATE_RUNNING + + def update(self, time): + """ + actions which occur over time should implement + this method. + + if the action does not need more that one cycle, then + you should use the calledonceaction class + """ + pass + + def fail(self, reason=None): + """ + maybe what we planned to do didn't work for whatever reason + """ + self.state = ACTIONSTATE_FAILED + + def abort(self): + """ + stop the action without the ability to complete or continue + """ + self.state = ACTIONSTATE_BAILED + + def finish(self): + """ + the planned action was completed and the result is correct + """ + if self.state == ACTIONSTATE_RUNNING: + self.state = ACTIONSTATE_FINISHED + + def ok_finish(self): + """ + determine if the action can finish now + if cannot finish now, then the action + should bail if it is forced to finish. + """ + return self.state == ACTIONSTATE_FINISHED + + def pause(self): + """ + stop the action from updating. should be able to continue. + """ + self.state = ACTIONSTATE_PAUSED + + +class CalledOnceAction(CallableAction): + """ + Is finished imediatly when started. + """ + + def start(self): + # valid might return a value less than 1 + # this means that some of the prereqs are not + # completely satisfied. + # since we want everything to be completely + # satisfied, we require valid == 1. + if self.test() == 1.0: + CallableAction.start(self) + CallableAction.finish(self) + else: + self.fail() + + def update(self, time): + pass + + diff --git a/pygoap/actionstates.py b/pygoap/actionstates.py new file mode 100644 index 0000000..16c8070 --- /dev/null +++ b/pygoap/actionstates.py @@ -0,0 +1,6 @@ +ACTIONSTATE_NOT_STARTED = 0 +ACTIONSTATE_FINISHED = 1 +ACTIONSTATE_RUNNING = 2 +ACTIONSTATE_PAUSED = 3 +ACTIONSTATE_ABORTED = 4 +ACTIONSTATE_FAILED = 5 diff --git a/pygoap/agent.py b/pygoap/agent.py new file mode 100644 index 0000000..a6d8cd1 --- /dev/null +++ b/pygoap/agent.py @@ -0,0 +1,177 @@ +""" +fill in later +""" + +from environment import ObjectBase +from planning import plan, InstancedAction +from blackboard import Blackboard, MemoryManager, Tag +from actionstates import * + + +NullAction = InstancedAction() + + +# required to reduce memory usage +def time_filter(precept): + if precept.sense == "time": + return None + else: + return precept + + +class GoapAgent(ObjectBase): + """ + AI Agent + + every agent should have at least one goal (otherwise, why use it?) + inventories will be implemented using precepts and a list. + + currently, only one action running concurrently is supported. + """ + + # this will set this class to listen for this type of precept + # not implemented yet + interested = [] + + def __init__(self): + self.idle_timeout = 30 + self.bb = Blackboard() + self.mem_manager = MemoryManager(self) + self.planner = plan + + self.current_goal = None + + self.goals = [] # all goals this instance can use + self.invalid_goals = [] # goals that cannot be satisfied now + self.filters = [] # list of methods to use as a filter + self.actions = [] # all actions this npc can perform + self.plan = [] # list of actions to perform + # '-1' will be the action currently used + + # this special filter will prevent time precepts from being stored + self.filters.append(time_filter) + + def add(self, other, origin): + # we simulate the agent's knowledge of its inventory with precepts + p = Precept(sense="inventory") + + # do the actual add + super(GoapAgent, self).add(other, origin) + + def remove(self, obj): + # we simulate the agent's knowledge of its inventory with precepts + p = Precept(sense="inventory") + + # do the actual remove + super(GoapAgent, self).remove(other, origin) + + def add_goal(self, goal): + self.goals.append(goal) + + def remove_goal(self, goal): + self.goals.remove(goal) + + def add_action(self, action): + self.actions.append(action) + + def remove_action(self, action): + self.actions.remove(action) + + def filter_precept(self, precept): + """ + precepts can be put through filters to change them. + this can be used to simulate errors in judgement by the agent. + """ + + for f in self.filters: + precept = f(precept) + if precept == None: + break + + return precept + + def handle_precept(self, pct): + """ + used by the environment to feed the agent precepts. + agents can respond by sending back an action to take. + """ + + # give our filters a chance to change the precept + pct = self.filter_precept(pct) + + # our filters may have caused us to ignore the precept + if pct == None: return None + + print "[agent] {} recv'd pct {}".format(self, pct) + + # this line has been added for debugging purposes + self.plan = [] + + if pct.sense == "position": + self.bb.post(Tag(position=pct.position, obj=pct.thing)) + + return self.next_action() + + def replan(self): + """ + force agent to re-evaluate goals and to formulate a plan + """ + + # get the relevancy of each goal according to the state of the agent + s = [ (g.get_relevancy(self.bb), g) for g in self.goals ] + s = [ g for g in s if g[0] > 0 ] + s.sort(reverse=True) + + print "[agent] goals {}".format(s) + + # starting for the most relevant goal, attempt to make a plan + for score, goal in s: + ok, plan = self.planner( + self, + self.actions, + self.current_action(), + self.bb, + goal) + + if ok: + print "[agent] {} has planned to {}".format(self, goal) + pretty = list(reversed(plan[:])) + print "[agent] {} has plan {}".format(self, pretty) + return plan + else: + print "[agent] {} cannot {}".format(self, goal) + + return [] + + def current_action(self): + try: + return self.plan[-1] + except IndexError: + return NullAction + + def running_actions(self): + return self.current_action() + + def next_action(self): + """ + get the next action of the current plan + """ + + if self.plan == []: + self.plan = self.replan() + + current_action = self.current_action() + + # this action is done, so return the next one + if current_action.state == ACTIONSTATE_FINISHED: + return self.plan.pop() + + # this action failed somehow + elif current_action.state == ACTIONSTATE_FAILED: + raise Exception, "action failed, don't know what to do now!" + + # our action is still running, just run that + elif current_action.state == ACTIONSTATE_RUNNING: + return current_action + + diff --git a/pygoap/blackboard.py b/pygoap/blackboard.py new file mode 100644 index 0000000..6007eb5 --- /dev/null +++ b/pygoap/blackboard.py @@ -0,0 +1,169 @@ +""" +Memories are stored precepts. +A blackboard is a device to share information amongst actions. +This implementation uses sqlite3 as a backend for storing memories. +""" + +import sqlite3 +from sqlalchemy import * +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relation, sessionmaker + +from collections import defaultdict + +DEBUG = 0 + +Base = declarative_base() + +class Tag(object): + """ + simple object for storing data on a blackboard + """ + + def __init__(self, **kwargs): + if 'kw' in kwargs.keys(): + del kwargs['kw'] + self.kw = kwargs + else: + self.kw = kwargs + + def __repr__(self): + return "".format(self.kw) + +class Memory(Base): + __tablename__ = 'memory' + + id = Column(Integer, primary_key=True) + owner = Column(String(255)) + value = Column(String(255)) + + def __init__(self, owner, value): + self.owner = owner + self.value = value + + +# initialize our database +engine = create_engine('sqlite://') +Base.metadata.create_all(engine) + + +class MemoryManager(object): + """ + a memory manager's purpose is to store precepts. + + memories here should be able to be recalled quickly. like a blackboard, + this class is designed to have many users (not thread safe now). + """ + + def __init__(self, owner=None): + self.owner = owner + + def add(self, precept, confidence=1.0): + """ + Precepts may have a confidence leve associated with them as a metric + for sorting out precepts that may not be reliable. + + This mechanism primarly exists for users of a shared blackboard where + they may post conflicting information. + """ + + Session = sessionmaker(bind=engine) + session = Session() + + m = Memory(None, "") + + try: + session.add(m) + session.commit() + except: + if DEBUG: print "error:", m + session.rollback() + raise + + def search(self, tag, owner=None): + alldata = session.query(Memory).all() + for somedata in alldata: + if DEBUG: print somedata + + +class Blackboard(object): + """ + a blackboard is an abstract memory device. + + alternative, more robust solution would be a xml backend or database + + tags belong to a set of values that are known by the actions that an actor + would find useful. tag names are simple strings and have a value + associated with them. for simplicity, a blackboard could be used like a + standard python dictionary. + + shared blackboards violate reality in that multiple agents share the same + thoughts, to extend the metaphore. but, the advantage of this is that in + a real-time simulation, it gives the player the impression that the agents + are able to collobroate in some meaningful way, without a significant + impact in performace. + + that being said, i have chosen to restrict blackboards to a per-agent + basis. this library is meant for rpgs, where the action isn't real-time + and would require a more realistic simulation of intelligence. + + however, i will still develop blackboards with the intention that they are + shared, so that in the future, it will be easier to simulate the borg-mind. + """ + + def __init__(self): + self.memory = [] + + def __eq__(self, other): + if isinstance(other, Blackboard): + return self.memory == other.memory + else: + return False + + def post(self, tag): + if not isinstance(tag, Tag): + m="Only instances of tag objects can be stored on a blackboard" + raise ValueError, m + + d = tag.kw.copy() + self.memory.append(d) + + def read(self, *args, **kwargs): + """ + return any data that match the keywords in the function call + returns a list of dictionaries + """ + + if (args == ()) and (kwargs == {}): + return self.memory + + tags = [] + r = [] + + if DEBUG: print "[bb] args {}".format(args) + if DEBUG: print "[bb] kwargs {}".format(kwargs) + + def check_args(tag): + if args == (): + return True + else: + keys = tag.keys() + return all([ a in keys for a in args ]) + + def check_kwargs(tag): + if kwargs == {}: + return True + else: + raise ValueError, "Blackboards do not support search...yet" + + for tag in self.memory: + if DEBUG: print "[bb] chk args {} {}".format(tag, check_args(tag)) + if DEBUG: print "[bb] chk kw {} {}".format(tag, check_kwargs(tag)) + if check_args(tag) and check_kwargs(tag): + r.append(tag) + + #r.reverse() + return r + + def update(self, other): + self.memory.extend(other.memory) diff --git a/pygoap/environment.py b/pygoap/environment.py new file mode 100644 index 0000000..48a16df --- /dev/null +++ b/pygoap/environment.py @@ -0,0 +1,186 @@ +""" +Since a pyGOAP agent relies on cues from the environment when planning, having +a stable and effecient virtual environment is paramount. + +When coding your game or simulation, you can think of the environment as the +conduit that connects your actors on screen to their simulated thoughts. This +environment simply provides enough basic information to the agents to work. It +is up to you to make it useful. + +objects should be able to produce actions that would be useful. this concept +comes from the sims, where each agent doesn't need to know how to use every +object, but can instead query the object for things to do with it. +""" + +from actionstates import * +from objectflags import * +from itertools import chain, repeat, product, izip + + + +class ObjectBase(object): + """ + class for objects that agents can interact with + """ + + def __init__(self, name): + self.name = name + self.inventory = [] + + def handle_precept(self, precept): + pass + + def add(self, other, origin): + """ + add something to this object's inventory + the object must have an origin. + the original position of the object will lose this object: + dont remove it manually! + """ + + origin.inventory.remove(other) + self.inventory.append(other) + + def remove(self, obj): + """ + remove something from this object's inventory + """ + + self.inventory.remove(other) + + def get_actions(self, other): + """ + generate a list of actions that could be used with this object + """ + return [] + + def __repr__(self): + return "".format(self.name) + + +class Precept(object): + """ + This is the building block class for how an agent interacts with the + simulated environment. + """ + + def __init__(self, *arg, **kwargs): + self.__dict__.update(kwargs) + + def __repr__(self): + return "" % self.__dict__ + + +class Environment(object): + """Abstract class representing an Environment. 'Real' Environment classes + inherit from this. + The environment keeps a list of .objects and .agents (which is a subset + of .objects). Each agent has a .performance slot, initialized to 0. + """ + + def __init__(self, things=[], agents=[], time=0): + self.time = time + self.agents = [] + self.things = [] + + [ self.add_thing(i) for i in things ] + [ self.add_thing(i) for i in agents ] + + self.action_que = [] + + def default_position(self, object): + """ + Default position to place a new object with unspecified position. + """ + + raise NotImplementedError + + def run(self, steps=1000): + """ + Run the Environment for given number of time steps. + """ + + [ self.update(1) for step in xrange(steps) ] + + def add_thing(self, thing, position=None): + """ + Add an object to the environment, setting its position. Also keep + track of objects that are agents. Shouldn't need to override this. + """ + + from agent import GoapAgent + + thing.position = position or self.default_position(thing) + self.things.append(thing) + + print "[env] adding {}".format(thing) + + # add the agent + if isinstance(thing, GoapAgent): + self.agents.append(thing) + thing.performance = 0 + thing.environment = self + + # for simplicity, agents always know where they are + i = Precept(sense="position", thing=thing, position=thing.position) + thing.handle_precept(i) + + # should update vision for all interested agents (correctly, that is) + [ self.look(a) for a in self.agents if a != thing ] + + def update(self, time_passed): + """ + * Update our time + * Let agents know time has passed + * Update actions that may be running + * Add new actions to the que + + this could be rewritten. + """ + + # update time in the simulation + self.time += time_passed + + # let all the agents know that time has passed + # bypass the modeler for simplicity + p = Precept(sense="time", time=self.time) + [ a.handle_precept(p) for a in self.agents ] + + # update all the actions that may be running + precepts = [ a.update(time_passed) for a in self.action_que ] + precepts = [ p for p in precepts if not p == None ] + + # get all the running actions for the agents + self.action_que = chain([ a.running_actions() for a in self.agents ]) + + # start any actions that are not started + [ action.start() for action in self.action_que + if action.state == ACTIONSTATE_NOT_STARTED ] + + def broadcast_precepts(self, precepts, agents=None): + """ + for effeciency, please use this for sending a list of precepts + """ + + if agents == None: + agents = self.agents + + model = self.model_precept + + for p in precepts: + [ a.handle_precept(model(p, a)) for a in agents ] + + def model_precept(self, precept, other): + """ + override this to model the way that precept objects move in the + simulation. by default, all precept objects will be distributed + indiscrimitely to all agents. + + while this behaviour may be desireable for some types of precepts, + it doesn't make sense in many. + + the two big thigs to model here would be vision and sound. + """ + + return precept + diff --git a/pygoap/environment2d.py b/pygoap/environment2d.py new file mode 100644 index 0000000..d6c3df2 --- /dev/null +++ b/pygoap/environment2d.py @@ -0,0 +1,137 @@ +""" +Since a pyGOAP agent relies on cues from the environment when planning, having +a stable and effecient virtual environment is paramount. This environment is +simply a placeholder and demonstration. + +When coding your game or simulation, you can think of the environment as the +conduit that connects your actors on screen to their simulated thoughts. This +environment simply provides enough basic information to the agents to work. It +is up to you to make it useful. +""" + +from pygoap.agent import GoapAgent +from environment import Environment, Precept +import random, math + + + +def distance((ax, ay), (bx, by)): + "The distance between two (x, y) points." + return math.hypot((ax - bx), (ay - by)) + +def distance2((ax, ay), (bx, by)): + "The square of the distance between two (x, y) points." + return (ax - bx)**2 + (ay - by)**2 + +def clip(vector, lowest, highest): + """Return vector, except if any element is less than the corresponding + value of lowest or more than the corresponding value of highest, clip to + those values. + >>> clip((-1, 10), (0, 0), (9, 9)) + (0, 9) + """ + return type(vector)(map(min, map(max, vector, lowest), highest)) + + +class Pathfinding2D(object): + def get_surrounding(self, position): + """ + Return all positions around this one. + """ + + x, y = position + return ((x-1, y-1), (x-1, y), (x-1, y+1), (x, y-1), (x, y+1), + (x+1, y-1), (x+1, y), (x+1, y+1)) + + def calc_h(self, position1, position2): + return distance(position1, position2) + + +class XYEnvironment(Environment, Pathfinding2D): + """ + This class is for environments on a 2D plane. + + This class is featured enough to run a simple simulation. + """ + + def __init__(self, width=10, height=10): + super(XYEnvironment, self).__init__() + self.width = width + self.height = height + + def model_vision(self, precept, origin, terminus): + return precept + + def model_sound(self, precept, origin, terminus): + return precept + + def look(self, caller, direction=None, distance=None): + """ + Simulate vision by sending precepts to the caller. + """ + + # a more intelligent approach would limit the number of agents + # to logical limit, ie: ones that could possibly been seen + agents = self.things[:] + agents.remove(caller) + + model = self.model_precept + for a in agents: + p = Precept(sense='position', thing=a, position=a.position) + caller.handle_precept(model(p, caller)) + + def move(self, thing, pos): + """ + move an object in the world + """ + + thing.position = pos + + print "[env] move {} to {}".format(thing, pos) + + [ self.look(a) for a in self.agents if a != thing ] + + def objects_at(self, position): + """ + Return all objects exactly at a given position. + """ + + return [ obj for obj in self.things if obj.position == position ] + + def objects_near(self, position, radius): + """ + Return all objects within radius of position. + """ + + radius2 = radius * radius + return [ obj for obj in self.things + if distance2(position, obj.position) <= radius2 ] + + def default_position(self, thing): + loc = (random.randint(0, self.width), random.randint(0, self.height)) + return (self, loc) + + def model_precept(self, precept, other): + if precept.sense == "vision": + return precept + + if precept.sense == "sound": + return precept + + return precept + + def can_move_from(self, agent, dist=100): + """ + return a list of positions that are possible for this agent to be + in if it were to move [dist] spaces or less. + """ + + x, y = agent.position[1] + pos = [] + + for xx in xrange(x - dist, x + dist): + for yy in xrange(y - dist, y + dist): + if distance2((xx, yy), (x, y)) <= dist: + pos.append((self, (xx, yy))) + + return pos diff --git a/pygoap/goals.py b/pygoap/goals.py new file mode 100644 index 0000000..d7b9b27 --- /dev/null +++ b/pygoap/goals.py @@ -0,0 +1,290 @@ +""" +Goals in the context of a pyGOAP agent give the planner some direction when +planning. Goals are known to the agent and are constantly monitored and +evaluated. The agent will attempt to choose the most relevant goal for it's +state (determined by the blackboard) and then the planner will determine a +plan for the agent to follw that will (possibly) satisfy the chosen goal. + +See the modules effects.py and goals.py to see how these are used. + +test() should return a float from 0-1 on how successful the action would be +if carried out with the given state of the bb. + +touch() should modify a bb in some meaningful way as if the action was +finished successfully. +""" + +from planning import GoalBase +from blackboard import Tag +import sys + + +DEBUG = 0 + +class SimpleGoal(GoalBase): + """ + Goal that uses a dict to match precepts stored on a bb. + """ + + def test(self, bb): + #f = [ k for (k, v) in self.kw.items() if v == False] + + for tag in bb.read(): + + if tag == self.kw: + return 1.0 + + return 0.0 + + def touch(self, bb): + bb.post(Tag(**self.kw)) + + def __repr__(self): + return "<{}=\"{}\">".format(self.__class__.__name__, self.kw) + + +class EvalGoal(GoalBase): + """ + uses what i think is a somewhat safe way of evaluating python statements. + + feel free to contact me if you have a better way + """ + + def test(self, bb): + + condition = self.args[0] + + # this only works for simple expressions + cmpop = (">", "<", ">=", "<=", "==") + + i = 0 + index = 0 + expr = condition.split() + while index == 0: + try: + index = expr.index(cmpop[i]) + except: + i += 1 + if i > 5: break + + try: + side0 = float(eval(" ".join(expr[:index]), bb)) + side1 = float(eval(" ".join(expr[index+1:]), bb)) + except NameError: + return 0.0 + + cmpop = cmpop[i] + + if (cmpop == ">") or (cmpop == ">="): + if side0 == side1: + return 1.0 + elif side0 > side1: + v = side0 / side1 + elif side0 < side1: + if side0 == 0: + return 0.0 + else: + v = 1 - ((side1 - side0) / side1) + + if v > 1: v = 1.0 + if v < 0: v = 0.0 + + return v + + def touch(self, bb): + def do_it(expr, d): + + try: + exec expr in d + except NameError as detail: + # get name of missing variable + name = detail[0].split()[1].strip('\'') + d[name] = 0 + do_it(expr, d) + + return d + + d = {} + d['__builtins__'] = None + d = do_it(self.args[0], d) + + # the bb was modified + bb.post(Tag(kw=d)) + + return True + + +class AlwaysValidGoal(GoalBase): + """ + Will always be valid. + """ + + def test(self, bb): + return 1.0 + + def touch(self, bb, tag): + pass + + + +class NeverValidGoal(GoalBase): + """ + Will never be valid. + """ + + def test(self, bb): + return 0.0 + + def touch(self, bb, tag): + pass + + + +class PositionGoal(GoalBase): + """ + This validator is for finding the position of objects. + """ + + def test(self, bb): + """ + search memory for last known position of the target if target is not + in agent's memory return 0.0. + + do pathfinding and determine if the target is accessable + - if not return 0.0 + + determine the distance required to travel to the target + return 1.0 if the target is reachable + """ + + target = None + target_position = None + tags = bb.read("position") + + if DEBUG: print "[PositionGoal] testing {}".format(self.kw) + + for tag in tags: + target = tag['obj'] + for k, v in self.kw.items(): + try: + value = getattr(target, k) + except AttributeError: + continue + + if not v == value: + continue + + target_position = tag['position'] + break + else: + continue + break + + if target_position: + if DEBUG: print "[PositionGoal] {} {}".format(self.kw['owner'], target) + return 1.0 + + d = distance(position, target_position) + if d > self.dist: + return (float(self.dist / d)) * float(self.dist) + elif d <= self.dist: + return 1.0 + else: + return 0.0 + + + def touch(self, bb): + + # this needs to be the same as what handle_precept() of an agent + # would post if it had recv'd this from the environment + tag = Tag(obj=self.kw['target'], + position=self.kw['position']) + + bb.post(tag) + +class HasItemGoal(GoalBase): + """ + returns true if item is in inventory (according to bb) + + when creating instance, 'owner' must be passed as a keyword. + its value can be any game object that is capable of holding an object + + NOTE: testing can be true to many different objects, + but touching requires a specific object to function + + any other keyword will be evaluated against tags in the bb passed. + """ + + def __init__(self, owner, target=None, **kwargs): + super(HasItemGoal, self).__init__(self) + + self.owner = owner + self.target = None + + if target: + self.target = target + else: + try: + self.target = kwargs['target'] + except KeyError: + pass + + if (self.target == None) and (kwargs == {}): + raise Exception, "HasItemGoal needs more information" + + + def test(self, bb): + for tag in bb.read("position"): + if (tag['position'][0] == self.owner) and \ + tag['obj'] == self.target: + return 1.0 + + return 0.0 + + def touch(self, bb): + # this has to be the same tag that the agent would add to its bb + tag = Tag(obj=self.target, position=(self.owner, 0)) + + if DEBUG: print "[HasItem] {} touch {}".format(self, tag) + + bb.post(Tag(obj=self.target, position=(self.owner, 0))) + +""" + +code for seaching a blackboard based on keywords +originally from hasitemgoal + + target = None + target_position = None + tags = bb.read("position") + + tags.reverse() + + for tag in tags: + target = tag['obj'] + for k, v in self.kw.items(): + if k == 'owner': + continue + + try: + value = getattr(target, k) + except AttributeError: + continue + + if not v == value: + continue + + target_position = tag['position'] + break + else: + continue + break + + if target_position: + print "[HasItem] {} {}".format(self.owner, self.target) + print " {} {}".format(self.owner, target_position) + if target_position[0] == owner: + print "[HasItem] {} {}".format(self.owner, self.target) + return 1.0 + +""" diff --git a/pygoap/goaltests.py b/pygoap/goaltests.py new file mode 100644 index 0000000..c5c2eee --- /dev/null +++ b/pygoap/goaltests.py @@ -0,0 +1,2 @@ +import unittest + diff --git a/pygoap/objectflags.py b/pygoap/objectflags.py new file mode 100644 index 0000000..3ffcad0 --- /dev/null +++ b/pygoap/objectflags.py @@ -0,0 +1,20 @@ +states = """ +liquid +glowing +hot +frozen +burning +normal +dead +dying +bleeding +cracked +broken +hard +soft +sticky +ooze +gas +""".strip().split('\n') + + diff --git a/pygoap/planning.py b/pygoap/planning.py new file mode 100644 index 0000000..d03c56a --- /dev/null +++ b/pygoap/planning.py @@ -0,0 +1,263 @@ +""" +Goals and prereqs are related. The Vaildator class system removes the need to +duplicate similar functions. +""" + +from blackboard import Blackboard +from heapq import heappop, heappush, heappushpop +import sys + + + +DEBUG = 0 + +def get_children(caller, parent, actions, dupe_parent=False): + """ + get the children of this action + + behaves like a tree search, kinda, not really sure, actually + return every other action on this branch that has not already been used + """ + + def keep_node(node): + # verify node is ok by making sure it is not duplicated in it's branch + + keep = True + + node0 = node.parent + while not node0.parent == None: + if node0.parent == node: + keep = False + break + node0 = node0.parent + + return keep + + children = [] + + if DEBUG: print "[plan] actions: {}".format([a for a in actions]) + + for a in actions: + if DEBUG: print "[plan] checking {}".format(a) + for child in a.get_actions(caller, parent.bb): + node = PlanningNode(parent, child) + + if keep_node(node): + #if DEBUG: print "[plan] got child {}".format(child) + children.append(node) + + return children + + +def calcG(node): + cost = node.cost + while not node.parent == None: + node = node.parent + cost += node.cost + return cost + + +class PlanningNode(object): + """ + each node has a copy of a bb (self.bb) in order to simulate a plan. + """ + + def __init__(self, parent, action, bb=None): + self.parent = parent + self.action = action + self.bb = Blackboard() + self.delta = Blackboard() + #self.cost = action.calc_cost() + self.cost = 1 + self.g = calcG(self) + self.h = 1 + + if not parent == None: + self.bb.update(parent.bb) + + elif not bb == None: + self.bb.update(bb) + + action.touch(self.delta) + self.bb.update(self.delta) + + def __eq__(self, other): + if isinstance(other, PlanningNode): + #if DEBUG: print "[cmp] {} {}".format(self.delta.memory, other.delta.memory) + return self.delta == other.delta + else: + return False + + def __repr__(self): + try: + return "" % \ + (self.action.__name__, + self.cost, + self.parent.action.__class__.__name__) + + except AttributeError: + return "" % \ + (self.action.__class__.__name__, + self.cost) + +class GoalBase(object): + """ + Goals: + can be satisfied. + can be valid + + This is meant to be a superclass along with a validator class to create + goals and action prereqs at runtime. When creating designing subclasses, + sibling superclass should be a validator. + + Goals, ActionPrereqs and ActionEffects are now that same class. They share + so much functionality and are so logically similar that they have been made + into one class. + + The only difference is how they are used. If a goal is used by the planner + then that will be the final point of the plan. if it is used in + conjunction with an action, then it will function as a prereq. + """ + + def __init__(self, *args, **kwargs): + try: + self.condition = args[0] + except IndexError: + self.condition = None + + self.value = 1.0 + self.args = args + self.kw = kwargs + + self.satisfied = self.test + + def touch(self, bb): + if DEBUG: print "[debug] goal {} has no touch method".format(self) + + def test(self, bb): + if DEBUG: print "[debug] goal {} has no test method".format(self) + + def get_relevancy(self, bb): + """ + will return the "relevancy" value for this goal/prereq. + + as a general rule, the return value here should never equal + what is returned from test() + """ + + if not self.test(bb): return self.value + return 0.0 + + + def self_test(self): + """ + make sure the goal is sane + """ + + bb = Blackboard() + self.touch(bb) + assert self.test(bb) == True + + + def __repr__(self): + return "<{}>".format(self.__class__.__name__) + + +class InstancedAction(object): + """ + This action is suitable as a generic 'idling' action. + """ + + builder = None + + def __init__(self): + self.state = None + + def touch(self, bb): + if DEBUG: print "[debug] action {} has no touch method".format(self) + + def test(self, bb): + if DEBUG: print "[debug] action {} has no test method".format(self) + + def __repr__(self): + return self.__class__.__name__ + + +def plan(caller, actions, start_action, start_blackboard, goal): + """ + differs slightly from normal astar in that: + there are no connections between nodes + the state of the "map" changes as the nodes are traversed + there is no closed list (behaves like a tree search) + hueristics are not available + + this is not implied to be correct or effecient + """ + + # the pushback is used to limit node access in the heap + pushback = None + success = False + + keyNode = PlanningNode(None, start_action, start_blackboard) + + openlist = [(0, keyNode)] + + # the root can return a copy of itself, the others cannot + # this allows the planner to produce plans that duplicate actions + # this feature is currently on a hiatus + return_parent = 0 + + if DEBUG: print "[plan] solve {} planning {}".format(goal, start_action) + + while openlist or pushback: + + # get the best node. + if pushback == None: + keyNode = heappop(openlist)[1] + else: + keyNode = heappushpop( + openlist, (pushback.g + pushback.h, pushback))[1] + + pushback = None + + if DEBUG: print "[plan] testing action {}".format(keyNode.action) + if DEBUG: print "[plan] against bb {}".format(keyNode.bb.read()) + + # if our goal is satisfied, then stop + #if (goal.satisfied(keyNode.bb)) and (return_parent == 0): + if goal.test(keyNode.bb): + success = True + if DEBUG: print "[plan] successful {}".format(keyNode.action) + break + + for child in get_children(caller, keyNode, actions, return_parent): + if child in openlist: + possG = keyNode.g + child.cost + if (possG < child.g): + child.parent = keyNode + child.g = calcG(child) + # TODO: update the h score + else: + # add node to our openlist, using pushpack if needed + if pushback == None: + heappush(openlist, (child.g + child.h, child)) + else: + heappush(openlist, (pushback.g + pushback.h, pushback)) + pushpack = child + + return_parent = 0 + + if success: + path0 = [keyNode.action] + path1 = [keyNode] + while not keyNode.parent == None: + keyNode = keyNode.parent + path0.append(keyNode.action) + path1.append(keyNode) + + return True, path0 + + else: + return False, [] + + diff --git a/pygoap/tiledenvironment.py b/pygoap/tiledenvironment.py new file mode 100644 index 0000000..bbe6a23 --- /dev/null +++ b/pygoap/tiledenvironment.py @@ -0,0 +1,42 @@ +from environment2d import XYEnvironment +import tmxloader +from pygame import Surface + + + +class TiledEnvironment(XYEnvironment): + """ + Environment that can use Tiled Maps + """ + + def __init__(self, filename): + self.filename = filename + self.tiledmap = tmxloader.load_pygame(self.filename) + + super(TiledEnvironment, self).__init__() + + def render(self, surface): + # not going for effeciency here + + for l in xrange(0, len(self.tiledmap.layers)): + for y in xrange(0, self.tiledmap.height): + for x in xrange(0, self.tiledmap.width): + tile = self.tiledmap.get_tile_image(x, y, l) + xx = x * self.tiledmap.tilewidth + yy = y * self.tiledmap.tileheight + if not tile == 0: + surface.blit(tile, (xx, yy)) + + for t in self.things: + x, y = t.position[1] + x *= self.tiledmap.tilewidth + y *= self.tiledmap.tileheight + + s = Surface((self.tiledmap.tilewidth, self.tiledmap.tileheight)) + s.fill((128,0,0)) + + surface.blit(s, (x, y)) + + def __repr__(self): + return "T-Env" + diff --git a/pygoap/tmxloader.py b/pygoap/tmxloader.py new file mode 100644 index 0000000..f575bd4 --- /dev/null +++ b/pygoap/tmxloader.py @@ -0,0 +1,625 @@ +""" +Map loader for TMX Files +bitcraft (leif.theden at gmail.com) +v.7 - for python 2.7 + +If you have any problems, please contact me via email. +Tested with Tiled 0.7.1 for Mac. + +====================================================================== + +This map loader can be used to load maps created in the Tiled map +editor. It provides a simple way to get tiles and associated metadata +so that you can draw a map onto the screen. + +This is not a rendering engine. It will load the data that is +necessary to render a map onto the screen. All tiles will be loaded +into in memory and available to blit onto the screen. + + +Design Goals: + Simple api + Memory efficient and fast + Quick access to tiles, attributes, and properties + +Non-Goals: + Rendering + +Works: + Image loading with pygame + Map loading with all required types + Properties for all types: maps, layers, objects, tiles + Automatic flipping of tiles + Supports csv, gzip, zlib and uncompressed TMX + +Todo: + Pygame: test colorkey transparency + +Optimized for maps that do not make heavy use of tile +properties. If I find that it is used a lot then I can rework +it for better performance. + +====================================================================== + +Basic usage sample: + + >>> import tmxloader + >>> tiledmap = tmxloader.load_pygame("map.tmx") + + +When you want to draw tiles, you simply call "get_tile_image": + + >>> image = tiledmap.get_tile_image(x, y, layer) + >>> screen.blit(position, image) + + +Layers, objectgroups, tilesets, and maps all have a simple way to access +metadata that was set inside tiled: they all become class attributes. + + >>> print layer.tilewidth + 32 + >>> print layer.weather + 'sunny' + + +Tiles are the exception here, and must be accessed through "getTileProperties" +and are regular Python dictionaries: + + >>> tile = tiledmap.getTileProperties(x, y, layer) + >>> tile["name"] + 'CobbleStone' + +""" + +from itertools import chain + + +# internal flags +FLIP_X = 1 +FLIP_Y = 2 + + +# Tiled gid flags +GID_FLIP_X = 1<<31 +GID_FLIP_Y = 1<<30 + + +class TiledElement(object): + pass + +class TiledMap(TiledElement): + """ + not really useful unless "loaded" ie: don't instance directly. + see the pygame loader for inspiration + """ + + def __init__(self): + TiledElement.__init__(self) + self.layers = [] # list of all layer types (tile layers + object layers) + self.tilesets = [] # list of TiledTileset objects + self.tilelayers = [] # list of TiledLayer objects + self.objectgroups = [] # list of TiledObjectGroup objects + self.tile_properties = {} # dict of tiles that have additional metadata (properties) + self.filename = None + + # this is a work around to tiled's strange way of storing gid's + self.images = [0] + + # defaults from the TMX specification + self.version = 0.0 + self.orientation = None + self.width = 0 + self.height = 0 + self.tilewidth = 0 + self.tileheight = 0 + + def get_tile_image(self, x, y, layer): + """ + return the tile image for this location + x and y must be integers and are in tile coordinates, not pixel + + return value will be 0 if there is no tile with that location. + """ + + try: + gid = self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception, msg + + else: + try: + return self.images[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invaid GID: {3}/{4}.".format(x, y, layer, gid, len(self.images)) + raise Exception, msg + + def getTileGID(self, x, y, layer): + """ + return GID of a tile in this location + x and y must be integers and are in tile coordinates, not pixel + """ + + try: + return self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception, msg + + def getDrawOrder(self): + """ + return a list of objects in the order that they should be drawn + this will also exclude any layers that are not set to visible + + may be useful if you have objects and want to control rendering + from tiled + """ + + raise NotImplementedError + + def getTileImages(self, r, layer): + """ + return a group of tiles in an area + expects a pygame rect or rect-like list/tuple + + usefull if you don't want to repeatedly call get_tile_image + probably not the most effecient way of doing this, but oh well. + """ + + raise NotImplementedError + + def getObjects(self): + """ + Return iterator all of the objects associated with this map + """ + + return chain(*[ i.objects for i in self.objectgroups ]) + + def getTileProperties(self, x, y, layer): + """ + return the properties for the tile, if any + x and y must be integers and are in tile coordinates, not pixel + + returns a dict of there are properties, otherwise will be None + """ + + try: + gid = self.tilelayers[layer].data[y][x] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid.".format(x, y, layer) + raise Exception, msg + + else: + try: + return self.tile_properties[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invaid GID: {3}/{4}.".format(x, y, layer, gid, len(self.images)) + raise Exception, msg + + def getTilePropertiesByGID(self, gid): + try: + return self.tile_properties[gid] + except KeyError: + return None + +# the following classes get their attributes filled in with the loader + +class TiledTileset(TiledElement): + def __init__(self): + TiledElement.__init__(self) + + # defaults from the specification + self.firstgid = 0 + self.lastgid = 0 + self.name = None + self.tilewidth = 0 + self.tileheight = 0 + self.spacing = 0 + self.margin = 0 + +class TiledLayer(TiledElement): + def __init__(self): + TiledElement.__init__(self) + self.data = None + + # defaults from the specification + self.name = None + self.opacity = 1.0 + self.visible = 1 + +class TiledObjectGroup(TiledElement): + def __init__(self): + TiledElement.__init__(self) + self.objects = [] + + # defaults from the specification + self.name = None + +class TiledObject(TiledElement): + __slots__ = ['name', 'type', 'x', 'y', 'width', 'height', 'gid'] + + def __init__(self): + TiledElement.__init__(self) + + # defaults from the specification + self.name = None + self.type = None + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.gid = 0 + + +def load_tmx(filename): + """ + Utility function to parse a Tiled TMX and return a usable object. + Images will not be loaded, so probably not useful to call this directly + + See the load_pygame func for an idea of what to do if you want to extend + this further. + """ + + from xml.dom.minidom import parse + from itertools import tee, islice, izip, chain, imap + from collections import defaultdict + from struct import unpack + import array, os + + # used to change the unicode string returned from minidom to + # proper python variable types. + types = { + "version": float, + "orientation": str, + "width": int, + "height": int, + "tilewidth": int, + "tileheight": int, + "firstgid": int, + "source": str, + "name": str, + "spacing": int, + "margin": int, + "source": str, + "trans": str, + "id": int, + "opacity": float, + "visible": bool, + "encoding": str, + "compression": str, + "gid": int, + "type": str, + "x": int, + "y": int, + "value": str, + } + + def pairwise(iterable): + # return a list as a sequence of pairs + a, b = tee(iterable) + next(b, None) + return izip(a, b) + + def group(l, n): + # return a list as a sequence of n tuples + return izip(*[islice(l, i, None, n) for i in xrange(n)]) + + def parse_properties(node): + """ + parse a node and return a dict that represents a tiled "property" + """ + + d = {} + + for child in node.childNodes: + if child.nodeName == "properties": + for subnode in child.getElementsByTagName("property"): + # the "properties" from tiled's tmx have an annoying + # quality that "name" and "value" is included as part of it. + # so we mangle it to get that stuff out. + d.update(dict(pairwise([ str(i.value) for i in subnode.attributes.values() ]))) + + return d + + def get_properties(node): + """ + parses a node and returns a dict that contains the data from the node's + attributes and any data from "property" elements as well. + """ + + d = {} + + # get tag attributes + d.update(get_attributes(node)) + + # get vlues of the properties element, if any + d.update(parse_properties(node)) + + return d + + def set_properties(obj, node): + """ + read the xml attributes and tiled "properties" from a xml node and fill in + the values into an object's dictionary + """ + + [ setattr(obj, k, v) for k,v in get_properties(node).items() ] + + def get_attributes(node): + """ + get the attributes from a node and fix them to the correct type + """ + + d = defaultdict(lambda:None) + + for k, v in node.attributes.items(): + k = str(k) + d[k] = types[k](v) + + return d + + def decode_gid(raw_gid): + # gid's are encoded with extra information + # as of 0.7.0 it determines if the tile should be flipped when rendered + + flags = 0 + if raw_gid & GID_FLIP_X == GID_FLIP_X: flags += FLIP_X + if raw_gid & GID_FLIP_Y == GID_FLIP_Y: flags += FLIP_Y + gid = raw_gid & ~(GID_FLIP_X | GID_FLIP_Y) + + return gid, flags + + + def parse_map(node): + """ + parse a map node from a tiled tmx file + return a tiledmap + """ + + tiledmap = TiledMap() + tiledmap.filename = filename + set_properties(tiledmap, map_node) + + for node in map_node.getElementsByTagName("tileset"): + t, tiles = parse_tileset(node) + tiledmap.tilesets.append(t) + tiledmap.tile_properties.update(tiles) + + for node in dom.getElementsByTagName("layer"): + l = parse_layer(tiledmap.tilesets, node) + tiledmap.tilelayers.append(l) + tiledmap.layers.append(l) + + for node in dom.getElementsByTagName("objectgroup"): + o = parse_objectgroup(node) + tiledmap.objectgroups.append(o) + tiledmap.layers.append(o) + + return tiledmap + + + def parse_tileset(node, firstgid=None): + """ + parse a tileset element and return a tileset object and properties for tiles as a dict + """ + + tileset = TiledTileset() + set_properties(tileset, node) + tiles = {} + + if firstgid != None: + tileset.firstgid = firstgid + + # since tile objects probably don't have a lot of metadata, + # we store it seperately from the class itself + for child in node.childNodes: + if child.nodeName == "tile": + p = get_properties(child) + gid = p["id"] + tileset.firstgid + del p["id"] + tiles[gid] = p + + # check for tiled "external tilesets" + if hasattr(tileset, "source"): + if tileset.source[-4:].lower() == ".tsx": + try: + # we need to mangle the path some because tiled stores relative paths + path = os.path.join(os.path.dirname(filename), tileset.source) + tsx = parse(path) + except IOError: + raise IOError, "Cannot load external tileset: " + path + + tileset_node = tsx.getElementsByTagName("tileset")[0] + tileset, tiles = parse_tileset(tileset_node, tileset.firstgid) + else: + raise Exception, "Found external tileset, but cannot handle type: " + tileset.source + + # if we have an "image" tag, process it here + try: + image_node = node.getElementsByTagName("image")[0] + except IndexError: + print "cannot find associated image" + else: + attr = get_attributes(image_node) + tileset.source = attr["source"] + tileset.trans = attr["trans"] + + # calculate the number of tiles in this tileset + x, r = divmod(attr["width"], tileset.tilewidth) + y, r = divmod(attr["height"], tileset.tileheight) + + tileset.lastgid = tileset.firstgid + x + y + + return tileset, tiles + + + def parse_layer(tilesets, node): + """ + parse a layer element and return a layer object + + tilesets is required since we need to mangle gid's here + """ + + layer = TiledLayer() + layer.data = [] + layer.flipped_tiles = [] + set_properties(layer, node) + + data = None + next_gid = None + + data_node = node.getElementsByTagName("data")[0] + attr = get_attributes(data_node) + + if attr["encoding"] == "base64": + from base64 import decodestring + data = decodestring(data_node.lastChild.nodeValue) + + elif attr["encoding"] == "csv": + next_gid = imap(int, "".join([line.strip() for line in data_node.lastChild.nodeValue]).split(",")) + + elif not attr["encoding"] == None: + raise Exception, "TMX encoding type: " + str(attr["encoding"]) + " is not supported." + + if attr["compression"] == "gzip": + from StringIO import StringIO + import gzip + with gzip.GzipFile(fileobj=StringIO(data)) as fh: + data = fh.read() + + if attr["compression"] == "zlib": + try: + import zlib + except: + raise Exception, "Cannot import zlib. Make sure modules and libraries are installed." + + data = zlib.decompress(data) + + elif not attr["compression"] == None: + raise Exception, "TMX compression type: " + str(attr["compression"]) + " is not supported." + + # if data is None, then it was not decoded or decompressed, so + # we assume here that it is going to be a bunch of tile elements + if attr["encoding"] == next_gid == None: + def get_children(parent): + for child in parent.getElementsByTagName("tile"): + yield int(child.getAttribute("gid")) + + next_gid = get_children(data_node) + + elif not data == None: + # cast the data as a list of 32-bit integers + next_gid = imap(lambda i: unpack("