From cf687846d858d47716aafa1615bbf7368eaf4ca5 Mon Sep 17 00:00:00 2001
From: Sam Wenke
-
Extraterrestrial Marauders and Aperture ( Extraterrestrial Maraudersenv.render()
)
7Ey5fR(ke|4%_YvpS=rXs3#8tYh;|17L_zI3B&J%?a zK9^qC?-%>yvA>)HE4&14>CN+Hkbnr0)rP?Q1>;!ojp(h1bzJPjo(SI81Osv?|9ei# zaWQy7)!l$M571iRfMH244X)eB76ozHj1Y1FqXTgmmLq64EUO_U&7CrXbsxGV;W;$& z!`@106lDi&Kr{=CD}3yCEG+Q2+FKWhcnU5oxP&t?u|quNG@rsCb-%ts2d%c810;Pe z*xWEcP>E3rSp#4`onz?JbaLygm|rHwu3ACX;f+{d8ZTH_OpY9?S!6EpvzSlb4&z#3 z{s>=~2~vWzK2=)EUS4SdB)(rFc}jv=NN|igkwLCK1e2I=!;;x~Wy?_+O=U$7!XbxZ5zyO_(ub^7JYkK2CO^tIhE za#l9-MUAD+=j0p~F8gQ5)g8xk=~cDgy*RAhACG3LyI&wW80sNemaKX>Nh@x;vs>UR z`W@*ka@)|e8yV%U&vf{tu;9ta+`PPSnH8)l2Vq7i>=W#S!$!yK#vwa-(A=2kJK`qa z27daFa(x-N=iz+?bC`iZ)Rdl$os4K;7uV=#MY{Wp*oZ@29$LG2Di}GjaKm2LvtkfE z`RH)BQNh-2tWBpd%~`+6&Z0$u){N*lap|;XQ(Y BDpz)nfr#i=s zh#H;n GxVd9QiuP0WjS5Z~VscoR(DjV@gu{`l*7Tszg`L*VbrBkx-H zor?!&p5J0JE&$wEu c?~dnE!^o(R3s`ptUk{oF!Nt?3VTq>s>0)JMsj8R~sNMrUd# z!aYq9xB`y}(_hd5e^u@JQYRa2;vs-XL*DlZLO#mfqK}wuX+#wN4muiA9IIC;Nb>0B zBV7jCPL&G*n`j^RS#3ta2BTQ#td#N+6F-gSYIt@aa}rbv8J)B4xY9?vWMY;zI{i`! z9eLrDF2w>S%Q=Z}aXBgD0fH= 3=^4?cFc`Uc`2`pWiF5$? z%! 9ck_8>Za=LpUD_Bs>2vv9-m~Gp52U_ zH>_(|9$8&0-8zlkG1xme>^wd>JG;=ox^BI_>v?zt)J7flH%4c(c%NQ`_HxW*6biti zLGbi5W`FgkAa+v#742~&6fxp2z($gQY@4v(g3wcA4Msy3j0zi6W5@u>43JUgs_|<+ zU%t5S;2Pb9SE#ep8lI>f#o$4N8zGRvu;5a{t1=i2radl=Igt{E;L)%{tkyCYwNd{V z `9zdeE1gKP4vD@VH@|C`a%{@McUhIEF;cdji1 zk924+qvzedA%el9yp9IF^{GjLXkJeV$0MVJ`XCg~X6)OYaf4<2)}OcctJV2xNU0s} zqMMC5G7;)dKO4S_1#f^}@s |5rXlGbcsR~GssBPP!jZ{sDO$(OA#z}BRLoh_#pw{n8LE-d@VD_ z`#yRdBL--KRnDYwK+4hGXeCZlp@dbb(1JvGUO-Til8vi|uGUAWBTv;KP #-)=NM^QYjLV#P$AIz2bIeQ(v|MqSU3&mA>WZHpTN zlk#AAtkZ};c Jmt{N<*+1)1@He3u zWz)&E=bY)*%3AFwJfU>!kvq>inoBhnq=tUb@AZs#Xhw;L<8?+3#QDw9QpDEAi5zL} z(1IK~{LB9E_cwgOqd4oSm$xt3P*pE;`SDf}r@8rAKZK+2v0F3a-)`E@Vy3E^PnwSf zZ>=lKH{3b0KJ;>3b|7^tUjWXt@V9YA7=Ar0sCYBp6LC*#mk~Qf_k7>+jvfsf10oIF zw&UbhMUbKsdX#oE06XBlJCIwgA3L>aJ-oA|zbG=cWM?+aFhZzZLVrNrad{}FxyZfn z$$%rYJP1xdupRbg@Yf;KvcY;dU@$@(*Bmsk$@$n8?n4xj2!jX?iRLE`f3U9ob7Qd- zRk}uwQIe=ZxTFhb>&8F8KO6QtRPFB;8z;VlbY!KGeh_FjxTjqfLvMwzWfI93H-H#A z9t2IDH0<$3RRb>TgyK!-GkLJY1`*2V#6jk#!%u2Ad(f33Jg7~!;c^GW=m#daj6e7y zC|&k~XDIuLvPzw(8RI|5r3hPh+WPt5^KVg~v{6sR_}zsYAd!pV!at$-v$K%gu!;o( znd)LB7x45%TOp We9~lT@u?{j z5?r@AQl?!joIVBo#&z~%U~=`M_W@%+rsia<+Dvlv0VXJIz(halX)<_26MQfbzaozV zGka^V5M;a@j>VfI9bRRxNHMBBc7y{ZS>l+OW}G~9B`YzV<+Ow@_PpgpLJ?c$u+(X+ z0`LNz==0*KOy?{D+^SW{*@y}ErN3gH**-dolBq(wZ`g3aI17E|8Dp7Q-g*jUxw<2? ziVQk 2#QDx=y6JR{)F8SV)4QmtyDsR4`JB9y69eF)w~6YcBVnNW4TrqYEm|N4r! zgAFa@H#&2&^*{;H2P)EUl;ChWJ`m4O&0U?9Ht)B04-?ojB31PoRkM)8^}Tf_vdW=Y zV)IP>dmH7-WR+9%xF#7?L%YW_z00t~)?Jzk;89}M)sh&j1FN~^d=Li!gADOEc0&f+ z2Fv;fyAehG3%k+A(u%#4aF*K~8&@7OAA{gw?5VJ$Qz8cWbbe&blw1dvt5-8+I>583 zM(G^HNzhxDt~8{XSrz*v*8G&$<=TAy3EO$K-{9uLhbd?;4AOV@4L9(L@&-g$`3H$$ zImV@ai_T#6Nz+J36auB>6o1YvvnY}G4@@i!t8Abs&&+P*$;+?fsc&;>&S>qW?Wipp z8tRW89JkiWFBqF?oQ#@YBCnoL?pe=S@mkv@+3Z|A+S@ljJhzz_&_B0^ge3kRd1-ci z k0!te1MNL5vwN`#rF_A3L(9R3P_{zZVJem$pFt0wZ0lDt%n)45wM&iU5g)(D_~dX zHEF9a3oTJ7#@vK~GFQ&fXyrOQb%a{U)msl^ G8&_N`HF;xb7dDWODjGdf 9MrNeF6d;M39yx@xH9-HuDd5d&y8*-fxtJgA(S0&hmN6bkq0p#!keSmVR10{ zW+Q0;53mJ>->(}5nCn=~cd{06=faK=RP5N4>+kB~f)z3!!igSQ%tVPD=2AlW4T?5O zk0>Hz)C4WkssywPM8D$9@@sp3R-?gx5&^~%oek-`Qozg~#Gorv9>!v^J8#?ZHR zsF{tf1b#~@IWSWeHqnYo1g+x4HCBwOq!>l0Z9>^#6Cc}ZJ{cIxx+LWq%X{?Y?I+p8 zP|xR7V~7vs=Iy2&X3u_7M=roEIzAFtQ@6npQCL$cq*tLEMn(PFzy%djB_vxGblhmV z !+87IU|_4Fa_On zlSYmDhvqoOUij@rMrx#U)Y^fkDh1mXAP3tm70%mw?J!n4njH;E{Kj<;W-;yDa85=< z*f>Kn!eZMOYDpG$MswnOD(?Gvd-80ju}FOWW|jt0p<{R9IiB^3WGiV2aL0wSEHy`T z6^?pV5~=eIHYRxq;l>qxV+AKm 7Mqz}j`vJ143 +4CpY$QaG;; zV`Vz>8y{9u5$%YSBB=5S*Xu-toKP{>I11H_|v>HwMh8EllTEgCCCN#~{lp?7=| zL26c7zn%*j_u2e4&FYefnaE*dQzZ?h<5Cf9k63b7Kbp?vB`-Jxu;Hal+FGDttXv#n zce#eygs~tVKm~r9b`5ke)62Z5@Z)9ypLW^O$s(H(!8tRX#@@025O)Cn &u$ zP02$Z^fz${4zB5^4vM0J)Kcu 3vp;3V zx)hox<>{316vvcStK^0Y7c@}R2GqAnHhHCXQ+N1x4RH5b_bWk8e}?oMG8q}=g27Bq zsZth)*=dblFkV`w9@lJ yYY2I`#${6O?_C}hh(uuM&ADm|VO^_M)o>DQ#2klm>-w;P;%TO9bSaAEC7b>^t zLy=-UL+E$g09YH8{F)3*`y>1>`0OMzSE=U9q mAVUI=XY#dYp6s$7oo!!+I(d k)E|$O>gcs4Yy%mcaQPaSbD!_zgdB|IcNJKs}~TuWE&n6`u-Gx)%7kI z9G3O}f=Jot0c{3a3qVf-)ARhUYpmyvtA3=*OB4676aG9{vJ!E;pc)%;wPTVK<$dcC z8O^duS>Pl%XK27Iidk?FNAV*$FAOt?lPnP;&*d;gtuBNpIoS2ZFaa~v7L PFHP`r~*J zZkbT41(`~KG#RxXA2c>@Kf?ei?naDgFGhx7^CjyFX|pfxgbBPscHJ!98733Nv^)3H zd>`l@W(0x1?(@O6E@&W{4>Rm0S*bfw=Mbpz{u)iYz);@P7U?}{Id*hZRZO$UjY2Wb zl2pmmSHjEcJ*)LA`|GxlQgF0v`iSC;DligxikK;XmBNzjGnGY7dMt^<{%6dk1p|SR zzdymCZ3zE0=GNs{jF(x__S B{7t= _FcADh_#wSgB(>$~bA2CESLzPD_9$L;U)$Ad+ppWG0y4`C6BA3Gr3q%S103(VJ%< zH!nqBAOFC>;E*8iaF4JkzsOjcm;~aa Ju`>Th@cI8J_toq5K^ $Z z`_)7ah stQj9X!1aY<=_={wm50s=wx4fS mo8mN&`Nbiv$q!T{#-(%6LE#%A8mMa*8!!O+nf z)alvfGvNC6Zv5eC^R97ySSIO zRPHKuk>~)l(dqQxe1s0=u+bYd=J^qse90I#oP_CLVWGx&q?874cNOt$zX6{whDK;` z&%_o}D<3ailjFEI-`a4|dWYp;APJ;}CD;Nuu2Klc(`Z+1#9np|5KG4Jb+$3U4kv?h zR_ooHkDC{5fPr|^F4rd_z62dd9 |C#Jjt}r-9Ep*jI@z1EG{| z#l%L~aVA<3@KYyiAt++1&=H()&jdl%i!1rzQedD0kFQ}PdA=O siKWi9 zQX%J6nayP^cmDWVQ70{E&fqBSYhDF$IhIq&`xB0qw=Zta9DN9XT(-P14=%K9stB6y zt$N@|_EKteESU~zy={Tcb^8kWs$%&g&1GwEQMpAcdrf(H`&JdrHQz&=`E}<`#4AAu z+f90O_qYHNW8KT|_Um2-vTrwN2w%+4vfxE<7YEXmEoxOiLQ3|NGzQY%jgTf=+>JV; zUfjY;e+~qLz}a75LN&l-^qW*J;JHT nm;pR%QcPRhP|wVvj5XSp4gBhi0Sq$Z)~K39U> `@^4 zxsnq_&zTZHVcpU;81MD-*i8Ga>Uv0_n_Irq_{-k*5h)F_6smxI0cytW}$zMjXw|3(qCk&`)T1Cam>=ILF^JzYd zrK7LiCACZx(mSi+Nc71in0Dxn=$MYBUQoa=k{rP`g7+bI=^#Oe{9nqwxI*}UDfemj z87Gp(T+5?VPHp#@Hxb1=$70iNYxi)KEyW*gk-?Hq71#uk`TiDI5a0WjFqqw+a{sU9 zS)5yCVV?6oJ(C)2yL_D0-@E5SBNcep_j?D3Q7poOhE14S9oTc{C=*Sg_3DQ#m*IJ@ ze8jI@p6Ie&PuVIFz2^7qus)k-@7^=I%X>0=`%w7F1uz8#lZ6`TMTHm&hciaT%OvWW zD5a5QWZGoskZ_f7#1)ayS22`VRHW9$H#7z}x45>on|F3;_Vh~k57Z9Z4UIY%PIOOA z_sz}?BA6{I1D2I(=Xh7NO7{;A_jjaWV)8BwC3acQF7NM8r0xWde!Qw8Kk+{=&6*!y z4}bQBTt%dUwT9^mKn|z<`QboUFa!+|LeQmF8Y<|N(+T$>mKZ6V *xk!s4;XpVU@6R@h%`C{38@yA}plI&8|dNa#1%kt%F z>2^Q7b|u=?dilvb&GAOidb2WoZ(zC8)oPpRh1=k(bhUDq*La)b8pn-dPf#tfbcU17 zd|yO1eH85~(_w`RgUDn>t6lt*H=Tdbm}`S1YY~pObf+5Y`MiymyEHL(#rblfRJvwQ zd-L^n&%2uc`+Bh)3Kn@`NwvWT_IUaNy29Z!#ewp?46QvY+KnA$Bev~D+NYiGvUkE> z;98>d`}NguKL_PC&vp)x36#)|ccTzb3yEcwZa(6A=6k1=ErCUBu-S^@F0N%=Uc^ z< FR CnR{`o5TXCD&HP@n5q2_heIeK)jA `j(KxJj*rsgLB5VDg?O49)D)io^amX-*B z2HV;;79M2&7Ok%49tnrOfkBkv5uu^(i7!+AGoR-cc$bzt$EMfDHs-gC_q5xOhIZJ` z&Mz*n&JY6u|7+&^GkStShY=EC>-{k~W{IU&Fvikac1$&9?o~(KbrDnoX~DyXh?r9~ z@H58>Y2hr1H3!NisMnJS-%XAPSVko8CPzG`^p%^2cJ%0JA;;&;7~|_#b8jCzEf;?i zcjREpK&Y_b(D2xp$Y>wK#NZS )enaBG*#F7-zdbz_*bbJ<2b5@H@8-;RPtU_;lqjabyQha} zcpy)-Qayf!7992T-JE%{AWo0dXo3ZtcnX`mF?YLCZ(hA_@e;kRzHrudbq_H13MMo6 zv6SVO4~zkX#A|4VTSPKP#|9*Xg!v} <1#`Ms3DTlLx0~4ChR~$!LcyE!(ZCw(Dxh982i$jP0pnyd&mY zAO`7y_Sw16rRblTkscOTRF)71{^`nbv^W~68}Bz5<~uK}BM^vy1LF#NtIZb%MTaWe zBD>EU#YFt(1!an?OBO*Mnj?7{d&n2NaAgp&s!xuP$gUALvT8&jlg;1*GblD^B3VG6 zux0=ke=J-=Td-E~{Vq!(UbTJ+LEPFzp mraam@tkytCFr_C2ch@mU4s>IlTW9F%MPt)DfwiS?6DtE!rOj 4HA0RShfBm2M z3iF>L(*K@o?^;Gd3F;qwl|X9#-ly;<8?V-R7O#Qmh@L`ya>?J0(xz}&+wuXWi};S% zhg|W<70!UDR1L9&wuEo3+X3tz^x56#PZ+!mYxlogeVqIpls#+$LV|*=LY?7hAm3q0 zqHVajIST{EA0HYT3n#F!sJIvl0|8J_Qe9LIphv-|sj1^^YA$KzXz!@4XzMNRV(%Fo zZXE2Z?4B$fAMKr=1uo5wFR%1-Yyd0hMsg0bD_9T_0cV$2*N7E=CFvjBm;xs7PcHuZ zd;OPD`d6OIi@>{4Iws&>{#iAq!^TnJs#OcYobjg%gc~TQ~iPqXk=pvL18lxD`Y1 zjkUTb8`?$Qr_Qr0>wO<7H+K{#7bt&suW&KnDArJiAd>*)h-9JYG{#s1 dqeR+t~W0ORQYkyI83=LMXX}Oq+}`a zu? 3pX(Zg6^dn5)>6b9&mzi&>WL4<2hLT( z1#5+&zp}@#f^?k264#o>H9)q8Q-&JLSDHXqxvO7sV49oVj0lQ0X||VP;4vf;S&})` z+5@yRMHm@#`Qg4$bo#$LR{T!qOBlX=JI~KO4?v@(0B!dDaZHew%+l?G TcLpA|AesJ9~L2f%PeZW-}B5bXJph2OdRaZHeU8BZV$Ph93&v0?%aH#-oYxj zmgwquJ-nIM*RJmDq&YkZY$ou$NDU^yxc$UNP^b3zq5|-F0l;cHd_;vH^7|p>@-5K) z8;IXeh;Bs5ABLG?*X38DYM>(!VMNFG`>NlLc~vVXam3rM+&4(>yOaHBzBNva(C?#7 zu(6-YbVxa=Fk1__IC(=ppuj7d9ww?{2N4It@B$3u#lm(BzvzgU9;BcOneHZ-gee}S z88^Acr{UF6QOG*X^y<7zF@@Gq-9&W~ZoCI|n! z$5XtAap`}Baa6u;mWf%+zy1D}%Vu7Eve$2XBXs=#vWcp7e9CUfSr`sitzNmX>Ai64 zo~cQJ8cnP2Q+e^&^4hl9cloGUUu&BD%gb^2P0PWL-QC#Sl`hc3%O_IIKZZHjB3Q}8 zBqAzJFgDXIImIkTIfMo9Exk0WoFptEDW{ORxU{UIyeg ifeqwoLHD chCCZaA1A_W$DuQ+V5zP@&1AR z;;C(C92h-}PW1>uk0mOy&4EXq0j5|Iu!|dvKp`)vXNxf8i_I~13OXX`iXr~=7F(f! zOj?$Jp~B1b^3hN{l|>`V0}xO0UXMms(O=L?rqX!aqiC@YOJoa~Qm0_k5X}@znM!!; zf54WL7Av*l-~L$C7%EU6HG<~W^I31U*&oY%P4Zc(H4{qp1h1{uMsrR+GTgn<3G5F+ zqtTiG-3IhX!BB)bANw7Sr?8lBS$z5$2qQT2lrYif(?6D>P~P?P(}U(5spN1vs&1p@ zRAn%AfaV@+I$X1V{kzmiXpK_Wi4_L+&E;kzPZysehG*&SXv*@dw`T`e_4%r`HIRGL zo$F>-CHh!>tC{uabP+AutG?Zx=VrfHD|?Ce(BS2?8OvL?@%y&>3Lph5!B-rsWWyTn z<46zH1IppHzp>!RY5=@qORRtG%+g8-DfLipFuL!^I4WYp$zC|)5aoV2h23zvt1}-M zmMd_mj>P?Q)w8}Ye^I|+uqb&cY1mg`r~I!{?5YM)a#8GLWJ==Bg+yvg-wk*))t?JF zbiG;(nGK6vidaprn#l1jvA>gJ+pgv3#X5%BQzW`-)l-oD9jFBFflBl~j)FnE6Y9P% zreF)cFQ&kC+R%Qrti+Zrxc0DKZ^?@JUJf1HuXi5uK&3KK6!p|`Sio(j fnR%S?G z4yBI|#(N(rLHn!zZ@*J*;O{?R(2zNQYxn=ZzkhyTtjPnD{a3&Ex2FGJiS{2qHi!lJ z|MFx1Mb8BW1?Wy%6{Fb@c}Q2UUqAC`@{~Tm`S_p-^8w)9-p{Z5Zn%D@=VGvc0bhc` zQq$aZBIB}SFp_vv(u+bPqH?nH_zR0`oJ-3q@;_JCw%Te#W;MiC1_rkc=yaBKNB1W8 z53~+-jRMCDCZ}pg6BUTiI&JNb;6>N3%+am`@*oOwz3tfA_!2?l%L-G5JXXx zQsSxPs?e9_w#QSLzixrlSbW7Y@Qs )1PMuyfJ3&}uRu74>-B&Ey#Xa;r9g^pGz*=b z$k#&?jW#cGz7)yUIz!m-`!vrB2>He6ROivp>o2p%uqj`GQLZZi$J3=O)zYhN1Lrll zo>p5D-=CH?>g|)dY`C8GcKF@XgAhHMbq@zp%bljM-ENQaKQ-TMV!G?EEESFyNptgD zovykfeZlK(2M(V#GrV1N!T&tY{FNi@>B{WA??SXJXGzLB!w2yJKPBBv;&gYpG^ z1XUB+?`xI~l5$CCfO2h3PcSy~^X51Dfaj^e(L|HHkZa%Ky)ZYM5bQw4p^!Zv)}@f( z2&kwWeL0f2q}^z3Hr35=LFOgm(68jBsj-Jdrv+BBdLw-a;%mox@d|gMDt~e9f6@m7 zjNv`}LjKLQ|My 6O-dp zRg+SzvNOy@)59}0^NMqe60#~wB+IH)!hnC}65@Mq%z^p*KXT)q@1NZ8#_9UAt8nKt z^uDX`K$QO9y9$5h#@|b4)$It+9a`C 7e}o4Y@Fc+_#SyLPd8bzGCVe0%P5`B-tYa&Vh}JdM#~ z1abL5<8qo)6OaJQyx=gcz~wlzN1Nd?qRSW}eJ8s*QX3;;{2K7a_s8!d0V^n) {^esF8O+@&iix;gOsy6sxJuT z9&Do4R;km9WH_+Q#TKF$7=q|Kp>p?e42?e=Q{YFXDP9RynO1oI x#xrfyo0(k5ye9L~IE%ADN?JS)s^Q=pD6E;8FJ`x03yKv2>qj#wc*n&wy1gk3;Y zlJWFor@9D+hE{!`&g`N#kqpPYd;;dkqH0Ds-SjQRb!ecx5l?D7K4NWYj813;4*RO5 zF101D_3KR7mDSnYXigPn%WGBsZ5{k#(|63o!?u{FaqI3DFNtfw)6;8Yb1 q2_@rfcI~>^8gWeqywT>*-h~FV#-W`sI%pJd&=q z3$N}g&c<2>X_phbXs`C`wEQX8u`G!!?#mhq>mPTVOIqwlV=PtfXDsni?T-`JTaMS+ z5^|iNV3}z*<$96rj;8^CtB#YAdIB%>eQ!Cho%OfhUN^T$U%sF}-uM0LQThB!^V&rz z9u?fnfdkUX8i5@|7>D_s{oo3xj}n4}{4~c3Zv-nKHNOLaxbq =9 zP>f{V%t^AzX?oL|Q^~45^`u2X%CP&|CBfr5|I47Ouc1c(mzT7UUzkc@Bw4UjXn4G6 zRMHoI9Y^7WO!nlcn6GhJg$y~7dAtQhm2M>gWuL1WifcUTcxd4}I=i}idi(kstDEh- zV8+HLCSd@uu!D`m?q0|%t844X(8ODd6ob3A4lwfCsb?t22b}X)#z!aTkC(T53(p$& z{HKo>Pmw+pH)3C$z!o>rL~C^7z23m+H U!DIV&V$sY*n0{Z4Ilem$0o0(`|LHAr*>&ZW_1HXDX%Z z`ZoG|(2^V%JA-66`FI` >iM zHSF|4$~r5TMLAL|d$j_-O942WBBnE_IQTr$>@i0X|GuYxT4(KlL!|#~(nb7X-SUX8 zE%yFl-DrQijF5u&z28&hLd67C&^ttOSoag`+NnuNuP~Z6A$0Y;3)|Ltn#!osKYS+h z$PS*p^!e&(VdbvxPv`9$&KqK273eM(MH3d`o1hUBq#d71nUw65EuWr|RX~=TXId&& z6jV~Bs9cd!&s`JOLR#LW-oeynQ&->Bt2dXx3exF;2!cl@5f2FI zh3Iuz;oJQxDo6wu^pPU0L-tm$K7wN4jp)Gz;-EhkRYIA@ HKb#nZ+ zyUBYIry|%b4Z1U|OPX|Dlcna|rQDnN+S&Q0F}Sw3HlvSP-5zv?pUepRpa3KBXy-q% z@~)4O-y6@mTkG-Bw5LdNm8<=u`9!TT(pcx~{ 9B%EmoRVutIj*p*Hbfu9I+82b+k_WUa0aqD(aI0iCKz~hE^5|OlNCzolCV5J|*FF zT%)4Maz!OSH1z!8N|EC`^mt4kh|NWr7i#B7$>hL3Hk-dJNKGXjF5q@rkgD8vDxO(n zd|Eswo2K>GOvnI+3|9D`Vr80TSV#IVNCGf>AG^?G58n6)2Wn`t6dMZcOf9$^J98E| zJlJE$E1C>1-CP$;N!5QH)vP `6A> @A9p2L3KU9TXG-^CWWixn7pE=^3NE7h*A0xBNg@o*;_#q#PIY7qD2?V`3UF^H{Y4CMMaPb=C}UXDaTrvmnPOq zYgjgqF5@Aa50@F1S1Odvsh+mqR%}N9Nid^t6g$yErM^!PB&sv}VU0zdq>YY$mM^f| z2NJ$ZmOo?H+V%0O5~M)32LFe`ir5v ~dK{)-pE@0-VmMdNhGNpR5~r^HGl z6h<%TGYSD{OhL-s0CZ(WVWeQ!driF;LlHj6cRG151d(zE44z^tCeJ&4w0VNyZ(NT1 zUL7g^;dkG4$bb81EXMzcL^0;=FzD|h(RPpAl*k3Nf4|->tR%I!Wcqz;xsWXv^|YIz zROv<)V|ir2AZgtb?Qh;y&1O`uMBu61D=Hl=&I|D;R)0nhYEqy7h09Y?6CpF+`CUko zoP0W}A__ohaj^hKRdr2mU40Fcz`OsX71+_)(c0eC*9L0kY3}VG8}Dud4*RxD%rDH4 zVl1qVFTt#=ZFg>L?QHKI>>M4hou18OUR;h}b64J8-8}W(qmTeFMqk`I=VxWGYxSOx z)g}0P1)$-(XhkzbYxRUIK2F}iC*=4JM-zSx!#$Pb_mBGD*M#()t@yXGNe%n`pJ)V~ zw#mLb8y~hv{#=uN&P?kO$gO~Z+}l9OrzFPWX&5@T!UZ$wwQ)0mO0f!#bX2+F9#$HD zRr2Mj^ZzODEyJSv`+iYrhLVs*L_iurR2mUb8l Cpke3^>F)0C4#n6D z{Xfs`{p@|8eXi@gJQwfg4Kr)j`u@J3oPPEp(B*bhSK{I?Z=Zkg%s NmeHOH^ZK^59IDb4+yYvDoF=K7*_R7Ok$2rKzH&jL2 zU&s4%*y{kZ;6(XYHw({b0Hc`1>nG()W!pri`@minctXlSfV5<08B@tsolucquimIz zo7u|JuG`$wrPvcuG{88d-Ip{fJpLtXns%IEGI1d;cerkCaU*agXq#tt b}N50NEBzLd*vW+5q;_mY4gQ&S|qxO?2ZWhKr5|cBA<&D79wbtT&s`= ziZYb_B~3T0_z@Impq=16tpveLdS?-lr6J1*N=tS>TL8L4h%& aL8g3jF^?};*_3kLj24XOM>1b2nZ6BvTQiuO=mRKk?JB9BqHqtNkDf3#wHHr|S z*bF(!%Iyh!Re{a}O$1hFmG5*{@vW`7Sm_T{{2})jr&W#7xAI7hbq*6&@5O|h3%0`c zrjvzvD4PUSXZkxrhcny+=1TZl3m*+N^NyU^$9TOH&irX`xM7pHY|qp){m69P$A9@3 zJ8FjhiciiH-b8O4SAZkL=U2;g Ia`=v8mmU`(m znSoS-oi47P)vi*aZ# <%6PDYV%O_*69DJCK z@0IEfC~#>S8WWl^u@e>2^T&6 zI$l_o!LpB8Ih6vlSK8ozSJd z7No%W{H8)+1uL!AQNI15?n^KdF5PumGRx@;o6dA?s;qY6y;d7f>TdFKCCXo`s=baG zVbC6yM_pXzG&P{-T=)ZQuztU##tL41JLXeLt(BtZIfATvh=JEe7BO9Kx(}?!A2AN@ zk15*`T_lTnDP0U}3a}GS=*n0=n`FQax1A8rU96bnfG#-Byzf{ YY9J+LrEgZY8 z#w{EgEVdsGF1@?{@p9ZAI(Rwvak#i 6 T0qe*{I@wk@ u9qfr =PT@KT zuc7cOBKV-|go1bNixj6ZHS2#+EUtihJe#-7i3ZWX_j|mAJ!cRn15|AFwnw3s1aCq( ziZP`^naHwUqu}y-Z^X!Q?{36OhK#lnyw0Kjz^8aIIURo^nY{^q_A;m=kxI!VJ@EsU z34@ObL_;fZnIIUPO2oyhif7+k2MHA3s9VUesosUgTlX>UWRcl`sP%#fv0&L6+*mO= zwrBLFIsQjZJNf$x^~}&oX9{N6dC(jqJm0u)4GQZrEVy6nSg;pW)MYkUq@H0`$Wgjh z$68wJ$hTJtx!`9gFF|iG!R=wVg+V&FQ-jKRBpU{PAqvIKi-r4CSthE-9a&AZHxkT` z>UJQ7M+s{sS!#Ea w@G60uv))}-M+>UBcMT~3*v&( HR$jx_&$0lH>SXG z$TRjX^ugtNsKn9ehM$;j)tH*{QRyayLCMX_?Npv6m#z53r(egAc8h7{P+ovff=e*S zEl}s((?0ju1umW*#gs?2HZ0+Z9*d#ly!(#%_n!3#;Eh}Fz4 &=~lK_uPEh-KIif(ccS;P=|SOL7GF(tjC5>c&KS08`J8@ILB$aQwb_Kt(~QOXB$ zI-*AtCzb)?xN1mtWOR8&Z@P7zbRN3cy*s{oxPDT5R(R2Sb(DD~G|yQ4sKVX9HJVUM%7Z6>O;bS3B5*yJVMYD@Lek>E+P?ESw4w^A zTCnCbEy*H }2&o3)=R+UaEB}<<+Yhtz_qf8R6v3Uu8ds(DX z7KDV7L9b3j&anwgcnqJ^zz8su_wLqo+^v9^ix+7j?+sibjM~sI4jR0DcmkH2M37oW zULvE;Whl2eM5jJ}z4WD6<6G;PHOPYL+)&U z;v C_ia3V-E})3}p?xz^KL7+tjQ)D~T=LJ(mK~pKZ6W zV7S)PH%lPj_OL5Z-DXI;+}+MhTijL2=&$-)8>9pJ^_~Idu6t;k|A;UtY%(#;$27zC zaz#>xt?s7ZTWJ(N^=#7oUa>`40G&hAbd&aXxt{n_GEQTv=AiEQfYE*{qrsC}s#x8I zW39SZx-I;_y!G7NJV4HY_+VdK|3K&9P}lGXtEgyqGqpI&ghb=mRNeH9tZePv#C(V* zEV`gjy*RW~E7+-;faD`QIiO5O-}pm^fe}fQcdxu6$>7lN;DaXdiOH#HaooB2g~d75 znbkGGv$?gsv%9yy&31Hr!gg?ead~xp^L1zC+wS)thxZC@8KbD$z-xH4Um(`mRRMTn zfAOOK33LbppyR=B^jM!I^1E&@rN}guO{%b+>swcq%QpXDY{N1W6aCLH!4L8pMt!Qa zv@(+3gfzev!S?B-1)O+Pr-VH_Wqj$nh~A?b-lrJj`%W%Dn?7C7vpLxLfF0TJ{qb>F z(Fuq?N9u=y@b6lx5=avj2=1QX9AaG7j2u{29t2)o2!a;p=9g8#O3SM=YwNQcn;I(X z>RVbno4UWWlaO__lMarMjZX}YlT^khkSr`NEj*~iy}Kgo3V&fPp?mE2P=LRCXqZ3- z!w*SB@h`eX2>7hTzg>s1G$Lfbolv3xz*2~TDFV}qe%wXxm^SmiB>q)U7ZsO_7MB-d zg{Uu;KU2+^Tgp$?VRWqqn-viRvk!)uHOwE>Xg>g)P>#;LUd!i~UbfC&zV7}mfSb}T z6pYP+au?7*H8EoV(Q~mA15T)HVlHl=0|f?$lR?Qya6$P62n2CiMP*$zh>d`xuD t667y_@3OAIP&AVqwvs%Zn) z6*p!!d&IQ&7knrx=}PG_Y8~kRGSXF%r$0H>HZxj29@V$pzPdJFxwT!hJG$R;cmzM0 z+sMDTgkCqE_1-jTeauR#cV9%svxmq~K@}?j_x82fID?fQy*$0-D%c-D^qvR)(ICHA zCgooRer50IvbFCsNG3@kAbyyH)N$H`+0fz4*L1aS{Lw|<<%)QT&cNy6%GqM<=^`N) zyGpF@V#VXl0?ei`)e7Cw>WQZ(a`ift$Zrt`c{wJCg7o$&=)9 f$>pkJzW)rN6ClwvOn&JevC(77(FEvaQG~d~0 zjlWZ=$#~#)b+}06uBN&0>Fk;bBh@LW`PTLN5F^%!Ur+nT*!9EsWlxPu(m}6GG|Z`A z-l%7Z=@JXGq!ecCVbl8W&?$WNjj;kgf&EF1d-?;S9eP&$UkBG>2L~tbAP3 `rVr|p^V@|f3nW{Vr+}f<~91)abJAMwxPc?@+F8#1x(nFCdlBNXMfnX zov$d#POBsdI2lC}sH{Ls=Af)P9H%=wBaq7jiXY_*dNZ(hFX0&(H>bN$Kb=koBWzzn z%>cCr6 QgfqGy<%-mXB?0RuG-mzGDCatJLqk zgf&Zx79q?oKES O$rDVSI-3!x*@bdGAOVKD_6p351 zQdR9WjzLf{N#WYYQfoG&*eK9Rf~$U9#y+EMVut$sTMUa9H#H?~s1TqPJ7o-wtfZ%UQtEy|N$Vh=_nhhKArnaMoxS2&T zljO^nu9h}}{(-uNrs1}+@#-m-k=Z%Yh1Qvs`R;YZ^4!|eX4S#Q(aG-7>h|H;^-TRH z2@cDS(qW%}c9w`|{ZDrFmE7$6@9(-9$jZU~SXf4;b3M;QeDC KVLKM_&G; zj{KJ|hpn&K8f(zrQp~0VA(crM6$EJaM-g>jaxmEVgL{}hf5m>4ImRZ^Td5m9$5iu; zeXYI61|cYz>tx0;|3hAB`T(;DFLc}A&0Sf?%N6YC=j9W~>=_c~;bdzO#bM?z8t?m_ zC;fQ}t67$bQEr|J#2_E8QCMUMD=ibMK!B=D%WL^d8lE*ZC)BsKSAL1^>K1%egffIW zG&(l?ss_$DBaA+Ynz6LZO?-Ez5#zjaPVyoJ{GEq)*KsF!|KQ}?5izb1wzJuao1wvS zv*)iVSqk;MtTyL!wj1S~h*jdi`-@VtwIhZdVaMD`2Rk;H;!(6n`7vzds~@`Jh&57B z;Wjza$*88)zI7FO{eglu{9jEuVG6YTPB~iE=Q~kOWI58|ST?(I*6N?%;D5>n` pbyKN-mw>Dkn=4(E~!dg$u~#RPAOFXOu%R_lND{(v#E !pp>JQS5{S&xX{ ?nT4)>%}4QN%F@cvE7>pMzqU(ECFK?&t<`Ub0lLKe(+WPNsy#V-!(+O zGca51BW9Q?)$a&-C{ta;nq{1yBZuw4t_8JP#tUcVVOp%`Ia-^ns4@L(33vo#XS45# z{Dv$@fZ1B~&c?Q1@~7IR_WKcF8%9O>9ouQmV=3?9%T&m>pQ9x6>n@7^W+d9OLhdA3 zZCItrHB4zGJb9Am5;WAMV^Oo@cznF&Ai?A*Bj*-q=oiB5?eo?*#3d*&G&DSdAj&T$ z)-^sPF(Sn?E!{HHFWV>ABOjs<^DJ;Eaw;iREO$bfRN2(j%GWoj*(Wtyx3=+i*fg5N z_w~CEni(f)g^rECn4IclT%{WgSg2dlHr{dG+-l!fQ9WUwKLVZK^d4UEeEq3?x$uJ$ zMxVt8#`GZDNGMwk^&h8__A8In3lAoH{Ir8t(TFpGP-qQ#S7C!S23PJ^ifHo48f5|u zy@#F#0U1nwy4+S|pAUVP2`bqbC|3@u&6Si zY7n2!I>Ley z3B{M7y_mg8g!Z^qF^%dITErXaw6{hg#hTSd`3#0GkUed7heO976x^jtaL(cH4iD7J zrrNETpIkMNm8};?^=kTQ)yc2-C*P;rlBq*P4@ZcFZdveSo3$0(M_74$J6o?#`8o)Z z+o)QcadkSsDkiB_S*^4bHg2V_5!xuOF-(&8_@=*woray3#=ILOYZEQ5ZU#KZ=R2dR zrQSNXZC^lhuL$N}{|x_zOSQ5*+#=#OdVPSp;yuy&X2pjeGiZg|M}VM=#uw`qroJyB zo}__);oG298w#tWaerz&G{c~e4w7qj3>h~WJ}g!VhM|;AnCp_9{(tS%e{|`) 8q`lRKz#a+u%`|lfyqezAg752D)!?02(h8gvR5hD>LvWx zbtTK&q)D=+bnDKV{68Wpzj|o8u-Q2V82JV(Tljcdg@Z#-{R87=Vv 1Lw9Sb2So7kN!^)>Mtqtp#Ae^B~OT{L@H~n8AalB)gAF`9is?sIC{gq4~^k^s4%v`<%HF@z$0; zRR-hHMjCN%$w*A+9=yfCp`lSbO0Kux-|L)D*wB(w><%6gxj{D|0*PM`-;$Q8sel+% zU^FfjoBgpEN tYjdZ38yY=4?pwba3sYic5 o_1v5Dvy-)!4Xr)5qo1x1ottO0aYP*5XJ4xT+u)a>VcPLgF}ZT_@rh1}seBok zZ{u?EDkLFsNnwF@MVUIK5#@-ys)!oDy0jO?wb>uq5FI|9@Iv0cu7RQ95tlK`?uN#I znc4Pvvqin-$*8!Ht-$?Snms1MBsZ=rhwDtBI?f&GG3#Ju ~iIaS10A*F >zeEhjP(h%WzQ6H8LX0C##G17A0?-e9+bvo}Y*1tH2EPXe)M=q0) zph>8@Z2WOvwd6^q+Ok=^ncAC$=E$8GwC|+WGHqI*^_OghrAnQvCRYwr54bpb%`jew z57c~K EttY%1n88*Hf%M=16-h9DbbLsles)ht#}Rn{|svy4vCHUzZV!iNd8qbl0Z( z#J%@9+`iu~Up%`W^H_4eMs_~F>>m2%Ifa%)t(H-)hT%KTOor#*#g|0wvrD#v3_kC? zUPhzX>RJ{4ddZ#R->C1baY!I2NniNsvA!8lnp1~G6n)6J;X_dJG)mqIU|yft;;^0BOHLyEz# zY$K;kc{Yc!WoKBD@EXM%oduTVb(X!56@(Q6jdOFHhT_~8FG$sHoTS;!S9}qomzrSc zx|_lzMBxj~G}Fw28Abu@^ZTK!y#k11&>nUvS3Pw&!uUN x*Lz1EnX&JqzKU)puzSlZ`@IpJ##uD< #N1RWZ8}wAVQhMJGJmXPG0=b5jJ)5mSKG>fGu*DsTV&mUL&<8xjW1*K zjE|hIv7CFJzu>gH&7O+0m*zHfzlSN2=nO*5L32LvV%h?57_NuGdxN4>DUdNMsqS|& z3`o5}qNQmX9eR`L-N*Iv*^e{0Jmp%5yi#G2#ial1hssl^xqyK7PLId|uH3KrQmnZQ zwOUg?T<7GYdT#N{R=WX@%h{MSS;uMJ21}u}lg`^+w| z25u?gAAkfNecNeT|8c$BectT6+F3&_cs6`X{o{b(E%nq!?Q`L#^J=l>rqg9FkU*s@ zKc3)5)n~8Uvke4?`&oV@N7MI-z|T!TW+?MK)NIV#mT3_J=N} XB zY)PZut*;GM&B6F-zDR^C2 P`_mWcL)V5GPSyS&DZ}r}HKhV_mQUtfo6R}aoq0VX5y12^ zcK3baDdiKS;uk6;tYsA({Wi=mKqMwfEY3GUFgf#Cs(ZRX7KA;=EsviC`;W(>vJ%P! zw|7Lpa~0nC2y38qc8>UWu0p^^IMU5nGj3*yF0UD}K!88R-ML~gGq-CX_9KvaUZVmFreI72Ly zhgO;mc0?yhL`zy4L-py5J~-}H6>Pp+0TM^1z{tt%DKSn+<>Pa_P#a~G&U$6<7;74C zESH4plEU@vTuLEA7Ru)Ua)5T{s7ToBun?=vh)VKAu-)z+^jAugdmipO7HQ|$56?No zUCvC!x-kx=?@d7_^F(jAzF~8nDER7Mw#yB0D;sn~mXtiSxT-Mfgp+9vNF>{7)u2#l z5cd^xLHY}xGdhURSJBTPo};65&3hjjwrU)3|IPjW!O4HNwclko`G4~IRARshq>#gC z`$z&ffygut+$D)@$Y1QmC=kT%ZoG{h|9BZPh*zVXdF_UC-6dM~H4wdE8dlwa6WPCf z8Pg~^ynpQBNr&(K4(w&=9LO8?G9-}WRb-$fNIuRWAs|UHMKw(!BP%^IS1Jb*pAQ#; z6>*i6a+X&xR#rW!simv0LZQuh+Rm!b(IEgrCkDR5A!4`Tv3{bSKGHj9sN4L~EIBEz z!1VI+;?@%JI?$syH+np`zd?L dU5#edgSbIz5D5`unW>WmHYJYN~@m<3B-H{ z&!Exc!IKnUUh(ysu=^ayZ0gb3fPF-Wqd=gSbF&rO2$R4SWJ^=;L(eE1OOj0xfhmw5 zEjyA08_ksnrq!yqry7GwI{nSPyaTvmq a(T38M%20 >q;9MOZ3ZH5N+vS;7#p4omI^PecdBn9bZN#TPmZX z1_limVX12A*_&BmtRUPyT!4i*4*P3w;Q-jCC%@UIl|O6~V9^8ECP-iV1#68$$+4hH zB#-C^;K}z8799b8_`s{*_*^4YI#OyA%c+yWH>%`0HVF~DvVspB03WoRPr%ph*7 m2OqD;VvrpXiUBHXrVo8k!|t8K2#l z*&3(|wjfwr$6>?XH$EmkWl0mdHOTpybEC>R{wAuk5FL| )(LqPhXVTKZSAj=`dgn(m(jonl(a0dln%{(BEajA3L(s%UFaQE)I8`v~_JMEhdwy zQY~M Q)$*ik8 $5il$u9`FDD&(-g=H{#B6l*WZ1xGSYSLcv*}{7vEOw$EpH#Y3 zh@^{kVHzKHl!X T#Xc;ogq2O@CW&%h2 zW({H@EsVC3@wz12Qy6mUda + z#J?=({|;B47D8(=fjb@F_P?RM+X&2D5>52+9kgeEnzmA=E}dXlXL9&*@G4~4`h!b^ z0HQ(YR>xAtecju@-uDHNFE Cg}OeTGtMKW)qM&66? z^unxS)ErY7H-w;qtDrEgB&*D%`bA|6XKh_XLuQk9J6D@kXJmIwuU7xF!4!a_^BYe} zfJ`!cnYEr@SXoY7eX_n`v}L~t4Ne?7+B~s4tJ=MsxxP7z=0n-XXc2r5n?Yfcq#@S~ z$~w^Z$$cosfY+;XR{-3NMXsmb2&4ZHiG n{vz1 zlAzC4e98nrnZ4@NDzRxP&=IJ78r-0C4%uw@QVMF7K>L;EHfjy}B1pXaVPNERIZkUe zN2f8dY?Fw%uVNw0&U=>+BBzXg 1`i`VW~ zXQV3}iNeb^9# (t4H)a^Cj(zH zV_29Um1^yRe~u6nEd%}8^kZKnNG yf=k>s6d%Sd^l=UWSkJmTV zRoTTd*4Crl=!5lQwFGBV0vV6E+ibcfV-^zoh)wC@(8d5jVgi9fQa)wOwm$!?`IS6D zGJ-QZ?c!POM7p>xoH5g5=qqE^?vJF+3_tEqCh4^F-xhNe=XRN_q7r6z@-m|NLh_PI z-tR&Z9p@&X<<=}_>M02ndvN>wy-;|O>Q(_)S(od5cxegn3oZyH<^7V`l>*tnj`IIB z{r}VJ|7xij{C!0JdrJ+{TYymoT576p$=%u>pZ3VINWEoHxOpHIImEJRHlSLkTudC6 z=h-WGOq9-DI&1bsfbfxc+vS2_WbRiZcV1RsbW|V1ry%SgAlPUa5&1qkMmjE@FEJ@N zG&MOLXsO{%$)(MQz;Z)SP$0#{1=-wQo*%fl?JV#`>P;F!%{6uH+ArHWWd(o(l3-v^ zyuS%>t@ca`P0zlXUl8p7vb@^nG&{2Vak%QBYFGppgnP<;C#qkciu|<%{IR+G-=f37 z67c`h=3*i_vu@R(gS86?viPol;*tJ<(FKS{MiSjQT>z0h89NvdWI_7BrCA{Q3;`mN z@8Xlo5>qoPtFrWRNZ<_+GLmBXQsB3os=I=-(TVdLVPwRua_t>So!!%Zz5RnjEs7K6 zQ#0$mBm)b1Bg-o(>ql<2=vzBYdkP1K#~Y{T7sI2nC-28=hGk0Z7Je2Jk%>^XX>Q_k zD?3LTbOcV*j>%wH2X}^&g_~Y{I_{MSd@S%Q{EftD{M$I@XrE}tXf+u|MkCR**~pK* z`aa+*ANaTeSqkN6{x;vhd!sqC&QlBdaWY`2!YX-OO>`U&ry9bRWqMLgg598#mNxZb zKe5s*{ET&f3VWeevF1wl2GOt6=&E`ql)A~WqU8#O6x4Te>bDWyYIdd&SIG5-GmBNp z5 S&d5rqS{9iQLgMdqi(c!Hc;GN{d{fR(<3? zO)kpm!7uy6;G~XI&lMH9mfG#-F6r_wZ!~V_U>Xdw%TBLy)EQ)7JnmXOzaTkSmNz-S zdT#(h_4TU%V>8J|UeA()7QxIUytJXrbwSJE2VFNyAyjic5&mpsQMnQK&eRRT99X>c z(O0A027$V4Uuhy_HD9d7zG{~E7_T};rj#&ouMQlgy=6~Nrld&=!TO-Ny_G6T?n0Av z? BB%*x{zg~slGAC(-yQAzjPK{EN@Ecn4cy#NfL-;Y1H zmSOfSxkuX^&{V^q)vq&t^MM^Iu6Zf`z=kAT5x)6raOv_^d^6nwu%UvDG zI7kPxi?X+&$MZlAKN(uaa6kXZkT=l=p>Y ^q6bINy|I+LJ3_3z!{Z;-y47&esbzFas zn}7WuT|gK7V2Tg04a^(O);Q(%DfEgZ$KSQbgV8?Mp8rr4$r*F51jUOP$42>GK?(}q zGg_L}L?4|t5dZ2?wZp}IkCM5 [[[0, 1, 1], ...]], the Bridge and Water are - represented. - - labels: the 2D board where each cell corresponds to a class - represented by the objects in the game. Only one object - class is represented, following the rendering order of the - game. For example, Bridge = 1, Water = 2 -> [[1, ...]], - the Bridge is represented instead of the Water. - - rgb: TODO(wenkesj): docstring. - exclude_from_state: set to exclude from the observations to states. - merge_layer_groups: merge layers for these group of observations keys. """ - assert observation_type in ['layers', 'labels', 'rgb'] assert max_iterations > 0 assert isinstance(default_reward, numbers.Number) - self._game_factory = game_factory self._max_iterations = max_iterations self._default_reward = default_reward - if callable(colors): - self._colors = None - self._colors_factory = colors - else: - self._colors = colors - self._colors_factory = None + + # At this point, the game would only want to access the random + # property, although it is set to None initially. self.np_random = None - test_game = self._game_factory() - setattr(test_game.the_plot, 'info', {}) + self._colors = self.make_colors() + test_game = self.make_game() + test_game.the_plot.info = {} observations, _, _ = test_game.its_showtime() layers = list(observations.layers.keys()) not_ordered = list(set(layers) - set(test_game.z_order)) - self._render_order = list(reversed(not_ordered + test_game.z_order)) - if exclude_from_state is None: - exclude_from_state = [] - self._exclude_from_state = set(exclude_from_state) - - if merge_layer_groups is None: - merge_layer_groups = [set([])] - self._merge_layer_groups = merge_layer_groups - - observation_layers = list(set(layers) - self._exclude_from_state) + # Create the observation space. + observation_layers = list(set(layers)) self._observation_order = sorted(observation_layers) - self._observation_type = observation_type - - if self._observation_type == 'layers': - merge_size_reduction = 0 - for group in self._merge_layer_groups: - if group: - merge_size_reduction += len(group) - 1 - channels = [len(observation_layers) - merge_size_reduction] - channel_max = 1. - channel_min = 0. - self.get_states = self._get_states_layers - elif self._observation_type == 'labels': - # TODO(wenkesj): implement merge_layer_groups - channels = [] - channel_max = 1. - channel_min = 0. - self.get_states = self._get_states_labels - elif self._observation_type == 'rgb': - channels = [3] - channel_max = 255. - channel_min = 0. - self.get_states = self._get_states_rgb - + channels = [3] + channel_max = 255. + channel_min = 0. self._game_shape = list(observations.board.shape) + channels self.observation_space = spaces.Box( - low=np.ones(self._game_shape, np.float32) * channel_min, - high=np.ones(self._game_shape, np.float32) * channel_max, + low=np.full(self._game_shape, channel_min, np.float32), + high=np.full(self._game_shape, channel_max, np.float32), dtype=np.float32) self.action_space = action_space self.current_game = None - self._last_observations = None self._empty_board = None self._last_state = None self._last_reward = None - self._game_over = False + self._game_over = False self.viewer = None self.resize_scale = resize_scale self.delay = delay - def _get_states_layers(self, observations): - """Transform the pycolab `rendering.Observations` to a state by layers.""" - layered_observations = np.stack([ - np.asarray(observations.layers[layer_key], np.float32) - for layer_key in self._observation_order], axis=-1) - - # TODO(wenkesj): is there a faster/better way to do this? - # We can probably precompute this and change the observation_order. - if self._merge_layer_groups[0]: - for group_set in self._merge_layer_groups: - group = list(group_set) - group_layers = [] - group_remove_indices = [] - - leader_key = group[0] - leader_layer_idx = self._observation_order.index(leader_key) - leader_layer = layered_observations[..., leader_layer_idx] - group_layers.append(leader_layer) - - for key in group[1:]: - layer_idx = self._observation_order.index(key) - group_remove_indices.append(layer_idx) - layer = layered_observations[..., layer_idx] - group_layers.append(layer) - - # remove layers that are merged. - layered_observations[..., leader_layer_idx] = np.logical_or.reduce(group_layers) - layered_observations = np.delete(layered_observations, group_remove_indices, -1) - return layered_observations - - # TODO(wenkesj): implement merge_layer_groups for labels. - def _get_states_labels(self, observations): - """Transform the pycolab `rendering.Observations` to a state by label.""" - board = np.zeros(self._game_shape, np.int32) - board_mask = np.zeros(self._game_shape, np.int32) + @abc.abstractmethod + def make_game(self): + """Function that creates a new pycolab game. - for key in self._render_order: - if (key in self._exclude_from_state): - continue - board_layer_mask = np.array(observations.layers[key]) * self._observation_order.index(key) - board = np.where(np.logical_not(board_mask), board_layer_mask, board) - board_mask = np.logical_or(board_layer_mask, board_mask) - return board.astype(np.float32) + Returns: + pycolab.Engine. + """ + pass - def _get_states_rgb(self, observations): - """Transform the pycolab `rendering.Observations` to a state by rgb.""" - board = self._paint_board( - observations.layers, exclude=True).astype(np.float32) - return board + def make_colors(self): + """Functions that returns colors. + + Returns: + Dictionary mapping key name to `tuple(R, G, B)`. + """ + return {} + + def _paint_board(self, layers): + """Method to privately paint layers to RGB. - def _paint_board(self, layers, exclude=False): - """Method to privately paint layers to RGB.""" + Args: + layers: a dictionary mapping a character to the respective curtain. + + Returns: + 3D np.array (np.uint32) representing the RGB of the observation + layers. + """ board_shape = self._last_observations.board.shape board = np.zeros(list(board_shape) + [3], np.uint32) board_mask = np.zeros(list(board_shape) + [3], np.bool) for key in self._render_order: - if exclude and (key in self._exclude_from_state): - continue - color = self._colors.get(key, (0, 0, 0)) color = np.reshape(color, [1, 1, -1]).astype(np.uint32) @@ -208,41 +140,23 @@ def _paint_board(self, layers, exclude=False): board_layer_mask = np.repeat(board_layer_mask, 3, axis=-1) # Update the board with the new layer. - board = np.where(np.logical_not(board_mask), board_layer_mask * color, board) + board = np.where( + np.logical_not(board_mask), + board_layer_mask * color, + board) # Update the mask. board_mask = np.logical_or(board_layer_mask, board_mask) return board - def paint_image(self, layers, board=None, resize=True, exclude=False): - """Paint the layers into the board and return an RGB array.""" - if self._colors: - img = self._paint_board(layers, exclude=exclude) - else: - assert board is not None, '`board` must not be `None` if there are no colors.' - img = board - if resize: - img = np.repeat(np.repeat( - img, self.resize_scale, axis=0), self.resize_scale, axis=1) - if len(img.shape) != 3: - img = np.repeat(img[..., None], 3, axis=-1) - img = img.astype(np.uint8) - return img - - def layers_from_board(self, board): - """Convert the observation board into into layers.""" - board_layers = np.rollaxis(board, axis=-1) - layers = {} - for idx, key in enumerate(self._observation_order): - layers[key] = board_layers[idx] - return layers - def _update_for_game_step(self, observations, reward): """Update internal state with data from an environment interaction.""" self._last_observations = observations self._empty_board = np.zeros_like(self._last_observations.board) - self._last_state = self.get_states(observations) - self._last_reward = reward if reward is not None else self._default_reward + self._last_state = self._paint_board(observations.layers).astype( + np.float32) + self._last_reward = reward if reward is not None else \ + self._default_reward self._game_over = self.current_game.game_over if self.current_game.the_plot.frame >= self._max_iterations: @@ -250,10 +164,9 @@ def _update_for_game_step(self, observations, reward): def reset(self): """Start a new episode.""" - self.current_game = self._game_factory() - if self._colors_factory: - self._colors = self._colors_factory() - setattr(self.current_game.the_plot, 'info', {}) + self.current_game = self.make_game() + self._colors = self.make_colors() + self.current_game.the_plot.info = {} self._game_over = None self._last_observations = None self._last_reward = None @@ -262,7 +175,14 @@ def reset(self): return self._last_state def step(self, action): - """Apply action, step the world forward, and return observations.""" + """Apply action, step the world forward, and return observations. + + Args: + action: the desired action to apply to the environment. + + Returns: + state, reward, done, info. + """ if self.current_game is None: logger.warn("Episode has already ended, call `reset` instead..") state = self._last_state @@ -271,10 +191,10 @@ def step(self, action): return state, reward, done, {} # Execute the action in pycolab. - setattr(self.current_game.the_plot, 'info', {}) + self.current_game.the_plot.info = {} observations, reward, _ = self.current_game.play(action) self._update_for_game_step(observations, reward) - info = getattr(self.current_game.the_plot, 'info', {}) + info = self.current_game.the_plot.info # Check the current status of the game. state = self._last_state @@ -286,37 +206,55 @@ def step(self, action): return state, reward, done, info def render(self, mode='human'): - """Render the board to the gym viewer.""" + """Render the board to an image viewer or an np.array. + + Args: + mode: One of the following modes: + - 'human': render to an image viewer. + - 'rgb_array': render to an RGB np.array (np.uint8) + + Returns: + 3D np.array (np.uint8) or a `viewer.isopen`. + """ + img = self._empty_board if self._last_observations: img = self._last_observations.board layers = self._last_observations.layers - img = self.paint_image(layers, board=img) - else: - img = self._empty_board - img = np.repeat(np.repeat( - img, self.resize_scale, axis=0), self.resize_scale, axis=1) - if len(img.shape) != 3: - img = np.repeat(img[..., None], 3, axis=-1) - img = img.astype(np.uint8) + if self._colors: + img = self._paint_board(layers) + else: + assert img is not None, '`board` must not be `None`.' + + img = _repeat_axes(img, self.resize_scale, axis=[0, 1]) + if len(img.shape) != 3: + img = np.repeat(img[..., None], 3, axis=-1) + img = img.astype(np.uint8) if mode == 'rgb_array': return img - elif mode == 'human': if self.viewer is None: - from gym.envs.classic_control.rendering import SimpleImageViewer + from gym.envs.classic_control.rendering import ( + SimpleImageViewer) self.viewer = SimpleImageViewer() self.viewer.imshow(img) time.sleep(self.delay / 1e3) return self.viewer.isopen def seed(self, seed=None): - """Seeds the environment.""" + """Seeds the environment. + + Args: + seed: seed of the random engine. + + Returns: + [seed]. + """ self.np_random, seed = seeding.np_random(seed) return [seed] def close(self): - """Sets up the renderer.""" + """Tears down the renderer.""" if self.viewer: self.viewer.close() self.viewer = None diff --git a/gym_pycolab/pycolab_env_test.py b/gym_pycolab/pycolab_env_test.py index 3309dc5..6534481 100644 --- a/gym_pycolab/pycolab_env_test.py +++ b/gym_pycolab/pycolab_env_test.py @@ -16,80 +16,69 @@ from gym_pycolab import pycolab_env +class CustomColorsFourRoomsEnv(pycolab_env.PyColabEnv): + """Classic four rooms game, with custom colors. + + Reference: + https://github.com/deepmind/pycolab/blob/master/pycolab/examples/classics/four_rooms.py + """ + + def __init__(self, + max_iterations=10, + default_reward=-1.): + super(CustomColorsFourRoomsEnv, self).__init__( + max_iterations=max_iterations, + default_reward=default_reward, + action_space=spaces.Discrete(4 + 1), + resize_scale=8) + + def make_game(self): + return four_rooms.make_game() + + def make_colors(self): + return { + 'P': (0, 0, 255), + ' ': (255, 0, 0), + '#': (0, 255, 0), + } + + class PyColabEnvTest(parameterized.TestCase): def setUp(self): super(PyColabEnvTest, self).setUp() - self._game_factory = four_rooms.make_game - self._action_space = spaces.Discrete(4 + 1) self._max_iterations = 10 self._default_reward = 0 - self._resize_scale = 8 - self._colors = { - 'P': (0, 0, 255), - ' ': (255, 0, 0), - '#': (0, 255, 0), - } - self._observation_type = 'layers' - - def testBadObservationTypeConstructor(self): - with self.assertRaises(AssertionError): - _ = pycolab_env.PyColabEnv( - game_factory=self._game_factory, - action_space=self._action_space, - max_iterations=self._max_iterations, - default_reward=self._default_reward, - resize_scale=self._resize_scale, - observation_type='bad_observation_type') def testBadMaxIterationsConstructor(self): with self.assertRaises(AssertionError): - _ = pycolab_env.PyColabEnv( - game_factory=self._game_factory, - action_space=self._action_space, + _ = CustomColorsFourRoomsEnv( max_iterations=-1, - default_reward=self._default_reward, - resize_scale=self._resize_scale, - observation_type=self._observation_type) + default_reward=self._default_reward) def testBadDefaultRewardConstructor(self): with self.assertRaises(AssertionError): - _ = pycolab_env.PyColabEnv( - game_factory=self._game_factory, - action_space=self._action_space, + _ = CustomColorsFourRoomsEnv( max_iterations=self._max_iterations, - default_reward=None, - resize_scale=self._resize_scale, - observation_type=self._observation_type) - - @parameterized.named_parameters( - ('Layers', 'layers', (13, 13, 3)), - ('Labels', 'labels', (13, 13)), - ('RGB', 'rgb', (13, 13, 3))) - def testObservationType(self, observation_type, shape): - env = pycolab_env.PyColabEnv( - game_factory=self._game_factory, - action_space=self._action_space, + default_reward=None) + + @parameterized.parameters( + ((13, 13, 3),),) + def testReset(self, shape): + env = CustomColorsFourRoomsEnv( max_iterations=self._max_iterations, - default_reward=self._default_reward, - colors=self._colors, - resize_scale=self._resize_scale, - observation_type=observation_type) + default_reward=self._default_reward) last_state = env.reset() self.assertEqual(shape, last_state.shape) @parameterized.parameters( - (1, (0, 1, 2)), - (2, (0, 1)), + (1, (0, 1, 2)), + (2, (0, 1)), (10, (0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0))) def testMaxIterations(self, max_iterations, actions): - env = pycolab_env.PyColabEnv( - game_factory=self._game_factory, - action_space=self._action_space, + env = CustomColorsFourRoomsEnv( max_iterations=max_iterations, - default_reward=self._default_reward, - resize_scale=self._resize_scale, - observation_type=self._observation_type) + default_reward=self._default_reward) _ = env.reset() for step, action in enumerate(actions): @@ -102,13 +91,9 @@ def testMaxIterations(self, max_iterations, actions): (-1., -3, (0, 1, 2)), (0., 1., (0, 0, 0, 0, 3, 3, 0, 0, 0, 2))) def testTotalRewards(self, default_reward, expected_total_reward, actions): - env = pycolab_env.PyColabEnv( - game_factory=self._game_factory, - action_space=self._action_space, + env = CustomColorsFourRoomsEnv( max_iterations=self._max_iterations, - default_reward=default_reward, - resize_scale=self._resize_scale, - observation_type=self._observation_type) + default_reward=default_reward) _ = env.reset() total_reward = 0. @@ -120,4 +105,4 @@ def testTotalRewards(self, default_reward, expected_total_reward, actions): if __name__ == '__main__': - absltest.main() \ No newline at end of file + absltest.main() diff --git a/gym_pycolab/pycolab_games.py b/gym_pycolab/pycolab_games.py index 678dc44..a5be6af 100644 --- a/gym_pycolab/pycolab_games.py +++ b/gym_pycolab/pycolab_games.py @@ -30,16 +30,18 @@ class OrdealEnv(pycolab_env.PyColabEnv): https://github.com/deepmind/pycolab/blob/master/pycolab/examples/ordeal.py """ - def __init__(self, - max_steps=10, + def __init__(self, + max_iterations=10, default_reward=-1.): super(OrdealEnv, self).__init__( - game_factory=ordeal.make_game, - max_iterations=max_steps, + max_iterations=max_iterations, default_reward=default_reward, action_space=spaces.Discrete(3 + 1), resize_scale=8) + def make_game(self): + return ordeal.make_game() + class WarehouseManagerEnv(pycolab_env.PyColabEnv): """Warehouse manager game. @@ -48,17 +50,20 @@ class WarehouseManagerEnv(pycolab_env.PyColabEnv): https://github.com/deepmind/pycolab/blob/master/pycolab/examples/warehouse_manager.py """ - def __init__(self, + def __init__(self, level=0, - max_steps=10, + max_iterations=10, default_reward=-1.): + self.level = level super(WarehouseManagerEnv, self).__init__( - game_factory=lambda: warehouse_manager.make_game(level), - max_iterations=max_steps, + max_iterations=max_iterations, default_reward=default_reward, action_space=spaces.Discrete(4 + 1), resize_scale=8) + def make_game(self): + return warehouse_manager.make_game(self.level) + class FluvialNatationEnv(pycolab_env.PyColabEnv): """Fluvial natation game. @@ -67,16 +72,18 @@ class FluvialNatationEnv(pycolab_env.PyColabEnv): https://github.com/deepmind/pycolab/blob/master/pycolab/examples/fluvial_natation.py """ - def __init__(self, - max_steps=10, + def __init__(self, + max_iterations=10, default_reward=-1.): super(FluvialNatationEnv, self).__init__( - game_factory=fluvial_natation.make_game, - max_iterations=max_steps, + max_iterations=max_iterations, default_reward=default_reward, action_space=spaces.Discrete(2 + 1), resize_scale=8) + def make_game(self): + return fluvial_natation.make_game() + class ChainWalkEnv(pycolab_env.PyColabEnv): """Classic chain walk game. @@ -85,16 +92,18 @@ class ChainWalkEnv(pycolab_env.PyColabEnv): https://github.com/deepmind/pycolab/blob/master/pycolab/examples/classics/chain_walk.py """ - def __init__(self, - max_steps=10, + def __init__(self, + max_iterations=10, default_reward=-1.): super(ChainWalkEnv, self).__init__( - game_factory=chain_walk.make_game, - max_iterations=max_steps, + max_iterations=max_iterations, default_reward=default_reward, action_space=spaces.Discrete(2 + 1), resize_scale=8) + def make_game(self): + return chain_walk.make_game() + class CliffWalkEnv(pycolab_env.PyColabEnv): """Classic cliff walk game. @@ -103,16 +112,18 @@ class CliffWalkEnv(pycolab_env.PyColabEnv): https://github.com/deepmind/pycolab/blob/master/pycolab/examples/classics/cliff_walk.py """ - def __init__(self, - max_steps=10, + def __init__(self, + max_iterations=10, default_reward=-1.): super(CliffWalkEnv, self).__init__( - game_factory=cliff_walk.make_game, - max_iterations=max_steps, + max_iterations=max_iterations, default_reward=default_reward, action_space=spaces.Discrete(4 + 1), resize_scale=8) + def make_game(self): + return cliff_walk.make_game() + class FourRoomsEnv(pycolab_env.PyColabEnv): """Classic four rooms game. @@ -121,16 +132,18 @@ class FourRoomsEnv(pycolab_env.PyColabEnv): https://github.com/deepmind/pycolab/blob/master/pycolab/examples/classics/four_rooms.py """ - def __init__(self, - max_steps=10, + def __init__(self, + max_iterations=10, default_reward=-1.): super(FourRoomsEnv, self).__init__( - game_factory=four_rooms.make_game, - max_iterations=max_steps, + max_iterations=max_iterations, default_reward=default_reward, action_space=spaces.Discrete(4 + 1), resize_scale=8) + def make_game(self): + return four_rooms.make_game() + class ExtraterrestrialMaraudersEnv(pycolab_env.PyColabEnv): """Extraterrestrial marauders game. @@ -139,16 +152,18 @@ class ExtraterrestrialMaraudersEnv(pycolab_env.PyColabEnv): https://github.com/deepmind/pycolab/blob/master/pycolab/examples/extraterrestrial_marauders.py """ - def __init__(self, - max_steps=10, + def __init__(self, + max_iterations=10, default_reward=-1.): super(ExtraterrestrialMaraudersEnv, self).__init__( - game_factory=extraterrestrial_marauders.make_game, - max_iterations=max_steps, + max_iterations=max_iterations, default_reward=default_reward, action_space=spaces.Discrete(3 + 1), resize_scale=8) + def make_game(self): + return extraterrestrial_marauders.make_game() + class ShockWaveEnv(pycolab_env.PyColabEnv): """Shock wave game. @@ -157,17 +172,20 @@ class ShockWaveEnv(pycolab_env.PyColabEnv): https://github.com/deepmind/pycolab/blob/master/pycolab/examples/shockwave.py """ - def __init__(self, + def __init__(self, level=0, - max_steps=10, + max_iterations=10, default_reward=-1.): + self.level = level super(ShockWaveEnv, self).__init__( - game_factory=lambda: shockwave.make_game(level), - max_iterations=max_steps, + max_iterations=max_iterations, default_reward=default_reward, action_space=spaces.Discrete(3 + 1), resize_scale=8) + def make_game(self): + return shockwave.make_game(self.level) + class ApertureEnv(pycolab_env.PyColabEnv): """Aperature game. @@ -176,17 +194,20 @@ class ApertureEnv(pycolab_env.PyColabEnv): https://github.com/deepmind/pycolab/blob/master/pycolab/examples/aperture.py """ - def __init__(self, + def __init__(self, level=0, - max_steps=10, + max_iterations=10, default_reward=-1.): + self.level = level super(ApertureEnv, self).__init__( - game_factory=lambda: aperture.make_game(level), - max_iterations=max_steps, + max_iterations=max_iterations, default_reward=default_reward, action_space=spaces.Discrete(8 + 1), resize_scale=16) + def make_game(self): + return aperture.make_game(self.level) + class ApprehendEnv(pycolab_env.PyColabEnv): """Apprehend game. @@ -195,16 +216,18 @@ class ApprehendEnv(pycolab_env.PyColabEnv): https://github.com/deepmind/pycolab/blob/master/pycolab/examples/apprehend.py """ - def __init__(self, - max_steps=10, + def __init__(self, + max_iterations=10, default_reward=-1.): super(ApprehendEnv, self).__init__( - game_factory=apprehend.make_game, - max_iterations=max_steps, + max_iterations=max_iterations, default_reward=default_reward, action_space=spaces.Discrete(2 + 1), resize_scale=8) + def make_game(self): + return apprehend.make_game() + class BetterScrollyMazeEnv(pycolab_env.PyColabEnv): """Better scrolly maze game. @@ -213,17 +236,20 @@ class BetterScrollyMazeEnv(pycolab_env.PyColabEnv): https://github.com/deepmind/pycolab/blob/master/pycolab/examples/better_scrolly_maze.py """ - def __init__(self, + def __init__(self, level=0, - max_steps=10, + max_iterations=10, default_reward=-1.): + self.level = level super(BetterScrollyMazeEnv, self).__init__( - game_factory=lambda: better_scrolly_maze.make_game(level), - max_iterations=max_steps, + max_iterations=max_iterations, default_reward=default_reward, action_space=spaces.Discrete(4 + 1), resize_scale=8) + def make_game(self): + return better_scrolly_maze.make_game(self.level) + if __name__ == "__main__": import argparse @@ -231,10 +257,10 @@ def __init__(self, parser = argparse.ArgumentParser() parser.add_argument( - '--game', + '--game', choices=[ - 'chain_walk', - 'cliff_walk', + 'chain_walk', + 'cliff_walk', 'four_rooms', 'extraterrestrial_marauders', 'shockwave', @@ -243,33 +269,33 @@ def __init__(self, 'better_scrolly_maze', 'ordeal', 'fluvial_natation', - 'warehouse_manager'], + 'warehouse_manager'], required=True) parser.add_argument('--benchmark', action='store_true') args = parser.parse_args() if args.game == 'chain_walk': - env = ChainWalkEnv(max_steps=250) + env = ChainWalkEnv(max_iterations=250) elif args.game == 'cliff_walk': - env = CliffWalkEnv(max_steps=250) + env = CliffWalkEnv(max_iterations=250) elif args.game == 'four_rooms': - env = FourRoomsEnv(max_steps=250) + env = FourRoomsEnv(max_iterations=250) elif args.game == 'extraterrestrial_marauders': - env = ExtraterrestrialMaraudersEnv(max_steps=250) + env = ExtraterrestrialMaraudersEnv(max_iterations=250) elif args.game == 'shockwave': - env = ShockWaveEnv(max_steps=250) + env = ShockWaveEnv(max_iterations=250) elif args.game == 'aperture': - env = ApertureEnv(max_steps=250) + env = ApertureEnv(max_iterations=250) elif args.game == 'apprehend': - env = ApprehendEnv(max_steps=250) + env = ApprehendEnv(max_iterations=250) elif args.game == 'better_scrolly_maze': - env = BetterScrollyMazeEnv(max_steps=250) + env = BetterScrollyMazeEnv(max_iterations=250) elif args.game == 'ordeal': - env = OrdealEnv(max_steps=250) + env = OrdealEnv(max_iterations=250) elif args.game == 'warehouse_manager': - env = WarehouseManagerEnv(max_steps=250) + env = WarehouseManagerEnv(max_iterations=250) elif args.game == 'fluvial_natation': - env = FluvialNatationEnv(max_steps=250) + env = FluvialNatationEnv(max_iterations=250) if args.benchmark: import time @@ -290,8 +316,8 @@ def __init__(self, average_eps_time = total_eps_time / num_eps average_fps = total_fps / num_eps print('total eps: {}ms, avg. eps: {}ms, avg. fps: {}fps'.format( - total_eps_time * 1e3, - average_eps_time * 1e3, + total_eps_time * 1e3, + average_eps_time * 1e3, average_fps)) else: state = env.reset() diff --git a/setup.py b/setup.py index 29aaf97..9c1c810 100644 --- a/setup.py +++ b/setup.py @@ -11,4 +11,4 @@ description='Gym interface for custom pycolab games.', url='https://github.com/fomorians/gym_pycolab', packages=find_packages(), - install_requires=['pycolab', 'gym']) \ No newline at end of file + install_requires=['pycolab', 'gym'])