From 3b51593f2b23716089421d41abbcbb41f3ea50c1 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 1 Dec 2016 16:18:50 -0700 Subject: [PATCH 1/5] Finazile example puzzles and document how to use them --- example-puzzles/example/10/puzzle.moth | 90 +++++++++++++++++++++++++ example-puzzles/example/100/puzzle.py | 25 +++++++ example-puzzles/example/3/salad.jpg | Bin 0 -> 13913 bytes 3 files changed, 115 insertions(+) create mode 100644 example-puzzles/example/10/puzzle.moth create mode 100755 example-puzzles/example/100/puzzle.py create mode 100644 example-puzzles/example/3/salad.jpg diff --git a/example-puzzles/example/10/puzzle.moth b/example-puzzles/example/10/puzzle.moth new file mode 100644 index 0000000..3d0e943 --- /dev/null +++ b/example-puzzles/example/10/puzzle.moth @@ -0,0 +1,90 @@ +Author: neale +Summary: Making excellent puzzles +Answer: moo + +Making Excellent Puzzles +==================== + +We (the dirtbags) use some core principles. +If you're writing puzzles for our events, +we expect you to bear these in mind. + +If you're doing your own event, +you might still be interested in these: +it's the result of years of doing these events +and thinking hard about how to make the best possible experience for participants. + + +Respect The Player +--------------------------- + +Start with the assumption that the person playing your puzzle is intelligent, +can learn, +and can figure out whatever you ask of them. +If you observe in trial runs that they can't figure it out, +that indicates a problem on your end, +not theirs. + + +Expect Creative Mayhem +------------------------------------ + +This is a hacking contest. +People are going to be on the lookout for "ways around" your puzzles. +This is something we want them to do: +as a defender, they need to be always searching for holes in the defense. + +Here are some examples of "ways around". +All of these have happened at prior events: + +* Logging into your appliance and changing the access password +* Flooding your service off the network +* Brute-force attacking your 2-letter answer +* Photographing your printed list of answers while a team-mate chats with you +* Writing a script to create hundreds of new accounts faster than you can remove them + +It's important to remember that they are here precisely to cause mayhem. +If you set up a playground where this sort of mayhem ruins the experience for other players, +we (the people running the event) can't penalize participants for doing what we've asked them to. +Instead, we have to devalue points in your category to the point where nobody wants to bother playing it any more. + + +Make The Work Easier than Guessing The Answer +-------------------------------------------------------- + +An important corrolary to expecting creative mayhem is that people are going to +work hard to come up with creative ways to get points without doing the work you ask. + +It's okay to make your work about the same level of difficulty as guessing the answer. +For instance, we use a few 4-byte answers. +It would take about four hours to try submitting every possible answer, +and if people want to spend their time coding up a brute-force attack, +that is a constructive use of their event time and we're okay with it. +Typically people give up on this line of attack when they realize +how much time it's going to take +(figuring this out is also a productive use of time), +but once in a while people will go ahead. + +This means your answers need to be resistant to guessing and brute force attacks. +True/false questions are, needless to say, going to result in us laughing at you. + + +There's More Than One Way To Do It +---------------------------------------------------- + +Don't make puzzles that require a specific solution strategy. +For instance, +we're not going to allow puzzles that require a certain technology +(such as "what is the third item in the File menu on product X's interface?"). + +Rather, if your goal is to highlight a technology, +you should create puzzles that show off how great the product is for this application, +compared to alternatives. +Something like "mount this file system from 1982 and paste the contents of the file named `foo`" +might be trivial in a certain product and take hours by other means. +That's fine. +That's what we want to see. + +---- + +The answer for this page is `moo` \ No newline at end of file diff --git a/example-puzzles/example/100/puzzle.py b/example-puzzles/example/100/puzzle.py new file mode 100755 index 0000000..6b29a25 --- /dev/null +++ b/example-puzzles/example/100/puzzle.py @@ -0,0 +1,25 @@ +#!/usr/bin/python3 + +import io + +def make(puzzle): + puzzle.author = 'neale' + puzzle.summary = 'crazy stuff you can do with puzzle generation' + + puzzle.body.write("## Crazy Things You Can Do With Puzzle Generation\n") + puzzle.body.write("\n") + puzzle.body.write("The source to this puzzle has some advanced examples of stuff you can do in Python.\n") + puzzle.body.write("\n") + + # You can use any file-like object; even your own class that generates output. + f = io.BytesIO("This is some text! Isn't that fantastic?".encode('utf-8')) + puzzle.add_stream(f) + + # We have debug logging + puzzle.log("You don't have to disable puzzle.log calls to move to production; the debug log is just ignored at build-time.") + puzzle.log("HTML is escaped, so you don't have to worry about that!") + + puzzle.answers.append('coffee') + answer = puzzle.make_answer() # Generates a random answer, appending it to puzzle.answers too + puzzle.log("Answers: {}".format(puzzle.answers)) + diff --git a/example-puzzles/example/3/salad.jpg b/example-puzzles/example/3/salad.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cf2239ed5b355259342c30a9244fd790e46e06b1 GIT binary patch literal 13913 zcmdtJWn3Ih(=WV>EbbCKi#seX!QCB#`{M5IZb1SB2n4s_?!iLP;7$k{0)*fa2yiyZ zb>+UF_kF&cuc!IVOm)}4Yr3bas%vV8$EC*&0JegRybJ&c1Ont>e}KnLAeFR_trY+u zFV6%(1^@sc02UAqb_M`+Uw$TW` z=>Qo0t4NwTIXnG1gQZn9!8|+=DZsBA|3>|LMFC`o{gM40#>xGMzJk&8F@I!R!e~G; z5&!{qfpBs_;FIA1_%QkbcA%vG!C=E^)YLz6aDNRn{~tUD1Om+egL!;}wZqCU_OIEZ z{X=s?I5-6%T!NfjU=AKZPJTg%0N~d>f8icC5z%ctJX{3X*`3_k%q*SFp==h;j_f{W zF6=8g+0^*Y!0=ybrPmO?tDiNwzU+df6k)}QFf7p+StnZxj{AkRJ1Jo z>@5T==|x3Qg?t2k99*?3|peFa)c+uak$F537?qHB163__quxsJn%mt&4}P zv(rDaps;2CE9`$v>=)hA2|`D-8|e4PJa{=4xnQty95{&(YFiWC#r{-WQ0<$q8_Fu{hqkp4*SN?(hBmRGp_D8v-VH&0b zb#wov6TfKiKNT=18^_;S{0H)Pcv5B_zo)F~W(%`BGY66X#f<;y|Nn#j6(Zwc=57Q2 zHFOs-Q~Sim2R{|)?)MaVc?m^mmxovb}wn%y&DQ7NiT)M`tN)$sf33r>A^jKhuMA0N2WK};n6rVxl-vXA2AeY&>gML`24?Vd zvUhU!b^?2uIe0?BygWQyJdFPg=f8Tvyut5<`7d_>a}j^qU~UHHaM=I4C0OOJV}m-u zyqViCxAnLRkOaWP{VKoC@UVgaLikmXK_EmB3Nk7x3Ni``DmoTADjFsl3JL}e1|}9Z zHV!r_IxZeAHXe+|{v`zXt0z1HGOQyu8VVY$_5ZRw_5*N`0rr4=cpwe{4hIO21AH6= zkO2U&QNi}NzXcJ5fCK=-!y&`Cq(A@=@xPfc4-W^AfCxf*TmhiN!z$SDuzdu0OPjPp zq>?(=k~FhIlrhr}sfLerEJN+*C7fO{3>k#v5;XDCqALYwfeQYqakG9yB%Hwd?WuZH9Rr01$_S5Jifc=y>!0gRfRnL(v z6Ej0gZ*Z`6-mN!NCY0Z=5IVg?Sp`B)QaLe^|$k=NX%$y`ayOD^76 zAz~MYH}!%$!^#PZpY9lbg{qO~TyybjpmvT|)~AE1;WmMfoJzRkVI&Wq_{dhx3(CuC zc?6cf@zhm~;Hg;{OKD2wN>vg5=58#bG#p6;|Lt_&@5eqi}Bvd^I!z9k~#08C>Uuz%?@0EhqwK>DN7@Cb0QAP0nlgo{UjjZa8P z4dD=zgsCqwOs|26aE}0Jc&wWCGNW6jGIT~wXwjGzg(9lEQ}oE$Z-HdJi4@PCM1*VT z>WJ~(8d zB$n~}R{hQLSafaKZjKflYlbZrpMGX5i*hI0P*mtVYe$%9El)C4|8WT%8R!{#wsuC& ziAEsU@90~DGfq$=oqZY9J!dS-(=Dr!vHkk;Wk2aER)(8^sY%0)Samr{rR*Y&6GO2Q zgUU-2d;QOAA7fcBCph&PTN#zYu9gDj!j1gDXA`37j?yd$m}!aOcKSPOR~<`)hq*SQ zJKV&RxR#*BD(j#a&y#Br?WY*oc(B%e_-W6~az{@>P(s~Rt2Ri+L8acZ;ej%;+Vt{k zt|rfvC^6dT?ek3 z-qZo3#^&%@1iyy;C2M&Pgb>27sJN`NOYdl(={ePr9l@v3mr<|dM$Ow|wmVvifj4Q1 zH+F%})ilLqc}@HZqkUalK4YX@Meo;eR5w8r34Zqer+V(Wh&sUuX;Lp4-!RdsX9-83 zzi;}ct1sSxN3??@5(|gWnXy|Q%y8lN5Ej$sUa)_y>Y^a3>U*Gc-fn?)NGxOl@z97` z?1(xoVRC4gd{t!%*-2e*ZMHAW$`m3lrBSWC3aB+yA}VR8mOLL7>Lx4|v?F(*Uy{Wj zYDboN8rImojj(?50L^}8y5uJdA9A?3Uf`_j#qu^vqR50b#fF8jj7t8!M&1~RAn9G8 zjs5qT1%y3jZ)5p5-uT@HKNN}V!EENm~_G3nIklB9q=lHemf&_xx$NdO9J3rI9I%>W^~ znD(<;Qj%MGRwXs3EXbb1OpY4|W^O<@1b9Rw#6SDUuT2660hbaH8;^rZTpb@=FnvM| z;p8%N4FyTm_idj3Hb2;|BMKDRJ$u((>*MqqO__kmVyIHQ2TGXCa+i6H*b6@K)sL4a zWy#R2mYP7P9Syhq+%Oz$HZnW3zUyx&XDM^cQ$-N8TR8ejNT02+AiY&peB8gOS?UAN zf$|j-uf0>Ydaz2?2OUY1!Y!iXIH!~|y(pU<%0-2JB;R}oRt15zkE@Yng~p%e%X9AE z3v!e^0ThqS|F8{dGk7Uc+>lFZ)TTZ$>%GFlUw1AuS#+vvsESGN5zpXv@1?hRtSf19g&)dnkWx5ox!M%^NbZooEutnX0UfmzJ>)8Di`ICb*BN?~)%B-G5 zSe%vICzdnm(x1ppb&RZE@`592ss_3F7~7ZGNyL^p&V+o1Ro>AU>pIFZTcT062#Ck> zs!YdAhFRi?PplqJi$+;;O+9>g1mLTWkT#V$HYNwfsjzlS3*3vBj3U39Rx;r0uy6L( zW83F@o;~ovOgJEM0p~*=;N}i-u+7GSVk+pJ$AAGfsZqP0+Ck!A_78?eHkQo^r}Gm|#`^oNcRN!=6T-3wmnOFW_c1StGe@wnJpOrmIJcILYSv=}8tj6iEX< z7}vFX4kyzm&Vem`Pc_xv!)JEKJ)pA)i~)36`$`>K*LwQ&T#+lr?!5=n?BSuMr;H8V z&Y~x;l!n8_dd`H-u+0k>{4A?|mmerE+mX^|ZBOyZ4I=iYczKoIgXY@&A|W%;`Kjs$vn%yl+(!i01Mp;AI!K5z_PT4L1}X7Frf+_aY`V zX5_`TNK-XAdP`;5(%+u3pwbSyRi~*WjeK7Tv$>F&#+3Q-WF_`iQy*>M&@=u+fYyD= zuw`NKCmZ;)(b4`Ftj*7!<84|auPV20#T&-kdxotvgenIjezKLcw0XNLE<6$Jk^?Wd zEYR1TmSqqb`ySnO|Jb;qORqNw>%~tER}@R~eI>~j@9b2fVGOIWrR{U2#QF!EVKgzf zW}CCQW*)m$IUQvEaKesnSq%2xGQrX{3(a%-Q_6c5>8#8OGdg+x%&7f6TH$KQl;i&4 zDaaANk%l4K9?ZB&f4@u1mLYPWd%#e0?fnSk%6HXv(LG z*Nm*s%HwQGttLUu<}bR0mWSTrl)s7ll9~3?ZQ3&VO!|d+Vgnm>ZDGmaZrSuoq;gE3 z#xQ(fM<>MB+_ZfZZH;p8@P|GcWYeXB4tgqva+tZ7)%>+t zyyDv1KLkUHs-ikLZD}wsCsb~4y>sGk&SWvN{U@@&KP!hk0uV8tRUA9Ud2rXh(>%-3 zI)Nv05D_$z9H<~RoJxE~N^CCy&)lWnuJlB?Hc*HcQBsbU%GT@haDr3!+*2DL7&T5< zr=b;w8R%55zhDv}_!eityxegh;NH8kic=2C+LC*aiII$f*rh|PthSIwmf<|Md85rC zw~(3j>EaeKVlK9ld@=aB%h8Jop3{{xtT(r+Wnns`@sy+mb9~a`{G7>0NN;Klz}glS z@xdKxFCg7<4hRO!@`k)JUwVwf-nttR;aq78Q*LJBHPi=ND3)qUEyEYjF;y;orQGJ_ z+}y}YLVeXJGZ3_WIW3L0^1zb)CRyuqrB|h(mDG6k(p!O4%V(7p{21>0tF@vOj{w=! z@-a1p)UeOMN=MreeL_!RpWE-+?vUZ(UEzYVqjv)mZxEX9WlD8~_?j(-{eCjVMt(o5 zJE%G%or9%G!C-LLd zn-Dem-WQQ2{UTG-4S^<`r2~tR^KD7Ed}_s|#D<2sN>twauk|i&7>VO#oOAfTi44!1 zLoVAK=^p{~KQy-R$U_KE9<-$$F6jnTX>J3$n?oTY<*n`ST;e?%QqFj?ES~+KntcRV zN-9PsRWyM`s{;K_G}JzTG3E%!@u=6V&LtA~KPk()dum6u6kD>1NDLf#JOZruH(G-1 zh!0)9@TP|dbS&L>yD7gv5Niqvz*;Ib*AQqO#X5d?sMNoSO1TVr!-}8oOtnALqudW& zt@Kw4*Zy`+vbHyNJjL@#@c0vkw`wxx%=k$)rW+|~;Cl}M#jalJpso0Kbx=&5XBim@eUgzgxUdeaLXA*G zZ$CF=tVhwkqesd$BoJ0{D$v%-e>XuSM*il_rjRTN{$ys=9Hd1!araz#ppdlOc00UI z(0Mu?5H?jqwA1E9O8vxugQRT88|CG9-$khvh~>v~Pae|U97)r{{vrF32At+jmbXl! zal!(qfTV=6n21nULZ^^u=?=D7sk;nmJ!~8xHqJEn%YIEwF&*ThV?>y9l7L0vuxJ1d z1VZ}tCJ8G*SU~#2(%{8@Di#&|JfCgPJHGek1daI>~4l4NJr zwLA$=s)^KAI<_lqSlnK#Jas~kzEfrLa&sA?9q*T5;Dmq=kc&d2e5nT*8)fi4V-^Ds zg!tqLb1+y7wGD7d#W1Zz?y_+z#z|!)n~6=yRlZAPu@u$#_%+oo|fw-@b;jv_8C)(PzwM6MV5iKG9<4fpt$ewezZq|tsQLnay zyO&TGgKR=HM&IIeQ(m%vQ)!pL!?nAHY29cndG&yD!}f7A1&+p@D-l8$3l3W6QO?U@?KCmAn`ik6LpZDlI41h=^m53M>9 zoi)Z{Oe#Y-!}3RRa;rdje#f;;W2D3HqpcZ1>jxPuaFriW4nwe<$Ir`4zv@yCOlD6U ze8?R17x13Bx5(Rd61)0&$i^hmHR%7)wePAf6qDpYaN8Hmc8i3DpE`d3+;bFeRSf0c z+0eIMJ;lGFWa52Or|n6t0=MXdU4X+wq`78R=d+(G>)h|_RFRz;Je5aHTsONXgnon# zbsHW8I&FSd^qO!jI4+APr{e61bj*I})2P9O+Jt*@eO)*$WvCxeN=PF#tR)ehUcpCtheRyu^E6D}+Qcw@mn`8P4kEdET+ ze$e}uZLj3pG_mkZ94(%bO-$c(Q5fTEl?_i-rlQ@JHMe^54{UC6b|On9zD@GK6D0{E+v8%5UAtpE)l_A6 z0rEYoZY~|n1m-O=?;29($sN07RRNDnaku1p+KF`I(rEf}PD?TgX?!y0mfF9cm@Fnr zP3B*6*QuWou-_!%EA2<7s5fv?u=bLxua^Z@_uK}}j<3H8FU@du2sfF1@=o zbeXBp8y<@K`qQFFL?0D-Q|@>=upt#g z8ux?9$F1%MGW%a$L`$1nNUtkprQJrokx|j6nGTUlY`qThM8_NYz-^<5^&;sJK(hJP zT6o#lcYY(eHkxU3D=jMMWTl!cMm7Aop&lPhXSDOyR|bZVY+rpI{Vb?^;`2ODeF25U zTI7Ih%Y{EEDN2#UnwRblCc)E|_WjcpqL=lZp~)0K-#weZBCy-){mFZ>w1^qddaXU9 z(KM0pwTgmr$(|nFRkYmR5TSP{hDRxEyytW7etuUh14XBu@QPX@O{XsUM0NG>bC8B} zsbpZ%ZBYAM7R^OO4m;B#wqx+E&d-CJcJ6m?OM=rw(JkKgTpTA3af^r4X(R25uLO8x zT)Nko9CaIdaBJ8)EBnf}Hx;1c$l|iamAAlk9GBRLBnG{aWX3{mI*BIVs-~8I8d_Uz z9lJYwvZ``xd&0)D(@1;8xBEP+&8GWqt}zRZC9K!PBv2taq+EJBY5i7@<9u*~G~L(( z?gJ5gf6W9*+>EF5UGPdbzU)Lfp8ma{anmht%|u|XmwoxT&|4*`ck}w@#_zU_o8K$m zYcJ+$J_1tSrq@g!9~tP(FV_rLTM%4H^o&1Wj=SS9v10!A-V4#o-uV-3zo)~7y?nsI z0pVeZ;(uQMaVP;C;;?s+&=hRgE6B+-c=O+Un<(Y2?2JgtpK|e{*wiw(-_p|b<{J_2 z1E;!}2W6T+~cX=KO0FcPYle zskG=4HteVGTAT>YrB(AY+}{?RCdtl0ULs0YA$^ol%N$d@8ex4Yz8}V<*e`f*uBkOm z%e{)*yAWgjiV)u^4c_N@1$>#47TW_~2sxUQ=n%0=5rI}0=Z=p~!n~%%m4(a(fi2j; zf^tczENU0j^w}33#9mMp$l#2{8fT5CX{M_kLsMW}w(U18gf}c8=Wx}ZzlBR(M%`vx zM3^)oc0jjgz$NI3Y_OVA*{l4ye*FH_RF}fKW(%T|o+=^^lA(enqlVMZhCJW3=SYW(P^5|)25yKH(2IG#I_&h@{B&(VsXvzExd?zFsK;N2> zzOCit>!#;bD}Y`~#WCl36qESFJROT~8=R!p8K@ZKQWo4IQ{td{+#>F2^fncijSWAl zw535QAT<|pvj!4eWy%M96ibzZivlPkYcSHix(p(0U^k-p&fK*r(t4IdM5fY^%W5vC zv`mQ1IqE+`qt-I*hwU3hR|B zyX9Awf*+Zfax)4s&5-@3x#WmV_m*C6+KitjjYv9CYrme~eXAb46jl#4&@yqiIQ^6ONBfvRP66O0bK3-465BVzfaOzHw?Clb9MENt@|cs#ifTG2@tiyg#vZ z0^xbkl7w37>DdU%uSgKURpCz2@SgV9;D4IOOWVAggk(7!6+PK7tuNB%(2Ec3`yj_(? z07nK9LR9|AF>ta%tpa7IkePMmyC3^(XYNflwo1%@ZE+7_@ z1ByMDk~$=3Xu^UgH;SkZD_1ucL*?GGqx8^WV=0qMQ>o4`|E_bfl=CcBNPnZ1u#E<8 zR1GVjAeJ3O$=I42TbYS zjWUFz(fs*3rJ`t(K+fQ}n~1}vmx*hN|IMsyPZ24fR6lUs7XVaovmXHl&9AW+kp*z* z(NV8mB;yH}f)p8dHzC+tKag1CuI_7eKYk{b3mhH*X>3f^UT-qVjC1g>3vD!^m1LZ6 zrWGiwf0Y+XypBcNbb16(TQ!b3U0re(U!=Yp-zpYe+$P3(B0=a&?_2ItKY2#f6>i9> z$1+!@I(FK->5i;o&UG3;L|s?PQr>y-Y(hW8BAeum%O<2wFhOuLJ2&$jz32O^^&&V> zd2)!I#2M;hv55WAGE>U$<^o*~qCm=fM}(eGAWj|fQ5BB->IeM;CoTe**Z~|vS*w)7 z9ZPfb2+&1N;~4s2r#i%rc*Jm7;rIxU(VZ1SzA1kFr501FM`R!`-6y*8g$3$lZ|nxo zQ3_L@9)`KwF+HAk*Fqbgv3#Pi?1vx8p=9+}!E3~zP0b@ZEL31bbP9gbbxnd}^H-Y8 zhRcpU5FA2_G5qz*cV_SlPwoaF&+4~_OhWuLEhNFw9f>19wpo@D(6821@Vpe~Jz*YwGT#~EoD_FscL+D#!5 z<>EoxvPJ3;txe2%XzNCR^=xLS~qsfaN zE5BTx%KPX1bz(e91@Jv|Jv*W`6l(>*rMri-M4p$(&p{YPX~Ov#ZM1KR_D%_SI<9kX zqXR`#`cNr?D0^FxNVuZ)xAa(F>66m~T`LY;!$9n|9%R0I<{I(jhS{bCyEU&%(U>DQ z`prn(Y4fAV0bI=Y_dP-bEYd6W2@MvtpDZAp`{?ymlZNo#-|6~WEJ@nL%Ypug`#<8<@@$2V#UB+=NR_hYjve} z?n5*q(>G^ZKkNzh>{vA;XRgb8p0*b`x4lSFa4w@S`H|?~*(=6=+$6u0(T0R-5>B)? zsyjuYeIL0jb3nxYJ=r_ifjy%Jc0Vws)&d?+PH|(1y~l1jp@2k{cj>KTV}*L1=0wic zRYs(GN9Dox%(hrb{+HJGHq`?443(?d^BV)KAX}$IHZz7kT~h7CNo3_=ce*7A;{H_k zk-I&5%aZb)`@ZIL4X>C_VxfkVpY83z`Da7(IzX>anUjh(W<#U z&$2v-d-IM&FQzEgpgY^{yFm-@@b>As`>!9>m_yu@*R8|6xMT*{-che_tW#;FusX0Z zf`~YeHLOWO)t*yz=ablXf3PF)ZQs7ELQudM{!!Yuo7g5J0?8L@!_8R8nxh8VmbEC!BOndK zE?#Gd#F=m(_x%u35h9pKpWRl=wn-t*nGpLm9Oqlms4!5VDAtwbo#I6urJr_K$1M;* zDcGyPya=;++FuqA2m9#nKNkNd;KlwG@%|2Y|KzZK<=V0P60%NJ5iHI0_^;ChC_t() zB9ywsM7^2FP5jCoL|uOHL;_wXPqA2(ULX(lCgAFFCaS)@GDAqD>v@&;_6>3`=))ZJ z1F#M+shK8zAGXFMTQa8z=DuSmim5X9W(m^|mp>9Jk)}BYPdKc5L*+IRzVo=b+}&8B|kQVm`lcuAhmj(@`g3Fo)7qB?v`Fz zL3|Xez2PRoR$Pk~Eg0wUONzRRgDf^PKbCEZamYi*PcQxrB7U_SUlEG_I)pTQ-X|OK zfi(q(Dn*}YWrNxg0@9+H_r^1CCOkUmUp08|6(o5f593I~&&^vf&h6_r?A0ZBoXAyJ z%}|D`V>oX1st<5Xpb0;4a3JB{i=y+tjt8XDhY9k3)|II?aR{!VDQb9Ce13aW7cP>g#I-Hj&iZ1YEKk7p&bmjzx3{@zYChn5Lt2jF(Z8A;@V1^7?Df^B`1! z_s8=Y_C3^7Vo;Al>Oi=joev2u3|b#Jf{YI7)bi9+y0Ckw496Gs zgD{IP!e#qkp5)-FyD2s(du&%Eu}B?G)CfnsmWv@wZ(K2Qz8^ZMNMbI$m(TH7fTQs* z6bus+J6o2IrD3zqJN*XFeLj(5T@_$TOquGz*FBesW8|%rqyhf%2;d~Iva?2PBI88??7 zJ|>G3KKj6H5R;!XGGY3?+v@ts;8X1^q@X)Ly<;{kpe-3o*O{DVw2;7A?!uSD>tuE! zkNn7NrlF9z4QWZHKFT#Eu^-*k#P4J(mRIR0Q^W`Jd$BoB#BgXqjJUWiQKOz@nP2=; z_$RQVC0BtYa|5}%5FuFl?JgzzLrx@nZe-s10ol$gb1p7W``O~21}*i$#OXw*;2t^? z3IEE2{vb19>r>od|1C(E#a-DR-1g1r2l+Fh_4OBe1w#g(apTgwxQw)+YskonmJ*2a!#9nE|zGOjO%wfQXDNNEh5qIpF-$Py+&(MZfe6EPp z?%d8>ynT8MRik^$_i!vhm%RWWlFfBc&P3h+QZjz z=&m@L)w!oh{z0dKVP}C!BIJHlVn>RB2=;&&k{60PJ@nT+8Wb82)6A2ULn8fx1`h`f z&&*QF15@!ErBx$U4`5X9@Vxt0}-Wk=y24j+oNk;GzCv8c#&XF z6{q{b73?j(8{Fkd!&o39Ab)+aO~!~9Wn#S_ujD%V0PdS$-|}57{!UK`XZ3??u*ci{ zbT4%9jn7XRwtQGysnGh4oePahU!z3O<}c%ZS|gx$;YMSa#Y=80WZPh#aSo;%L{h

N!!>5 zAW&>tfqHn_1M_(?Vh?55fSlw~xxMRgn@hw}2#vi+{8I>O$$M&dKikO)mM|}|5=?5C zl1xZW0?dT^XaTeua+n88*$eaTl~Q>hQOw6d%gD3{GgMTz@4E!Tt`dvUrI7J8KOmvg z&52ovQL|Azxez3Np5=;eAd_Cv$DlxVJu6md9-0hxT1Kmg^vWp>nRSZ28IzL0v8+(= zdC>*HqUf?DB-Rfg%gD4 zfy>gB$mg~TF^_4ewRRA%5SF;o6yK#6pIu?(OiCsxD&c@o2eyYa;Izb(L6#(Zr#i$X zK5!v#gz{iC1wA|bI-z%H9zL9iXR9yWwaxC+rMs>sE(cf6uh}_f3ZzTDu+?&HA~9oC zEM&!wRdY9LdIZE-@LS0bdovl?bwzkiOLLIT2U<3uCG`SL;gLB^i9v`)spX-cKH^qt4A`;&)SP=Aw2ccl?t|W>dH}N(bX@KB#@xF`QPY#Js2(nK8UY5b4GZs?C zp~T{-`Ql<@m1w9?qk;A3~h$u{}KtL!0Bz}i*kcJ~JA0NG=BIDX#4nwFD z-y={tNE$&I6|jm?iv0;MlF_v+xeJr0Vn8LQ$49tvSl^w7?C3}M;Iyg^ffvs06dbltZ1`}NNh8e2-inxRaWT&7R3 z#3R6`{G=%PK)D4cxDZ;w8o0s}y|^PRQ8m`*5_MEWz<9qaU2qCMp4iZEf*@$ma6jU+ zDLObV3U9V-$OOQfC~YuKFkCtT#UvYTqg-3^E(f691YjT>Jl6?zlnFbs6E|=g^Bhr< zdt#AYAobjsoBbo9lGKzw8KZbWk1PH+HPiR_p&H+LLiZ4f_2;jGzq6(Exl@;iP{X^h z&h&G55bPF{?6)fsB*@Utt0P8e<>$+Dvb=|zuV|zXuRN}h)lFXZ+T3Wcmw<@H0T2X* zOdpa->%^G7mc+0Dadm0{<{Wx5lRja|HSNUoOl8b3V zRh9@3m52zxNio(*7f`KF;O@ZPd1f?feoh(#89*Ay@$RcU_6dhm=j1}p7jN#^%Y%8K z8@eY9p76dVC~n#>lbkccfp4YMGz0LoxnHwk+chuDy>whGWo@{IE*3?UO^rXX*;amL zIYt8istzIN^_1825RJ}uVHs6XH5?^MG;wS(fQk9xX{QTPaYVw?B>2G|L-F!e3CWCV z-y1KK$tOI-YUtlQzN}XG0Cfy;#NiQ|%~&zrYFT^V+V5DSh~Di!0@~#CMckC@tFQA& z?`+!ai=P-N;oL+4Do{FGu5vuMxIQ+=#VwmGv=M@d<>I*sLbxUw^aL58uH=3O zQ53fyk1ftkJ|}=0?HVn+JwbLZN!aU=V0$ltqZY zg0g`wG7GmHRgO#=MVuBY3 Date: Thu, 1 Dec 2016 16:20:04 -0700 Subject: [PATCH 2/5] Oops, stuff that should have been in last commit --- README.md | 1 - example-puzzles/example/1/puzzle.moth | 8 +++--- example-puzzles/example/2/puzzle.moth | 1 + example-puzzles/example/3/puzzle.py | 9 ++++++- tools/devel-server.py | 37 ++++++++++++++++++++------- tools/moth.py | 19 +++++++++----- 6 files changed, 53 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c167201..ac36fca 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ for details. Getting Started Developing ------------------------------- - $ git clone $your_puzzles_repo puzzles $ python3 tools/devel-server.py Then point a web browser at http://localhost:8080/ diff --git a/example-puzzles/example/1/puzzle.moth b/example-puzzles/example/1/puzzle.moth index 294c99c..8f06395 100644 --- a/example-puzzles/example/1/puzzle.moth +++ b/example-puzzles/example/1/puzzle.moth @@ -13,17 +13,17 @@ Puzzle categories are laid out on the filesystem: ├─3 │ └─puzzle.py ├─10 - │ └─puzzle.py + │ └─puzzle.moth └─100 - └─puzzle.moth + └─puzzle.py In this example, there are puzzles with point values 1, 2, 3, 10, and 100. -Puzzles 1, 2, and 100 are "static" puzzles: +Puzzles 1, 2, and 10 are "static" puzzles: their content was written by hand. -Puzzles 3 and 10 are "dynamic" puzzles: +Puzzles 3 and 100 are "dynamic" puzzles: they are generated from a Python module. To create a static puzzle, all you must have is a diff --git a/example-puzzles/example/2/puzzle.moth b/example-puzzles/example/2/puzzle.moth index 61b63ec..d909c6c 100644 --- a/example-puzzles/example/2/puzzle.moth +++ b/example-puzzles/example/2/puzzle.moth @@ -1,5 +1,6 @@ Author: neale Summary: Static puzzle resource files +File: salad.jpg Answer: salad You can include additional resources in a static puzzle, diff --git a/example-puzzles/example/3/puzzle.py b/example-puzzles/example/3/puzzle.py index 39636f7..a5e93df 100644 --- a/example-puzzles/example/3/puzzle.py +++ b/example-puzzles/example/3/puzzle.py @@ -14,7 +14,14 @@ def make(puzzle): puzzle.body.write("(Participants don't like it when puzzles and answers change.)\n") puzzle.body.write("\n") + puzzle.add_file('salad.jpg') + puzzle.body.write("Here are some more pictures of salad:\n") + puzzle.body.write("Markdown lets you insert raw HTML if you want") + puzzle.body.write("![salad](salad.jpg)") + puzzle.body.write("\n\n") + number = puzzle.rand.randint(20, 500) puzzle.log("One is the loneliest number, but {} is the saddest number.".format(number)) - puzzle.body.write("The answer for this page is {}.\n".format(answer)) + puzzle.body.write("The answer for this page is `{}`.\n".format(answer)) + diff --git a/tools/devel-server.py b/tools/devel-server.py index 4b9ecd2..3b934e1 100755 --- a/tools/devel-server.py +++ b/tools/devel-server.py @@ -1,5 +1,10 @@ #!/usr/bin/env python3 +# To pick up any changes to this file without restarting anything: +# while true; do ./tools/devel-server.py --once; done +# It's kludgy, but it gets the job done. +# Feel free to make it suck less, for example using the `tcpserver` program. + import glob import html import http.server @@ -26,7 +31,6 @@ sys.dont_write_bytecode = True # XXX: This will eventually cause a problem. Do something more clever here. seed = 1 - def page(title, body): return """ @@ -58,6 +62,8 @@ class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer): class MothHandler(http.server.SimpleHTTPRequestHandler): + puzzles_dir = "puzzles" + def handle_one_request(self): try: super().handle_one_request() @@ -122,7 +128,7 @@ you are a fool. puzzle = None try: - fpath = os.path.join("puzzles", parts[2]) + fpath = os.path.join(self.puzzles_dir, parts[2]) points = int(parts[3]) except: pass @@ -135,15 +141,16 @@ you are a fool. if not cat: title = "Puzzle Categories" body.write("

    ") - for i in sorted(glob.glob(os.path.join("puzzles", "*", ""))): - body.write('
  • {}
  • '.format(i, i)) + for i in sorted(glob.glob(os.path.join(self.puzzles_dir, "*", ""))): + bn = os.path.basename(i.strip('/\\')) + body.write('
  • puzzles/{}/
  • '.format(bn, bn)) body.write("
") elif not puzzle: # List all point values in a category title = "Puzzles in category `{}`".format(parts[2]) body.write("") elif len(parts) == 4: # Serve up a puzzle @@ -175,7 +182,7 @@ you are a fool. try: pfile = puzzle.files[parts[4]] except KeyError: - self.send_error(HTTPStatus.NOT_FOUND, "File not found") + self.send_error(HTTPStatus.NOT_FOUND, "File not found. Did you add it to the Files: header or puzzle.add_stream?") return ctype = self.guess_type(pfile.name) self.send_response(HTTPStatus.OK) @@ -226,10 +233,22 @@ you are a fool. self.wfile.write(payload) -def run(address=('localhost', 8080)): +def run(address=('localhost', 8080), once=False): httpd = ThreadingServer(address, MothHandler) print("=== Listening on http://{}:{}/".format(address[0], address[1])) - httpd.serve_forever() + if once: + httpd.handle_request() + else: + httpd.serve_forever() if __name__ == '__main__': - run() + import argparse + + parser = argparse.ArgumentParser(description="MOTH puzzle development server") + parser.add_argument('--puzzles', default='puzzles', + help="Directory containing your puzzles") + parser.add_argument('--once', default=False, action='store_true', + help="Serve one page, then exit. For debugging the server.") + args = parser.parse_args() + MothHandler.puzzles_dir = args.puzzles + run(once=args.once) diff --git a/tools/moth.py b/tools/moth.py index 7ba1a23..017f882 100644 --- a/tools/moth.py +++ b/tools/moth.py @@ -74,7 +74,7 @@ class Puzzle: def log(self, msg): """Add a new log message to this puzzle.""" - self.logs.append(msg) + self.logs.append(str(msg)) def read_stream(self, stream): header = True @@ -116,12 +116,12 @@ class Puzzle: except FileNotFoundError: puzzle_mod = None - if puzzle_mod: - with pushd(path): + with pushd(path): + if puzzle_mod: puzzle_mod.make(self) - else: - with open(os.path.join(path, 'puzzle.moth')) as f: - self.read_stream(f) + else: + with open('puzzle.moth') as f: + self.read_stream(f) def random_hash(self): """Create a file basename (no extension) with our number generator.""" @@ -146,12 +146,17 @@ class Puzzle: name = self.random_hash() self.files[name] = PuzzleFile(stream, name, visible) + def add_file(self, filename, visible=True): + fd = open(filename, 'rb') + name = os.path.basename(filename) + self.add_stream(fd, name=name, visible=visible) + def randword(self): """Return a randomly-chosen word""" return self.rand.choice(ANSWER_WORDS) - def make_answer(self, word_count, sep=' '): + def make_answer(self, word_count=4, sep=' '): """Generate and return a new answer. It's automatically added to the puzzle answer list. :param int word_count: The number of words to include in the answer. :param str|bytes sep: The word separator. From a6fb2d612057efa18a638b8269465d6a0cafd502 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 1 Dec 2016 16:22:35 -0700 Subject: [PATCH 3/5] Document how to use example puzzles --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ac36fca..42d127b 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,16 @@ for details. Getting Started Developing ------------------------------- +If you don't have a `puzzles` directory, +you can copy the example puzzles as a starting point: + + $ cp -r example-puzzles puzzles + +Then launch the development server: + $ python3 tools/devel-server.py -Then point a web browser at http://localhost:8080/ +Point a web browser at http://localhost:8080/ and start hacking on things in your `puzzles` directory. More on how the devel sever works in From d8af9dfe10f9e4e58f2205ea0b17709009a86a61 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 1 Dec 2016 16:23:54 -0700 Subject: [PATCH 4/5] spacing issue --- example-puzzles/example/1/puzzle.moth | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example-puzzles/example/1/puzzle.moth b/example-puzzles/example/1/puzzle.moth index 8f06395..3b9fc1b 100644 --- a/example-puzzles/example/1/puzzle.moth +++ b/example-puzzles/example/1/puzzle.moth @@ -15,7 +15,7 @@ Puzzle categories are laid out on the filesystem: ├─10 │ └─puzzle.moth └─100 - └─puzzle.py + └─puzzle.py In this example, there are puzzles with point values 1, 2, 3, 10, and 100. From 8899927b9fb3bbb5fa53b8bbf3a342ea0b7c1616 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 5 Jan 2017 16:50:41 -0700 Subject: [PATCH 5/5] Productionize puzzle packager --- install | 75 ----------------------- package-puzzles | 127 --------------------------------------- tools/build-puzzles.py | 84 -------------------------- tools/moth.py | 5 +- tools/package-puzzles.py | 112 ++++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 288 deletions(-) delete mode 100755 install delete mode 100755 package-puzzles delete mode 100755 tools/build-puzzles.py create mode 100755 tools/package-puzzles.py diff --git a/install b/install deleted file mode 100755 index 8b5f77b..0000000 --- a/install +++ /dev/null @@ -1,75 +0,0 @@ -#! /bin/sh - -DESTDIR=$1 - -if [ -z "$DESTDIR" ]; then - echo "Usage: $0 DESTDIR" - exit -fi - -cd $(dirname $0) - -older () { - [ -z "$1" ] && return 1 - target=$1; shift - [ -f $target ] || return 0 - for i in "$@"; do - [ $i -nt $target ] && return 0 - done - return 1 -} - -copy () { - target=$DESTDIR/$1 - if older $target $1; then - echo "COPY $1" - mkdir -p $(dirname $target) - cp $1 $target - fi -} - -setup() { - [ -d $DESTDIR/state ] && return - echo "SETUP" - for i in points.new points.tmp teams; do - dir=$DESTDIR/state/$i - mkdir -p $dir - setfacl -m ${www}:rwx $dir - done - mkdir -p $DESTDIR/packages - >> $DESTDIR/state/points.log - if ! [ -f $DESTDIR/assigned.txt ]; then - hd $DESTDIR/assigned.txt - fi -} - - -echo "Figuring out web user..." -for www in www-data http tc _ _www; do - id $www && break -done -if [ $www = _ ]; then - echo "Unable to determine httpd user on this system. Dying." - exit 1 -fi - -mkdir -p $DESTDIR || exit 1 - -setup -git ls-files | while read fn; do - case "$fn" in - install|.*) - ;; - doc/*) - ;; - www/*) - copy $fn - ;; - bin/*) - copy $fn - ;; - *) - echo "??? $fn" - ;; - esac -done diff --git a/package-puzzles b/package-puzzles deleted file mode 100755 index f730a17..0000000 --- a/package-puzzles +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import binascii -import glob -import hashlib -import io -import json -import os -import shutil -import sys -import zipfile - -import puzzles - -TMPFILE = "%s.tmp" - -def write_kv_pairs(ziphandle, filename, kv): - """ Write out a sorted map to file - :param ziphandle: a zipfile object - :param filename: The filename to write within the zipfile object - :param kv: the map to write out - :return: - """ - filehandle = io.StringIO() - for key in sorted(kv.keys()): - if type(kv[key]) == type([]): - for val in kv[key]: - filehandle.write("%s: %s%s" % (key, val, os.linesep)) - else: - filehandle.write("%s: %s%s" % (key, kv[key], os.linesep)) - filehandle.seek(0) - ziphandle.writestr(filename, filehandle.read()) - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Build a category package') - parser.add_argument('categorydirs', nargs='+', help='Directory of category source') - parser.add_argument('outdir', help='Output directory') - args = parser.parse_args() - - for categorydir in args.categorydirs: - puzzles_dict = {} - secrets = {} - - categoryname = os.path.basename(categorydir.strip(os.sep)) - - # create zipfile - zipfilename = os.path.join(args.outdir, "%s.zip" % categoryname) - if os.path.exists(zipfilename): - # open and gather some state - zf = zipfile.ZipFile(zipfilename, 'r') - try: - category_seed = zf.open(os.path.join(categoryname, "category_seed.txt")).read().strip() - except: - pass - zf.close() - - zf = zipfile.ZipFile(TMPFILE % zipfilename, 'w') - if 'category_seed' not in locals(): - category_seed = binascii.b2a_hex(os.urandom(20)) - - # read in category details (will be pflarr in future) - for categorypath in glob.glob(os.path.join(categorydir, "*", "puzzle.moth")): - points = categorypath.split(os.sep)[-2] # directory before '/puzzle.moth' - categorypath = os.path.dirname(categorypath) - print(categorypath) - try: - points = int(points) - except: - print("Failed to identify points on: %s" % categorypath, file=sys.stderr) - continue - puzzle = puzzles.Puzzle(category_seed, categorypath) - puzzles_dict[points] = puzzle - - # build mapping, answers, and summary - mapping = {} - answers = {} - summary = {} - for points in sorted(puzzles_dict): - puzzle = puzzles_dict[points] - hashmap = hashlib.sha1(category_seed) - hashmap.update(str(points).encode('utf-8')) - mapping[points] = hashmap.hexdigest() - answers[points] = puzzle['answer'] - if len(puzzle['summary']) > 0: - summary[points] = puzzle['summary'] - - # write mapping, answers, summary, category_seed - write_kv_pairs(zf, os.path.join(categoryname, 'map.txt'), mapping) - write_kv_pairs(zf, os.path.join(categoryname, 'answers.txt'), answers) - write_kv_pairs(zf, os.path.join(categoryname, 'summary.txt'), summary) - zf.writestr(os.path.join(categoryname, "category_seed.txt"), category_seed) - - # write out puzzles - for points in sorted(puzzles_dict): - puzzle = puzzles_dict[points] - puzzledir = os.path.join(categoryname, 'content', mapping[points]) - puzzledict = { - 'author': puzzle.author, - 'hashes': puzzle.hashes(), - 'files': [f.name for f in puzzle.files if f.visible], - 'body': puzzle.html_body(), - } - secretsdict = { - 'summary': puzzle.summary, - 'answers': puzzle.answers, - } - - # write associated files - assoc_files = [] - for fobj in puzzle['files']: - if fobj.visible == True: - assoc_files.append(fobj.name) - zf.writestr(os.path.join(puzzledir, fobj.name), \ - fobj.handle.read()) - - if len(assoc_files) > 0: - puzzlejson["associated_files"] = assoc_files - - # non-optimal writing of file-like objects, but it works - zf.writestr(os.path.join(puzzledir, 'puzzle.json'), \ - json.dumps(puzzlejson)) - - # clean up - zf.close() - shutil.move(TMPFILE % zipfilename, zipfilename) -#vim:py diff --git a/tools/build-puzzles.py b/tools/build-puzzles.py deleted file mode 100755 index 8138e83..0000000 --- a/tools/build-puzzles.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/python3 - -import hmac -import base64 -import argparse -import glob -import json -import os -import markdown -import random - -messageChars = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' - -def djb2hash(buf): - h = 5381 - for c in buf: - h = ((h * 33) + c) & 0xffffffff - return h - -class Puzzle: - def __init__(self, stream): - self.message = bytes(random.choice(messageChars) for i in range(20)) - self.fields = {} - self.answers = [] - self.hashes = [] - - body = [] - header = True - for line in stream: - if header: - line = line.strip() - if not line.strip(): - header = False - continue - key, val = line.split(':', 1) - key = key.lower() - val = val.strip() - self._add_field(key, val) - else: - body.append(line) - self.body = ''.join(body) - - def _add_field(self, key, val): - if key == 'answer': - h = djb2hash(val.encode('utf8')) - self.answers.append(val) - self.hashes.append(h) - else: - self.fields[key] = val - - def publish(self): - obj = { - 'author': self.fields['author'], - 'hashes': self.hashes, - 'body': markdown.markdown(self.body), - } - return obj - - def secrets(self): - obj = { - 'answers': self.answers, - 'summary': self.fields['summary'], - } - return obj - - -parser = argparse.ArgumentParser(description='Build a puzzle category') -parser.add_argument('puzzledir', nargs='+', help='Directory of puzzle source') -args = parser.parse_args() - -for puzzledir in args.puzzledir: - puzzles = {} - secrets = {} - for puzzlePath in glob.glob(os.path.join(puzzledir, "*.moth")): - filename = os.path.basename(puzzlePath) - points, ext = os.path.splitext(filename) - points = int(points) - puzzle = Puzzle(open(puzzlePath)) - puzzles[points] = puzzle - - for points in sorted(puzzles): - puzzle = puzzles[points] - print(puzzle.secrets()) - diff --git a/tools/moth.py b/tools/moth.py index 017f882..9e842d4 100644 --- a/tools/moth.py +++ b/tools/moth.py @@ -86,6 +86,7 @@ class Puzzle: continue key, val = line.split(':', 1) key = key.lower() + val = val.strip() if key == 'author': self.author = val elif key == 'summary': @@ -178,7 +179,7 @@ class Puzzle: def hashes(self): "Return a list of answer hashes" - return [djbhash(a) for a in self.answers] + return [djb2hash(a.encode('utf-8')) for a in self.answers] class Category: @@ -212,6 +213,6 @@ class Category: puzzle.read_directory(path) return puzzle - def puzzles(self): + def __iter__(self): for points in self.pointvals: yield self.puzzle(points) diff --git a/tools/package-puzzles.py b/tools/package-puzzles.py new file mode 100755 index 0000000..90b248b --- /dev/null +++ b/tools/package-puzzles.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +import argparse +import binascii +import glob +import hashlib +import io +import json +import logging +import moth +import os +import shutil +import string +import sys +import tempfile +import zipfile + +def write_kv_pairs(ziphandle, filename, kv): + """ Write out a sorted map to file + :param ziphandle: a zipfile object + :param filename: The filename to write within the zipfile object + :param kv: the map to write out + :return: + """ + filehandle = io.StringIO() + for key in sorted(kv.keys()): + if type(kv[key]) == type([]): + for val in kv[key]: + filehandle.write("%s: %s%s" % (key, val, os.linesep)) + else: + filehandle.write("%s: %s%s" % (key, kv[key], os.linesep)) + filehandle.seek(0) + ziphandle.writestr(filename, filehandle.read()) + +def build_category(categorydir, outdir): + zipfileraw = tempfile.NamedTemporaryFile(delete=False) + zf = zipfile.ZipFile(zipfileraw, 'x') + + category_seed = binascii.b2a_hex(os.urandom(20)) + puzzles_dict = {} + secrets = {} + + categoryname = os.path.basename(categorydir.strip(os.sep)) + seedfn = os.path.join("category_seed.txt") + zipfilename = os.path.join(outdir, "%s.zip" % categoryname) + logging.info("Building {} from {}".format(zipfilename, categorydir)) + + if os.path.exists(zipfilename): + # open and gather some state + existing = zipfile.ZipFile(zipfilename, 'r') + try: + category_seed = existing.open(seedfn).read().strip() + except: + pass + existing.close() + logging.debug("Using PRNG seed {}".format(category_seed)) + + zf.writestr(seedfn, category_seed) + + cat = moth.Category(categorydir, category_seed) + mapping = {} + answers = {} + summary = {} + for puzzle in cat: + logging.info("Processing point value {}".format(puzzle.points)) + + hashmap = hashlib.sha1(category_seed) + hashmap.update(str(puzzle.points).encode('utf-8')) + puzzlehash = hashmap.hexdigest() + + mapping[puzzle.points] = puzzlehash + answers[puzzle.points] = puzzle.answers + summary[puzzle.points] = puzzle.summary + + puzzledir = os.path.join('content', puzzlehash) + files = [] + for fn, f in puzzle.files.items(): + if f.visible: + files.append(fn) + payload = f.stream.read() + zf.writestr(os.path.join(puzzledir, fn), payload) + + puzzledict = { + 'author': puzzle.author, + 'hashes': puzzle.hashes(), + 'files': files, + 'body': puzzle.html_body(), + } + puzzlejson = json.dumps(puzzledict) + zf.writestr(os.path.join(puzzledir, 'puzzle.json'), puzzlejson) + + write_kv_pairs(zf, 'map.txt', mapping) + write_kv_pairs(zf, 'answers.txt', answers) + write_kv_pairs(zf, 'summaries.txt', summary) + + # clean up + zf.close() + + shutil.move(zipfileraw.name, zipfilename) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Build a category package') + parser.add_argument('categorydirs', nargs='+', help='Directory of category source') + parser.add_argument('outdir', help='Output directory') + args = parser.parse_args() + + logging.basicConfig(level=logging.DEBUG) + + for categorydir in args.categorydirs: + build_category(categorydir, args.outdir) +