_p0){return function(_p6){return new F(function(){return A(_p6,[new T(function(){return B(A(_oX,[_d]));})]);});};}else{if(_p0>55){return function(_p7){return new F(function(){return A(_p7,[new T(function(){return B(A(_oX,[_d]));})]);});};}else{return new F(function(){return _p1([0,_p0-48|0]);});}}break;case 10:if(48>_p0){return function(_p8){return new F(function(){return A(_p8,[new T(function(){return B(A(_oX,[_d]));})]);});};}else{if(_p0>57){return function(_p9){return new F(function(){return A(_p9,[new T(function(){return B(A(_oX,[_d]));})]);});};}else{return new F(function(){return _p1([0,_p0-48|0]);});}}break;case 16:if(48>_p0){if(97>_p0){if(65>_p0){return function(_pa){return new F(function(){return A(_pa,[new T(function(){return B(A(_oX,[_d]));})]);});};}else{if(_p0>70){return function(_pb){return new F(function(){return A(_pb,[new T(function(){return B(A(_oX,[_d]));})]);});};}else{return new F(function(){return _p1([0,(_p0-65|0)+10|0]);});}}}else{if(_p0>102){if(65>_p0){return function(_pc){return new F(function(){return A(_pc,[new T(function(){return B(A(_oX,[_d]));})]);});};}else{if(_p0>70){return function(_pd){return new F(function(){return A(_pd,[new T(function(){return B(A(_oX,[_d]));})]);});};}else{return new F(function(){return _p1([0,(_p0-65|0)+10|0]);});}}}else{return new F(function(){return _p1([0,(_p0-97|0)+10|0]);});}}}else{if(_p0>57){if(97>_p0){if(65>_p0){return function(_pe){return new F(function(){return A(_pe,[new T(function(){return B(A(_oX,[_d]));})]);});};}else{if(_p0>70){return function(_pf){return new F(function(){return A(_pf,[new T(function(){return B(A(_oX,[_d]));})]);});};}else{return new F(function(){return _p1([0,(_p0-65|0)+10|0]);});}}}else{if(_p0>102){if(65>_p0){return function(_pg){return new F(function(){return A(_pg,[new T(function(){return B(A(_oX,[_d]));})]);});};}else{if(_p0>70){return function(_ph){return new F(function(){return A(_ph,[new T(function(){return B(A(_oX,[_d]));})]);});};}else{return new F(function(){return _p1([0,(_p0-65|0)+10|0]);});}}}else{return new F(function(){return _p1([0,(_p0-97|0)+10|0]);});}}}else{return new F(function(){return _p1([0,_p0-48|0]);});}}break;default:return E(_oR);}}};return function(_pi){return new F(function(){return A(_oV,[_pi,_jE,function(_pj){var _pk=E(_pj);return _pk[0]==0?[2]:B(A(_oU,[_pk]));}]);});};},_pl=[0,10],_pm=[0,1],_pn=[0,2147483647],_po=function(_pp,_pq){while(1){var _pr=E(_pp);if(!_pr[0]){var _ps=_pr[1],_pt=E(_pq);if(!_pt[0]){var _pu=_pt[1],_pv=addC(_ps,_pu);if(!E(_pv[2])){return [0,_pv[1]];}else{_pp=[1,I_fromInt(_ps)];_pq=[1,I_fromInt(_pu)];continue;}}else{_pp=[1,I_fromInt(_ps)];_pq=_pt;continue;}}else{var _pw=E(_pq);if(!_pw[0]){_pp=_pr;_pq=[1,I_fromInt(_pw[1])];continue;}else{return [1,I_add(_pr[1],_pw[1])];}}}},_px=new T(function(){return B(_po(_pn,_pm));}),_py=function(_pz){var _pA=E(_pz);if(!_pA[0]){var _pB=E(_pA[1]);return _pB==(-2147483648)?E(_px):[0, -_pB];}else{return [1,I_negate(_pA[1])];}},_pC=[0,10],_pD=[0,0],_pE=function(_pF){return [0,_pF];},_pG=function(_pH,_pI){while(1){var _pJ=E(_pH);if(!_pJ[0]){var _pK=_pJ[1],_pL=E(_pI);if(!_pL[0]){var _pM=_pL[1];if(!(imul(_pK,_pM)|0)){return [0,imul(_pK,_pM)|0];}else{_pH=[1,I_fromInt(_pK)];_pI=[1,I_fromInt(_pM)];continue;}}else{_pH=[1,I_fromInt(_pK)];_pI=_pL;continue;}}else{var _pN=E(_pI);if(!_pN[0]){_pH=_pJ;_pI=[1,I_fromInt(_pN[1])];continue;}else{return [1,I_mul(_pJ[1],_pN[1])];}}}},_pO=function(_pP,_pQ,_pR){while(1){var _pS=E(_pR);if(!_pS[0]){return E(_pQ);}else{var _pT=B(_po(B(_pG(_pQ,_pP)),B(_pE(E(_pS[1])[1]))));_pR=_pS[2];_pQ=_pT;continue;}}},_pU=function(_pV){var _pW=new T(function(){return B(_nw(B(_nw([0,function(_pX){return E(E(_pX)[1])==45?[1,B(_oS(_pl,function(_pY){return new F(function(){return A(_pV,[[1,new T(function(){return B(_py(B(_pO(_pC,_pD,_pY))));})]]);});}))]:[2];}],[0,function(_pZ){return E(E(_pZ)[1])==43?[1,B(_oS(_pl,function(_q0){return new F(function(){return A(_pV,[[1,new T(function(){return B(_pO(_pC,_pD,_q0));})]]);});}))]:[2];}])),new T(function(){return [1,B(_oS(_pl,function(_q1){return new F(function(){return A(_pV,[[1,new T(function(){return B(_pO(_pC,_pD,_q1));})]]);});}))];})));});return new F(function(){return _nw([0,function(_q2){return E(E(_q2)[1])==101?E(_pW):[2];}],[0,function(_q3){return E(E(_q3)[1])==69?E(_pW):[2];}]);});},_q4=function(_q5){return new F(function(){return A(_q5,[_0]);});},_q6=function(_q7){return new F(function(){return A(_q7,[_0]);});},_q8=function(_q9){return function(_qa){return E(E(_qa)[1])==46?[1,B(_oS(_pl,function(_qb){return new F(function(){return A(_q9,[[1,_qb]]);});}))]:[2];};},_qc=function(_qd){return [0,B(_q8(_qd))];},_qe=function(_qf){return new F(function(){return _oS(_pl,function(_qg){return [1,B(_oi(_qc,_q4,function(_qh){return [1,B(_oi(_pU,_q6,function(_qi){return new F(function(){return A(_qf,[[5,[1,_qg,_qh,_qi]]]);});}))];}))];});});},_qj=function(_qk){return [1,B(_qe(_qk))];},_ql=new T(function(){return B(unCStr("!@#$%&*+./<=>?\\^|:-~"));}),_qm=function(_qn){return new F(function(){return _eC(_mo,_qn,_ql);});},_qo=[0,8],_qp=[0,16],_qq=function(_qr){var _qs=function(_qt){return new F(function(){return A(_qr,[[5,[0,_qo,_qt]]]);});},_qu=function(_qv){return new F(function(){return A(_qr,[[5,[0,_qp,_qv]]]);});};return function(_qw){return E(E(_qw)[1])==48?E([0,function(_qx){switch(E(E(_qx)[1])){case 79:return [1,B(_oS(_qo,_qs))];case 88:return [1,B(_oS(_qp,_qu))];case 111:return [1,B(_oS(_qo,_qs))];case 120:return [1,B(_oS(_qp,_qu))];default:return [2];}}]):[2];};},_qy=function(_qz){return [0,B(_qq(_qz))];},_qA=false,_qB=function(_qC){var _qD=new T(function(){return B(A(_qC,[_qo]));}),_qE=new T(function(){return B(A(_qC,[_qp]));});return function(_qF){switch(E(E(_qF)[1])){case 79:return E(_qD);case 88:return E(_qE);case 111:return E(_qD);case 120:return E(_qE);default:return [2];}};},_qG=function(_qH){return [0,B(_qB(_qH))];},_qI=[0,92],_qJ=function(_qK){return new F(function(){return A(_qK,[_pl]);});},_qL=function(_qM){return new F(function(){return err(B(unAppCStr("Prelude.chr: bad argument: ",new T(function(){return B(_2v(9,_qM,_d));}))));});},_qN=function(_qO){var _qP=E(_qO);return _qP[0]==0?E(_qP[1]):I_toInt(_qP[1]);},_qQ=function(_qR,_qS){var _qT=E(_qR);if(!_qT[0]){var _qU=_qT[1],_qV=E(_qS);return _qV[0]==0?_qU<=_qV[1]:I_compareInt(_qV[1],_qU)>=0;}else{var _qW=_qT[1],_qX=E(_qS);return _qX[0]==0?I_compareInt(_qW,_qX[1])<=0:I_compare(_qW,_qX[1])<=0;}},_qY=function(_qZ){return [2];},_r0=function(_r1){var _r2=E(_r1);if(!_r2[0]){return E(_qY);}else{var _r3=_r2[1],_r4=E(_r2[2]);return _r4[0]==0?E(_r3):function(_r5){return new F(function(){return _nw(B(A(_r3,[_r5])),new T(function(){return B(A(new T(function(){return B(_r0(_r4));}),[_r5]));}));});};}},_r6=function(_r7){return [2];},_r8=function(_r9,_ra){var _rb=function(_rc,_rd){var _re=E(_rc);if(!_re[0]){return function(_rf){return new F(function(){return A(_rf,[_r9]);});};}else{var _rg=E(_rd);return _rg[0]==0?E(_r6):E(_re[1])[1]!=E(_rg[1])[1]?E(_r6):function(_rh){return [0,function(_ri){return E(new T(function(){return B(A(new T(function(){return B(_rb(_re[2],_rg[2]));}),[_rh]));}));}];};}};return function(_rj){return new F(function(){return A(_rb,[_r9,_rj,_ra]);});};},_rk=new T(function(){return B(unCStr("SOH"));}),_rl=[0,1],_rm=function(_rn){return [1,B(_r8(_rk,function(_ro){return E(new T(function(){return B(A(_rn,[_rl]));}));}))];},_rp=new T(function(){return B(unCStr("SO"));}),_rq=[0,14],_rr=function(_rs){return [1,B(_r8(_rp,function(_rt){return E(new T(function(){return B(A(_rs,[_rq]));}));}))];},_ru=function(_rv){return [1,B(_oi(_rm,_rr,_rv))];},_rw=new T(function(){return B(unCStr("NUL"));}),_rx=[0,0],_ry=function(_rz){return [1,B(_r8(_rw,function(_rA){return E(new T(function(){return B(A(_rz,[_rx]));}));}))];},_rB=new T(function(){return B(unCStr("STX"));}),_rC=[0,2],_rD=function(_rE){return [1,B(_r8(_rB,function(_rF){return E(new T(function(){return B(A(_rE,[_rC]));}));}))];},_rG=new T(function(){return B(unCStr("ETX"));}),_rH=[0,3],_rI=function(_rJ){return [1,B(_r8(_rG,function(_rK){return E(new T(function(){return B(A(_rJ,[_rH]));}));}))];},_rL=new T(function(){return B(unCStr("EOT"));}),_rM=[0,4],_rN=function(_rO){return [1,B(_r8(_rL,function(_rP){return E(new T(function(){return B(A(_rO,[_rM]));}));}))];},_rQ=new T(function(){return B(unCStr("ENQ"));}),_rR=[0,5],_rS=function(_rT){return [1,B(_r8(_rQ,function(_rU){return E(new T(function(){return B(A(_rT,[_rR]));}));}))];},_rV=new T(function(){return B(unCStr("ACK"));}),_rW=[0,6],_rX=function(_rY){return [1,B(_r8(_rV,function(_rZ){return E(new T(function(){return B(A(_rY,[_rW]));}));}))];},_s0=new T(function(){return B(unCStr("BEL"));}),_s1=[0,7],_s2=function(_s3){return [1,B(_r8(_s0,function(_s4){return E(new T(function(){return B(A(_s3,[_s1]));}));}))];},_s5=new T(function(){return B(unCStr("BS"));}),_s6=[0,8],_s7=function(_s8){return [1,B(_r8(_s5,function(_s9){return E(new T(function(){return B(A(_s8,[_s6]));}));}))];},_sa=new T(function(){return B(unCStr("HT"));}),_sb=[0,9],_sc=function(_sd){return [1,B(_r8(_sa,function(_se){return E(new T(function(){return B(A(_sd,[_sb]));}));}))];},_sf=new T(function(){return B(unCStr("LF"));}),_sg=[0,10],_sh=function(_si){return [1,B(_r8(_sf,function(_sj){return E(new T(function(){return B(A(_si,[_sg]));}));}))];},_sk=new T(function(){return B(unCStr("VT"));}),_sl=[0,11],_sm=function(_sn){return [1,B(_r8(_sk,function(_so){return E(new T(function(){return B(A(_sn,[_sl]));}));}))];},_sp=new T(function(){return B(unCStr("FF"));}),_sq=[0,12],_sr=function(_ss){return [1,B(_r8(_sp,function(_st){return E(new T(function(){return B(A(_ss,[_sq]));}));}))];},_su=new T(function(){return B(unCStr("CR"));}),_sv=[0,13],_sw=function(_sx){return [1,B(_r8(_su,function(_sy){return E(new T(function(){return B(A(_sx,[_sv]));}));}))];},_sz=new T(function(){return B(unCStr("SI"));}),_sA=[0,15],_sB=function(_sC){return [1,B(_r8(_sz,function(_sD){return E(new T(function(){return B(A(_sC,[_sA]));}));}))];},_sE=new T(function(){return B(unCStr("DLE"));}),_sF=[0,16],_sG=function(_sH){return [1,B(_r8(_sE,function(_sI){return E(new T(function(){return B(A(_sH,[_sF]));}));}))];},_sJ=new T(function(){return B(unCStr("DC1"));}),_sK=[0,17],_sL=function(_sM){return [1,B(_r8(_sJ,function(_sN){return E(new T(function(){return B(A(_sM,[_sK]));}));}))];},_sO=new T(function(){return B(unCStr("DC2"));}),_sP=[0,18],_sQ=function(_sR){return [1,B(_r8(_sO,function(_sS){return E(new T(function(){return B(A(_sR,[_sP]));}));}))];},_sT=new T(function(){return B(unCStr("DC3"));}),_sU=[0,19],_sV=function(_sW){return [1,B(_r8(_sT,function(_sX){return E(new T(function(){return B(A(_sW,[_sU]));}));}))];},_sY=new T(function(){return B(unCStr("DC4"));}),_sZ=[0,20],_t0=function(_t1){return [1,B(_r8(_sY,function(_t2){return E(new T(function(){return B(A(_t1,[_sZ]));}));}))];},_t3=new T(function(){return B(unCStr("NAK"));}),_t4=[0,21],_t5=function(_t6){return [1,B(_r8(_t3,function(_t7){return E(new T(function(){return B(A(_t6,[_t4]));}));}))];},_t8=new T(function(){return B(unCStr("SYN"));}),_t9=[0,22],_ta=function(_tb){return [1,B(_r8(_t8,function(_tc){return E(new T(function(){return B(A(_tb,[_t9]));}));}))];},_td=new T(function(){return B(unCStr("ETB"));}),_te=[0,23],_tf=function(_tg){return [1,B(_r8(_td,function(_th){return E(new T(function(){return B(A(_tg,[_te]));}));}))];},_ti=new T(function(){return B(unCStr("CAN"));}),_tj=[0,24],_tk=function(_tl){return [1,B(_r8(_ti,function(_tm){return E(new T(function(){return B(A(_tl,[_tj]));}));}))];},_tn=new T(function(){return B(unCStr("EM"));}),_to=[0,25],_tp=function(_tq){return [1,B(_r8(_tn,function(_tr){return E(new T(function(){return B(A(_tq,[_to]));}));}))];},_ts=new T(function(){return B(unCStr("SUB"));}),_tt=[0,26],_tu=function(_tv){return [1,B(_r8(_ts,function(_tw){return E(new T(function(){return B(A(_tv,[_tt]));}));}))];},_tx=new T(function(){return B(unCStr("ESC"));}),_ty=[0,27],_tz=function(_tA){return [1,B(_r8(_tx,function(_tB){return E(new T(function(){return B(A(_tA,[_ty]));}));}))];},_tC=new T(function(){return B(unCStr("FS"));}),_tD=[0,28],_tE=function(_tF){return [1,B(_r8(_tC,function(_tG){return E(new T(function(){return B(A(_tF,[_tD]));}));}))];},_tH=new T(function(){return B(unCStr("GS"));}),_tI=[0,29],_tJ=function(_tK){return [1,B(_r8(_tH,function(_tL){return E(new T(function(){return B(A(_tK,[_tI]));}));}))];},_tM=new T(function(){return B(unCStr("RS"));}),_tN=[0,30],_tO=function(_tP){return [1,B(_r8(_tM,function(_tQ){return E(new T(function(){return B(A(_tP,[_tN]));}));}))];},_tR=new T(function(){return B(unCStr("US"));}),_tS=[0,31],_tT=function(_tU){return [1,B(_r8(_tR,function(_tV){return E(new T(function(){return B(A(_tU,[_tS]));}));}))];},_tW=new T(function(){return B(unCStr("SP"));}),_tX=[0,32],_tY=function(_tZ){return [1,B(_r8(_tW,function(_u0){return E(new T(function(){return B(A(_tZ,[_tX]));}));}))];},_u1=new T(function(){return B(unCStr("DEL"));}),_u2=[0,127],_u3=function(_u4){return [1,B(_r8(_u1,function(_u5){return E(new T(function(){return B(A(_u4,[_u2]));}));}))];},_u6=[1,_u3,_d],_u7=[1,_tY,_u6],_u8=[1,_tT,_u7],_u9=[1,_tO,_u8],_ua=[1,_tJ,_u9],_ub=[1,_tE,_ua],_uc=[1,_tz,_ub],_ud=[1,_tu,_uc],_ue=[1,_tp,_ud],_uf=[1,_tk,_ue],_ug=[1,_tf,_uf],_uh=[1,_ta,_ug],_ui=[1,_t5,_uh],_uj=[1,_t0,_ui],_uk=[1,_sV,_uj],_ul=[1,_sQ,_uk],_um=[1,_sL,_ul],_un=[1,_sG,_um],_uo=[1,_sB,_un],_up=[1,_sw,_uo],_uq=[1,_sr,_up],_ur=[1,_sm,_uq],_us=[1,_sh,_ur],_ut=[1,_sc,_us],_uu=[1,_s7,_ut],_uv=[1,_s2,_uu],_uw=[1,_rX,_uv],_ux=[1,_rS,_uw],_uy=[1,_rN,_ux],_uz=[1,_rI,_uy],_uA=[1,_rD,_uz],_uB=[1,_ry,_uA],_uC=[1,_ru,_uB],_uD=new T(function(){return B(_r0(_uC));}),_uE=[0,1114111],_uF=[0,34],_uG=[0,39],_uH=function(_uI){var _uJ=new T(function(){return B(A(_uI,[_s1]));}),_uK=new T(function(){return B(A(_uI,[_s6]));}),_uL=new T(function(){return B(A(_uI,[_sb]));}),_uM=new T(function(){return B(A(_uI,[_sg]));}),_uN=new T(function(){return B(A(_uI,[_sl]));}),_uO=new T(function(){return B(A(_uI,[_sq]));}),_uP=new T(function(){return B(A(_uI,[_sv]));});return new F(function(){return _nw([0,function(_uQ){switch(E(E(_uQ)[1])){case 34:return E(new T(function(){return B(A(_uI,[_uF]));}));case 39:return E(new T(function(){return B(A(_uI,[_uG]));}));case 92:return E(new T(function(){return B(A(_uI,[_qI]));}));case 97:return E(_uJ);case 98:return E(_uK);case 102:return E(_uO);case 110:return E(_uM);case 114:return E(_uP);case 116:return E(_uL);case 118:return E(_uN);default:return [2];}}],new T(function(){return B(_nw([1,B(_oi(_qG,_qJ,function(_uR){return [1,B(_oS(_uR,function(_uS){var _uT=B(_pO(new T(function(){return B(_pE(E(_uR)[1]));}),_pD,_uS));return !B(_qQ(_uT,_uE))?[2]:B(A(_uI,[new T(function(){var _uU=B(_qN(_uT));if(_uU>>>0>1114111){var _uV=B(_qL(_uU));}else{var _uV=[0,_uU];}var _uW=_uV,_uX=_uW,_uY=_uX;return _uY;})]));}))];}))],new T(function(){return B(_nw([0,function(_uZ){return E(E(_uZ)[1])==94?E([0,function(_v0){switch(E(E(_v0)[1])){case 64:return E(new T(function(){return B(A(_uI,[_rx]));}));case 65:return E(new T(function(){return B(A(_uI,[_rl]));}));case 66:return E(new T(function(){return B(A(_uI,[_rC]));}));case 67:return E(new T(function(){return B(A(_uI,[_rH]));}));case 68:return E(new T(function(){return B(A(_uI,[_rM]));}));case 69:return E(new T(function(){return B(A(_uI,[_rR]));}));case 70:return E(new T(function(){return B(A(_uI,[_rW]));}));case 71:return E(_uJ);case 72:return E(_uK);case 73:return E(_uL);case 74:return E(_uM);case 75:return E(_uN);case 76:return E(_uO);case 77:return E(_uP);case 78:return E(new T(function(){return B(A(_uI,[_rq]));}));case 79:return E(new T(function(){return B(A(_uI,[_sA]));}));case 80:return E(new T(function(){return B(A(_uI,[_sF]));}));case 81:return E(new T(function(){return B(A(_uI,[_sK]));}));case 82:return E(new T(function(){return B(A(_uI,[_sP]));}));case 83:return E(new T(function(){return B(A(_uI,[_sU]));}));case 84:return E(new T(function(){return B(A(_uI,[_sZ]));}));case 85:return E(new T(function(){return B(A(_uI,[_t4]));}));case 86:return E(new T(function(){return B(A(_uI,[_t9]));}));case 87:return E(new T(function(){return B(A(_uI,[_te]));}));case 88:return E(new T(function(){return B(A(_uI,[_tj]));}));case 89:return E(new T(function(){return B(A(_uI,[_to]));}));case 90:return E(new T(function(){return B(A(_uI,[_tt]));}));case 91:return E(new T(function(){return B(A(_uI,[_ty]));}));case 92:return E(new T(function(){return B(A(_uI,[_tD]));}));case 93:return E(new T(function(){return B(A(_uI,[_tI]));}));case 94:return E(new T(function(){return B(A(_uI,[_tN]));}));case 95:return E(new T(function(){return B(A(_uI,[_tS]));}));default:return [2];}}]):[2];}],new T(function(){return B(A(_uD,[_uI]));})));})));}));});},_v1=function(_v2){return new F(function(){return A(_v2,[_iG]);});},_v3=function(_v4){var _v5=E(_v4);if(!_v5[0]){return E(_v1);}else{var _v6=_v5[2],_v7=E(E(_v5[1])[1]);switch(_v7){case 9:return function(_v8){return [0,function(_v9){return E(new T(function(){return B(A(new T(function(){return B(_v3(_v6));}),[_v8]));}));}];};case 10:return function(_va){return [0,function(_vb){return E(new T(function(){return B(A(new T(function(){return B(_v3(_v6));}),[_va]));}));}];};case 11:return function(_vc){return [0,function(_vd){return E(new T(function(){return B(A(new T(function(){return B(_v3(_v6));}),[_vc]));}));}];};case 12:return function(_ve){return [0,function(_vf){return E(new T(function(){return B(A(new T(function(){return B(_v3(_v6));}),[_ve]));}));}];};case 13:return function(_vg){return [0,function(_vh){return E(new T(function(){return B(A(new T(function(){return B(_v3(_v6));}),[_vg]));}));}];};case 32:return function(_vi){return [0,function(_vj){return E(new T(function(){return B(A(new T(function(){return B(_v3(_v6));}),[_vi]));}));}];};case 160:return function(_vk){return [0,function(_vl){return E(new T(function(){return B(A(new T(function(){return B(_v3(_v6));}),[_vk]));}));}];};default:var _vm=u_iswspace(_v7),_vn=_vm;return E(_vn)==0?E(_v1):function(_vo){return [0,function(_vp){return E(new T(function(){return B(A(new T(function(){return B(_v3(_v6));}),[_vo]));}));}];};}}},_vq=function(_vr){var _vs=new T(function(){return B(_vq(_vr));}),_vt=[1,function(_vu){return new F(function(){return A(_v3,[_vu,function(_vv){return E([0,function(_vw){return E(E(_vw)[1])==92?E(_vs):[2];}]);}]);});}];return new F(function(){return _nw([0,function(_vx){return E(E(_vx)[1])==92?E([0,function(_vy){var _vz=E(E(_vy)[1]);switch(_vz){case 9:return E(_vt);case 10:return E(_vt);case 11:return E(_vt);case 12:return E(_vt);case 13:return E(_vt);case 32:return E(_vt);case 38:return E(_vs);case 160:return E(_vt);default:var _vA=u_iswspace(_vz),_vB=_vA;return E(_vB)==0?[2]:E(_vt);}}]):[2];}],[0,function(_vC){var _vD=E(_vC);return E(_vD[1])==92?E(new T(function(){return B(_uH(function(_vE){return new F(function(){return A(_vr,[[0,_vE,_eX]]);});}));})):B(A(_vr,[[0,_vD,_qA]]));}]);});},_vF=function(_vG,_vH){return new F(function(){return _vq(function(_vI){var _vJ=E(_vI),_vK=E(_vJ[1]);if(E(_vK[1])==34){if(!E(_vJ[2])){return E(new T(function(){return B(A(_vH,[[1,new T(function(){return B(A(_vG,[_d]));})]]));}));}else{return new F(function(){return _vF(function(_vL){return new F(function(){return A(_vG,[[1,_vK,_vL]]);});},_vH);});}}else{return new F(function(){return _vF(function(_vM){return new F(function(){return A(_vG,[[1,_vK,_vM]]);});},_vH);});}});});},_vN=new T(function(){return B(unCStr("_\'"));}),_vO=function(_vP){var _vQ=u_iswalnum(_vP),_vR=_vQ;return E(_vR)==0?B(_eC(_mo,[0,_vP],_vN)):true;},_vS=function(_vT){return new F(function(){return _vO(E(_vT)[1]);});},_vU=new T(function(){return B(unCStr(",;()[]{}`"));}),_vV=new T(function(){return B(unCStr(".."));}),_vW=new T(function(){return B(unCStr("::"));}),_vX=new T(function(){return B(unCStr("->"));}),_vY=[0,64],_vZ=[1,_vY,_d],_w0=[0,126],_w1=[1,_w0,_d],_w2=new T(function(){return B(unCStr("=>"));}),_w3=[1,_w2,_d],_w4=[1,_w1,_w3],_w5=[1,_vZ,_w4],_w6=[1,_vX,_w5],_w7=new T(function(){return B(unCStr("<-"));}),_w8=[1,_w7,_w6],_w9=[0,124],_wa=[1,_w9,_d],_wb=[1,_wa,_w8],_wc=[1,_qI,_d],_wd=[1,_wc,_wb],_we=[0,61],_wf=[1,_we,_d],_wg=[1,_wf,_wd],_wh=[1,_vW,_wg],_wi=[1,_vV,_wh],_wj=function(_wk){return new F(function(){return _nw([1,function(_wl){return E(_wl)[0]==0?E(new T(function(){return B(A(_wk,[_oP]));})):[2];}],new T(function(){return B(_nw([0,function(_wm){return E(E(_wm)[1])==39?E([0,function(_wn){var _wo=E(_wn);switch(E(_wo[1])){case 39:return [2];case 92:return E(new T(function(){return B(_uH(function(_wp){return [0,function(_wq){return E(E(_wq)[1])==39?E(new T(function(){return B(A(_wk,[[0,_wp]]));})):[2];}];}));}));default:return [0,function(_wr){return E(E(_wr)[1])==39?E(new T(function(){return B(A(_wk,[[0,_wo]]));})):[2];}];}}]):[2];}],new T(function(){return B(_nw([0,function(_ws){return E(E(_ws)[1])==34?E(new T(function(){return B(_vF(_jE,_wk));})):[2];}],new T(function(){return B(_nw([0,function(_wt){return !B(_eC(_mo,_wt,_vU))?[2]:B(A(_wk,[[2,[1,_wt,_d]]]));}],new T(function(){return B(_nw([0,function(_wu){return !B(_eC(_mo,_wu,_ql))?[2]:[1,B(_oE(_qm,function(_wv){var _ww=[1,_wu,_wv];return !B(_eC(_mx,_ww,_wi))?B(A(_wk,[[4,_ww]])):B(A(_wk,[[2,_ww]]));}))];}],new T(function(){return B(_nw([0,function(_wx){var _wy=E(_wx),_wz=_wy[1],_wA=u_iswalpha(_wz),_wB=_wA;return E(_wB)==0?E(_wz)==95?[1,B(_oE(_vS,function(_wC){return new F(function(){return A(_wk,[[3,[1,_wy,_wC]]]);});}))]:[2]:[1,B(_oE(_vS,function(_wD){return new F(function(){return A(_wk,[[3,[1,_wy,_wD]]]);});}))];}],new T(function(){return [1,B(_oi(_qy,_qj,_wk))];})));})));})));})));})));}));});},_wE=function(_wF){return E(E(_wF)[3]);},_wG=function(_wH,_wI,_wJ){return function(_wK){return new F(function(){return A(new T(function(){return B(A(_wE,[_wH,_wJ]));}),[function(_wL){return [1,function(_wM){return new F(function(){return A(_v3,[_wM,function(_wN){return E(new T(function(){return B(_wj(function(_wO){var _wP=E(_wO);return _wP[0]==2?!B(_md(_wP[1],_mc))?[2]:E(new T(function(){return B(A(new T(function(){return B(_wE(_wI));}),[_wJ,function(_wQ){return new F(function(){return A(_wK,[[0,_wL,_wQ]]);});}]));})):[2];}));}));}]);});}];}]);});};},_wR=[0,41],_wS=[1,_wR,_d],_wT=[0,40],_wU=[1,_wT,_d],_wV=[0,0],_wW=function(_wX,_wY){return function(_wZ){return new F(function(){return A(_v3,[_wZ,function(_x0){return E(new T(function(){return B(_wj(function(_x1){var _x2=E(_x1);return _x2[0]==2?!B(_md(_x2[1],_wU))?[2]:E(new T(function(){return B(A(_wX,[_wV,function(_x3){return [1,function(_x4){return new F(function(){return A(_v3,[_x4,function(_x5){return E(new T(function(){return B(_wj(function(_x6){var _x7=E(_x6);return _x7[0]==2?!B(_md(_x7[1],_wS))?[2]:E(new T(function(){return B(A(_wY,[_x3]));})):[2];}));}));}]);});}];}]));})):[2];}));}));}]);});};},_x8=function(_x9,_xa){var _xb=function(_xc){return new F(function(){return _nw([1,B(_wW(_x9,_xc))],new T(function(){return [1,B(_wW(function(_xd,_xe){return new F(function(){return _xb(_xe);});},_xc))];}));});};return new F(function(){return _xb(_xa);});},_xf=function(_xg,_xh,_xi,_xj){return new F(function(){return _x8(function(_xk){return new F(function(){return _wG(_xg,_xh,_xk);});},_xj);});},_xl=[0,91],_xm=[1,_xl,_d],_xn=function(_xo,_xp){var _xq=function(_xr,_xs){return [1,function(_xt){return new F(function(){return A(_v3,[_xt,function(_xu){return E(new T(function(){return B(_wj(function(_xv){var _xw=E(_xv);if(_xw[0]==2){var _xx=E(_xw[1]);if(!_xx[0]){return [2];}else{var _xy=_xx[2];switch(E(E(_xx[1])[1])){case 44:return E(_xy)[0]==0?!E(_xr)?[2]:E(new T(function(){return B(A(_xo,[_wV,function(_xz){return new F(function(){return _xq(_eX,function(_xA){return new F(function(){return A(_xs,[[1,_xz,_xA]]);});});});}]));})):[2];case 93:return E(_xy)[0]==0?E(new T(function(){return B(A(_xs,[_d]));})):[2];default:return [2];}}}else{return [2];}}));}));}]);});}];},_xB=function(_xC){return new F(function(){return _nw([1,function(_xD){return new F(function(){return A(_v3,[_xD,function(_xE){return E(new T(function(){return B(_wj(function(_xF){var _xG=E(_xF);return _xG[0]==2?!B(_md(_xG[1],_xm))?[2]:E(new T(function(){return B(_nw(B(_xq(_qA,_xC)),new T(function(){return B(A(_xo,[_wV,function(_xH){return new F(function(){return _xq(_eX,function(_xI){return new F(function(){return A(_xC,[[1,_xH,_xI]]);});});});}]));})));})):[2];}));}));}]);});}],new T(function(){return [1,B(_wW(function(_xJ,_xK){return new F(function(){return _xB(_xK);});},_xC))];}));});};return new F(function(){return _xB(_xp);});},_xL=function(_xM,_xN,_xO,_xP){return new F(function(){return _xn(function(_xQ,_xk){return new F(function(){return _xf(_xM,_xN,_xQ,_xk);});},_xP);});},_xR=function(_xS,_xT){return function(_oB){return new F(function(){return _nm(new T(function(){return B(_xn(function(_xQ,_xk){return new F(function(){return _xf(_xS,_xT,_xQ,_xk);});},_ob));}),_oB);});};},_xU=function(_xV,_xW,_xX){return function(_oB){return new F(function(){return _nm(new T(function(){return B(_x8(function(_xk){return new F(function(){return _wG(_xV,_xW,_xk);});},_ob));}),_oB);});};},_xY=function(_xZ,_y0){return [0,function(_xk){return new F(function(){return _xU(_xZ,_y0,_xk);});},new T(function(){return B(_xR(_xZ,_y0));}),function(_xQ,_xk){return new F(function(){return _xf(_xZ,_y0,_xQ,_xk);});},function(_xQ,_xk){return new F(function(){return _xL(_xZ,_y0,_xQ,_xk);});}];},_y1=function(_y2,_y3,_y4){var _y5=function(_y6,_y7){return new F(function(){return _nw([1,function(_y8){return new F(function(){return A(_v3,[_y8,function(_y9){return E(new T(function(){return B(_wj(function(_ya){var _yb=E(_ya);if(_yb[0]==4){var _yc=E(_yb[1]);if(!_yc[0]){return new F(function(){return A(_y2,[_yb,_y6,_y7]);});}else{return E(E(_yc[1])[1])==45?E(_yc[2])[0]==0?E([1,function(_yd){return new F(function(){return A(_v3,[_yd,function(_ye){return E(new T(function(){return B(_wj(function(_yf){return new F(function(){return A(_y2,[_yf,_y6,function(_yg){return new F(function(){return A(_y7,[new T(function(){return [0, -E(_yg)[1]];})]);});}]);});}));}));}]);});}]):B(A(_y2,[_yb,_y6,_y7])):B(A(_y2,[_yb,_y6,_y7]));}}else{return new F(function(){return A(_y2,[_yb,_y6,_y7]);});}}));}));}]);});}],new T(function(){return [1,B(_wW(_y5,_y7))];}));});};return new F(function(){return _y5(_y3,_y4);});},_yh=function(_yi,_yj){return [2];},_yk=function(_yl){var _ym=E(_yl);return _ym[0]==0?[1,new T(function(){return B(_pO(new T(function(){return B(_pE(E(_ym[1])[1]));}),_pD,_ym[2]));})]:E(_ym[2])[0]==0?E(_ym[3])[0]==0?[1,new T(function(){return B(_pO(_pC,_pD,_ym[1]));})]:[0]:[0];},_yn=function(_yo){var _yp=E(_yo);if(_yp[0]==5){var _yq=B(_yk(_yp[1]));return _yq[0]==0?E(_yh):function(_yr,_ys){return new F(function(){return A(_ys,[new T(function(){return [0,B(_qN(_yq[1]))];})]);});};}else{return E(_yh);}},_yt=function(_xQ,_xk){return new F(function(){return _y1(_yn,_xQ,_xk);});},_yu=function(_yv,_yw){return new F(function(){return _xn(_yt,_yw);});},_yx=function(_yy){return function(_oB){return new F(function(){return _nm(new T(function(){return B(_y1(_yn,_yy,_ob));}),_oB);});};},_yz=new T(function(){return B(_xn(_yt,_ob));}),_yA=function(_xk){return new F(function(){return _nm(_yz,_xk);});},_yB=[0,_yx,_yA,_yt,_yu],_yC=new T(function(){return B(_xY(_yB,_yB));}),_yD=new T(function(){return B(unCStr("Ring"));}),_yE=new T(function(){return B(unCStr("Marker"));}),_yF=[0,11],_yG=function(_yH,_yI){return new F(function(){return A(_yI,[_d4]);});},_yJ=[1,_1Z,_d],_yK=[0,_yJ,_yG],_yL=function(_yM,_yN){return new F(function(){return A(_yN,[_d6]);});},_yO=[1,_1Y,_d],_yP=[0,_yO,_yL],_yQ=[1,_yP,_d],_yR=[1,_yK,_yQ],_yS=function(_yT,_yU,_yV){var _yW=E(_yT);if(!_yW[0]){return [2];}else{var _yX=E(_yW[1]),_yY=_yX[1],_yZ=new T(function(){return B(A(_yX[2],[_yU,_yV]));});return new F(function(){return _nw([1,function(_z0){return new F(function(){return A(_v3,[_z0,function(_z1){return E(new T(function(){return B(_wj(function(_z2){var _z3=E(_z2);switch(_z3[0]){case 3:return !B(_md(_yY,_z3[1]))?[2]:E(_yZ);case 4:return !B(_md(_yY,_z3[1]))?[2]:E(_yZ);default:return [2];}}));}));}]);});}],new T(function(){return B(_yS(_yW[2],_yU,_yV));}));});}},_z4=function(_z5,_z6){return new F(function(){return _yS(_yR,_z5,_z6);});},_z7=function(_z8,_z9){var _za=function(_zb){return function(_zc){return new F(function(){return _nw(B(A(new T(function(){return B(A(_z8,[_zb]));}),[_zc])),new T(function(){return [1,B(_wW(_za,_zc))];}));});};};return new F(function(){return _za(_z9);});},_zd=function(_ze,_zf){var _zg=new T(function(){if(_ze>10){var _zh=[2];}else{var _zh=[1,function(_zi){return new F(function(){return A(_v3,[_zi,function(_zj){return E(new T(function(){return B(_wj(function(_zk){var _zl=E(_zk);return _zl[0]==3?!B(_md(_zl[1],_yE))?[2]:E(new T(function(){return B(A(_z7,[_z4,_yF,function(_zm){return new F(function(){return A(_zf,[[1,_zm]]);});}]));})):[2];}));}));}]);});}];}var _zn=_zh;return _zn;});if(_ze>10){return new F(function(){return _nw(_oa,_zg);});}else{return new F(function(){return _nw([1,function(_zo){return new F(function(){return A(_v3,[_zo,function(_zp){return E(new T(function(){return B(_wj(function(_zq){var _zr=E(_zq);return _zr[0]==3?!B(_md(_zr[1],_yD))?[2]:E(new T(function(){return B(A(_z7,[_z4,_yF,function(_zs){return new F(function(){return A(_zf,[[0,_zs]]);});}]));})):[2];}));}));}]);});}],_zg);});}},_zt=function(_zu,_zv){return new F(function(){return _zd(E(_zu)[1],_zv);});},_zw=function(_2j){return new F(function(){return _z7(_zt,_2j);});},_zx=function(_zy,_zz){return new F(function(){return _xn(_zw,_zz);});},_zA=new T(function(){return B(_xn(_zw,_ob));}),_zB=function(_2j){return new F(function(){return _nm(_zA,_2j);});},_zC=function(_zD){return function(_oB){return new F(function(){return _nm(new T(function(){return B(A(_z7,[_zt,_zD,_ob]));}),_oB);});};},_zE=[0,_zC,_zB,_zw,_zx],_zF=new T(function(){return B(unCStr("fromList"));}),_zG=function(_zH,_zI,_zJ,_zK){var _zL=E(_zI),_zM=E(_zK);if(!_zM[0]){var _zN=_zM[2],_zO=_zM[3],_zP=_zM[4],_zQ=_zM[5];switch(B(A(_kY,[_zH,_zL,_zN]))){case 0:return new F(function(){return _7M(_zN,_zO,B(_zG(_zH,_zL,_zJ,_zP)),_zQ);});break;case 1:return [0,_zM[1],E(_zL),_zJ,E(_zP),E(_zQ)];default:return new F(function(){return _8t(_zN,_zO,_zP,B(_zG(_zH,_zL,_zJ,_zQ)));});}}else{return [0,1,E(_zL),_zJ,E(_7H),E(_7H)];}},_zR=function(_zS,_zT,_zU,_zV){return new F(function(){return _zG(_zS,_zT,_zU,_zV);});},_zW=function(_zX,_zY,_zZ){return new F(function(){return (function(_A0,_A1){while(1){var _A2=E(_A1);if(!_A2[0]){return E(_A0);}else{var _A3=E(_A2[1]),_A4=B(_zR(_zX,_A3[1],_A3[2],_A0));_A1=_A2[2];_A0=_A4;continue;}}})(_zY,_zZ);});},_A5=function(_A6,_A7){return [0,1,E(E(_A6)),_A7,E(_7H),E(_7H)];},_A8=function(_A9,_Aa,_Ab){var _Ac=E(_Ab);if(!_Ac[0]){return new F(function(){return _8t(_Ac[2],_Ac[3],_Ac[4],B(_A8(_A9,_Aa,_Ac[5])));});}else{return new F(function(){return _A5(_A9,_Aa);});}},_Ad=function(_Ae,_Af,_Ag){var _Ah=E(_Ag);if(!_Ah[0]){return new F(function(){return _7M(_Ah[2],_Ah[3],B(_Ad(_Ae,_Af,_Ah[4])),_Ah[5]);});}else{return new F(function(){return _A5(_Ae,_Af);});}},_Ai=function(_Aj,_Ak,_Al,_Am,_An,_Ao,_Ap){return new F(function(){return _7M(_Am,_An,B(_Ad(_Aj,_Ak,_Ao)),_Ap);});},_Aq=function(_Ar,_As,_At,_Au,_Av,_Aw,_Ax,_Ay){var _Az=E(_At);if(!_Az[0]){var _AA=_Az[1],_AB=_Az[2],_AC=_Az[3],_AD=_Az[4],_AE=_Az[5];if((imul(3,_AA)|0)>=_Au){if((imul(3,_Au)|0)>=_AA){return [0,(_AA+_Au|0)+1|0,E(E(_Ar)),_As,E(_Az),E([0,_Au,E(_Av),_Aw,E(_Ax),E(_Ay)])];}else{return new F(function(){return _8t(_AB,_AC,_AD,B(_Aq(_Ar,_As,_AE,_Au,_Av,_Aw,_Ax,_Ay)));});}}else{return new F(function(){return _7M(_Av,_Aw,B(_AF(_Ar,_As,_AA,_AB,_AC,_AD,_AE,_Ax)),_Ay);});}}else{return new F(function(){return _Ai(_Ar,_As,_Au,_Av,_Aw,_Ax,_Ay);});}},_AF=function(_AG,_AH,_AI,_AJ,_AK,_AL,_AM,_AN){var _AO=E(_AN);if(!_AO[0]){var _AP=_AO[1],_AQ=_AO[2],_AR=_AO[3],_AS=_AO[4],_AT=_AO[5];if((imul(3,_AI)|0)>=_AP){if((imul(3,_AP)|0)>=_AI){return [0,(_AI+_AP|0)+1|0,E(E(_AG)),_AH,E([0,_AI,E(_AJ),_AK,E(_AL),E(_AM)]),E(_AO)];}else{return new F(function(){return _8t(_AJ,_AK,_AL,B(_Aq(_AG,_AH,_AM,_AP,_AQ,_AR,_AS,_AT)));});}}else{return new F(function(){return _7M(_AQ,_AR,B(_AF(_AG,_AH,_AI,_AJ,_AK,_AL,_AM,_AS)),_AT);});}}else{return new F(function(){return _A8(_AG,_AH,[0,_AI,E(_AJ),_AK,E(_AL),E(_AM)]);});}},_AU=function(_AV,_AW,_AX,_AY){var _AZ=E(_AX);if(!_AZ[0]){var _B0=_AZ[1],_B1=_AZ[2],_B2=_AZ[3],_B3=_AZ[4],_B4=_AZ[5],_B5=E(_AY);if(!_B5[0]){var _B6=_B5[1],_B7=_B5[2],_B8=_B5[3],_B9=_B5[4],_Ba=_B5[5];if((imul(3,_B0)|0)>=_B6){if((imul(3,_B6)|0)>=_B0){return [0,(_B0+_B6|0)+1|0,E(E(_AV)),_AW,E(_AZ),E(_B5)];}else{return new F(function(){return _8t(_B1,_B2,_B3,B(_Aq(_AV,_AW,_B4,_B6,_B7,_B8,_B9,_Ba)));});}}else{return new F(function(){return _7M(_B7,_B8,B(_AF(_AV,_AW,_B0,_B1,_B2,_B3,_B4,_B9)),_Ba);});}}else{return new F(function(){return _A8(_AV,_AW,_AZ);});}}else{return new F(function(){return _Ad(_AV,_AW,_AY);});}},_Bb=function(_Bc,_Bd){var _Be=new T(function(){return B(_kH(_Bc));}),_Bf=E(_Bd);if(!_Bf[0]){return [1];}else{var _Bg=E(_Bf[1]),_Bh=_Bg[1],_Bi=_Bg[2],_Bj=E(_Bf[2]);if(!_Bj[0]){return [0,1,E(E(_Bh)),_Bi,E(_7H),E(_7H)];}else{var _Bk=E(_Bj[1]),_Bl=_Bk[1];if(!B(A(_kH,[_Bc,_Bh,_Bl]))){var _Bm=function(_Bn,_Bo,_Bp,_Bq){var _Br=E(_Bn);if(_Br==1){var _Bs=E(_Bq);return _Bs[0]==0?[0,new T(function(){return [0,1,E(E(_Bo)),_Bp,E(_7H),E(_7H)];}),_d,_d]:!B(A(_Be,[_Bo,E(_Bs[1])[1]]))?[0,new T(function(){return [0,1,E(E(_Bo)),_Bp,E(_7H),E(_7H)];}),_Bs,_d]:[0,new T(function(){return [0,1,E(E(_Bo)),_Bp,E(_7H),E(_7H)];}),_d,_Bs];}else{var _Bt=B(_Bm(_Br>>1,_Bo,_Bp,_Bq)),_Bu=_Bt[1],_Bv=_Bt[3],_Bw=E(_Bt[2]);if(!_Bw[0]){return [0,_Bu,_d,_Bv];}else{var _Bx=E(_Bw[1]),_By=_Bx[1],_Bz=_Bx[2],_BA=E(_Bw[2]);if(!_BA[0]){return [0,new T(function(){return B(_A8(_By,_Bz,_Bu));}),_d,_Bv];}else{var _BB=E(_BA[1]),_BC=_BB[1];if(!B(A(_Be,[_By,_BC]))){var _BD=B(_Bm(_Br>>1,_BC,_BB[2],_BA[2]));return [0,new T(function(){return B(_AU(_By,_Bz,_Bu,_BD[1]));}),_BD[2],_BD[3]];}else{return [0,_Bu,_d,_Bw];}}}}};return new F(function(){return (function(_BE,_BF,_BG,_BH,_BI){var _BJ=E(_BI);if(!_BJ[0]){return new F(function(){return _A8(_BG,_BH,_BF);});}else{var _BK=E(_BJ[1]),_BL=_BK[1];if(!B(A(_Be,[_BG,_BL]))){var _BM=B(_Bm(_BE,_BL,_BK[2],_BJ[2])),_BN=_BM[1],_BO=E(_BM[3]);if(!_BO[0]){return new F(function(){return (function(_BP,_BQ,_BR){while(1){var _BS=E(_BR);if(!_BS[0]){return E(_BQ);}else{var _BT=E(_BS[1]),_BU=_BT[1],_BV=_BT[2],_BW=E(_BS[2]);if(!_BW[0]){return new F(function(){return _A8(_BU,_BV,_BQ);});}else{var _BX=E(_BW[1]),_BY=_BX[1];if(!B(A(_Be,[_BU,_BY]))){var _BZ=B(_Bm(_BP,_BY,_BX[2],_BW[2])),_C0=_BZ[1],_C1=E(_BZ[3]);if(!_C1[0]){var _C2=_BP<<1,_C3=B(_AU(_BU,_BV,_BQ,_C0));_BR=_BZ[2];_BP=_C2;_BQ=_C3;continue;}else{return new F(function(){return _zW(_Bc,B(_AU(_BU,_BV,_BQ,_C0)),_C1);});}}else{return new F(function(){return _zW(_Bc,_BQ,_BS);});}}}}})(_BE<<1,B(_AU(_BG,_BH,_BF,_BN)),_BM[2]);});}else{return new F(function(){return _zW(_Bc,B(_AU(_BG,_BH,_BF,_BN)),_BO);});}}else{return new F(function(){return _zW(_Bc,_BF,[1,[0,_BG,_BH],_BJ]);});}}})(1,[0,1,E(E(_Bh)),_Bi,E(_7H),E(_7H)],_Bl,_Bk[2],_Bj[2]);});}else{return new F(function(){return _zW(_Bc,[0,1,E(E(_Bh)),_Bi,E(_7H),E(_7H)],_Bj);});}}}},_C4=function(_C5,_C6,_C7,_C8){return new F(function(){return _z7(function(_C9){return E(_C9)[1]>10?E(_qY):function(_Ca){return [1,function(_Cb){return new F(function(){return A(_v3,[_Cb,function(_Cc){return E(new T(function(){return B(_wj(function(_Cd){var _Ce=E(_Cd);return _Ce[0]==3?!B(_md(_Ce[1],_zF))?[2]:E(new T(function(){return B(_xn(function(_Cf,_Cg){return new F(function(){return _xf(_C6,_C7,_Cf,_Cg);});},function(_Ch){return new F(function(){return A(_Ca,[new T(function(){return B(_Bb(_C5,_Ch));})]);});}));})):[2];}));}));}]);});}];};},_C8);});},_Ci=[0,123],_Cj=[1,_Ci,_d],_Ck=new T(function(){return B(unCStr("bmap"));}),_Cl=new T(function(){return B(unCStr("Board"));}),_Cm=new T(function(){return B(unCStr("markersW"));}),_Cn=new T(function(){return B(unCStr("markersB"));}),_Co=new T(function(){return B(unCStr("ringsW"));}),_Cp=new T(function(){return B(unCStr("ringsB"));}),_Cq=[1,_20,_d],_Cr=function(_2j){return new F(function(){return _wG(_yB,_yB,_2j);});},_Cs=function(_Ct,_Cu){return new F(function(){return _x8(_Cr,_Cu);});},_Cv=[0,61],_Cw=[1,_Cv,_d],_Cx=[0,44],_Cy=[1,_Cx,_d],_Cz=function(_CA,_CB){return _CA>11?[2]:[1,function(_CC){return new F(function(){return A(_v3,[_CC,function(_CD){return E(new T(function(){return B(_wj(function(_CE){var _CF=E(_CE);return _CF[0]==3?!B(_md(_CF[1],_Cl))?[2]:E([1,function(_CG){return new F(function(){return A(_v3,[_CG,function(_CH){return E(new T(function(){return B(_wj(function(_CI){var _CJ=E(_CI);return _CJ[0]==2?!B(_md(_CJ[1],_Cj))?[2]:E([1,function(_CK){return new F(function(){return A(_v3,[_CK,function(_CL){return E(new T(function(){return B(_wj(function(_CM){var _CN=E(_CM);return _CN[0]==3?!B(_md(_CN[1],_Ck))?[2]:E([1,function(_CO){return new F(function(){return A(_v3,[_CO,function(_CP){return E(new T(function(){return B(_wj(function(_CQ){var _CR=E(_CQ);return _CR[0]==2?!B(_md(_CR[1],_Cw))?[2]:E(new T(function(){return B(A(_C4,[_ma,_yC,_zE,_wV,function(_CS){return [1,function(_CT){return new F(function(){return A(_v3,[_CT,function(_CU){return E(new T(function(){return B(_wj(function(_CV){var _CW=E(_CV);return _CW[0]==2?!B(_md(_CW[1],_Cy))?[2]:E([1,function(_CX){return new F(function(){return A(_v3,[_CX,function(_CY){return E(new T(function(){return B(_wj(function(_CZ){var _D0=E(_CZ);return _D0[0]==3?!B(_md(_D0[1],_Cp))?[2]:E([1,function(_D1){return new F(function(){return A(_v3,[_D1,function(_D2){return E(new T(function(){return B(_wj(function(_D3){var _D4=E(_D3);return _D4[0]==2?!B(_md(_D4[1],_Cw))?[2]:E(new T(function(){return B(_xn(_Cs,function(_D5){return [1,function(_D6){return new F(function(){return A(_v3,[_D6,function(_D7){return E(new T(function(){return B(_wj(function(_D8){var _D9=E(_D8);return _D9[0]==2?!B(_md(_D9[1],_Cy))?[2]:E([1,function(_Da){return new F(function(){return A(_v3,[_Da,function(_Db){return E(new T(function(){return B(_wj(function(_Dc){var _Dd=E(_Dc);return _Dd[0]==3?!B(_md(_Dd[1],_Co))?[2]:E([1,function(_De){return new F(function(){return A(_v3,[_De,function(_Df){return E(new T(function(){return B(_wj(function(_Dg){var _Dh=E(_Dg);return _Dh[0]==2?!B(_md(_Dh[1],_Cw))?[2]:E(new T(function(){return B(_xn(_Cs,function(_Di){return [1,function(_Dj){return new F(function(){return A(_v3,[_Dj,function(_Dk){return E(new T(function(){return B(_wj(function(_Dl){var _Dm=E(_Dl);return _Dm[0]==2?!B(_md(_Dm[1],_Cy))?[2]:E([1,function(_Dn){return new F(function(){return A(_v3,[_Dn,function(_Do){return E(new T(function(){return B(_wj(function(_Dp){var _Dq=E(_Dp);return _Dq[0]==3?!B(_md(_Dq[1],_Cn))?[2]:E([1,function(_Dr){return new F(function(){return A(_v3,[_Dr,function(_Ds){return E(new T(function(){return B(_wj(function(_Dt){var _Du=E(_Dt);return _Du[0]==2?!B(_md(_Du[1],_Cw))?[2]:E(new T(function(){return B(_xn(_Cs,function(_Dv){return [1,function(_Dw){return new F(function(){return A(_v3,[_Dw,function(_Dx){return E(new T(function(){return B(_wj(function(_Dy){var _Dz=E(_Dy);return _Dz[0]==2?!B(_md(_Dz[1],_Cy))?[2]:E([1,function(_DA){return new F(function(){return A(_v3,[_DA,function(_DB){return E(new T(function(){return B(_wj(function(_DC){var _DD=E(_DC);return _DD[0]==3?!B(_md(_DD[1],_Cm))?[2]:E([1,function(_DE){return new F(function(){return A(_v3,[_DE,function(_DF){return E(new T(function(){return B(_wj(function(_DG){var _DH=E(_DG);return _DH[0]==2?!B(_md(_DH[1],_Cw))?[2]:E(new T(function(){return B(_xn(_Cs,function(_DI){return [1,function(_DJ){return new F(function(){return A(_v3,[_DJ,function(_DK){return E(new T(function(){return B(_wj(function(_DL){var _DM=E(_DL);return _DM[0]==2?!B(_md(_DM[1],_Cq))?[2]:E(new T(function(){return B(A(_CB,[[0,_CS,_D5,_Di,_Dv,_DI]]));})):[2];}));}));}]);});}];}));})):[2];}));}));}]);});}]):[2];}));}));}]);});}]):[2];}));}));}]);});}];}));})):[2];}));}));}]);});}]):[2];}));}));}]);});}]):[2];}));}));}]);});}];}));})):[2];}));}));}]);});}]):[2];}));}));}]);});}]):[2];}));}));}]);});}];}));})):[2];}));}));}]);});}]):[2];}));}));}]);});}]):[2];}));}));}]);});}];}]));})):[2];}));}));}]);});}]):[2];}));}));}]);});}]):[2];}));}));}]);});}]):[2];}));}));}]);});}];},_DN=function(_DO,_DP){return new F(function(){return _Cz(E(_DO)[1],_DP);});},_DQ=[0],_DR=function(_DS,_DT){return new F(function(){return A(_DT,[_DQ]);});},_DU=[0,_2k,_DR],_DV=function(_DW,_DX){return new F(function(){return A(_DX,[_ex]);});},_DY=[0,_2h,_DV],_DZ=[6],_E0=function(_E1,_E2){return new F(function(){return A(_E2,[_DZ]);});},_E3=[0,_2m,_E0],_E4=[1,_E3,_d],_E5=[1,_DY,_E4],_E6=[1,_DU,_E5],_E7=new T(function(){return B(unCStr("WaitRemoveRun"));}),_E8=new T(function(){return B(unCStr("RemoveRing"));}),_E9=new T(function(){return B(unCStr("RemoveRun"));}),_Ea=new T(function(){return B(unCStr("MoveRing"));}),_Eb=function(_Ec,_Ed){return new F(function(){return _nw(B(_yS(_E6,_Ec,_Ed)),new T(function(){var _Ee=E(_Ec)[1],_Ef=new T(function(){var _Eg=new T(function(){var _Eh=new T(function(){if(_Ee>10){var _Ei=[2];}else{var _Ei=[1,function(_Ej){return new F(function(){return A(_v3,[_Ej,function(_Ek){return E(new T(function(){return B(_wj(function(_El){var _Em=E(_El);return _Em[0]==3?!B(_md(_Em[1],_E7))?[2]:E(new T(function(){return B(A(_z7,[_z4,_yF,function(_En){return new F(function(){return A(_Ed,[[5,_En]]);});}]));})):[2];}));}));}]);});}];}var _Eo=_Ei;return _Eo;});if(_Ee>10){var _Ep=B(_nw(_oa,_Eh));}else{var _Ep=B(_nw([1,function(_Eq){return new F(function(){return A(_v3,[_Eq,function(_Er){return E(new T(function(){return B(_wj(function(_Es){var _Et=E(_Es);return _Et[0]==3?!B(_md(_Et[1],_E8))?[2]:E(new T(function(){return B(A(_z7,[_z4,_yF,function(_Eu){return new F(function(){return A(_Ed,[[4,_Eu]]);});}]));})):[2];}));}));}]);});}],_Eh));}var _Ev=_Ep;return _Ev;});if(_Ee>10){var _Ew=B(_nw(_oa,_Eg));}else{var _Ew=B(_nw([1,function(_Ex){return new F(function(){return A(_v3,[_Ex,function(_Ey){return E(new T(function(){return B(_wj(function(_Ez){var _EA=E(_Ez);return _EA[0]==3?!B(_md(_EA[1],_E9))?[2]:E(new T(function(){return B(A(_z7,[_z4,_yF,function(_EB){return new F(function(){return A(_Ed,[[3,_EB]]);});}]));})):[2];}));}));}]);});}],_Eg));}var _EC=_Ew;return _EC;});if(_Ee>10){var _ED=B(_nw(_oa,_Ef));}else{var _ED=B(_nw([1,function(_EE){return new F(function(){return A(_v3,[_EE,function(_EF){return E(new T(function(){return B(_wj(function(_EG){var _EH=E(_EG);return _EH[0]==3?!B(_md(_EH[1],_Ea))?[2]:E(new T(function(){return B(_x8(_Cr,function(_EI){return new F(function(){return A(_Ed,[[2,_EI]]);});}));})):[2];}));}));}]);});}],_Ef));}var _EJ=_ED,_EK=_EJ;return _EK;}));});},_EL=new T(function(){return B(unCStr("activePlayer"));}),_EM=new T(function(){return B(unCStr("GameState"));}),_EN=new T(function(){return B(unCStr("board"));}),_EO=new T(function(){return B(unCStr("turnMode"));}),_EP=new T(function(){return B(unCStr("pointsW"));}),_EQ=new T(function(){return B(unCStr("pointsB"));}),_ER=function(_ES,_ET){return _ES>11?[2]:[1,function(_EU){return new F(function(){return A(_v3,[_EU,function(_EV){return E(new T(function(){return B(_wj(function(_EW){var _EX=E(_EW);return _EX[0]==3?!B(_md(_EX[1],_EM))?[2]:E([1,function(_EY){return new F(function(){return A(_v3,[_EY,function(_EZ){return E(new T(function(){return B(_wj(function(_F0){var _F1=E(_F0);return _F1[0]==2?!B(_md(_F1[1],_Cj))?[2]:E([1,function(_F2){return new F(function(){return A(_v3,[_F2,function(_F3){return E(new T(function(){return B(_wj(function(_F4){var _F5=E(_F4);return _F5[0]==3?!B(_md(_F5[1],_EL))?[2]:E([1,function(_F6){return new F(function(){return A(_v3,[_F6,function(_F7){return E(new T(function(){return B(_wj(function(_F8){var _F9=E(_F8);return _F9[0]==2?!B(_md(_F9[1],_Cw))?[2]:E(new T(function(){return B(A(_z7,[_z4,_wV,function(_Fa){return [1,function(_Fb){return new F(function(){return A(_v3,[_Fb,function(_Fc){return E(new T(function(){return B(_wj(function(_Fd){var _Fe=E(_Fd);return _Fe[0]==2?!B(_md(_Fe[1],_Cy))?[2]:E([1,function(_Ff){return new F(function(){return A(_v3,[_Ff,function(_Fg){return E(new T(function(){return B(_wj(function(_Fh){var _Fi=E(_Fh);return _Fi[0]==3?!B(_md(_Fi[1],_EO))?[2]:E([1,function(_Fj){return new F(function(){return A(_v3,[_Fj,function(_Fk){return E(new T(function(){return B(_wj(function(_Fl){var _Fm=E(_Fl);return _Fm[0]==2?!B(_md(_Fm[1],_Cw))?[2]:E(new T(function(){return B(A(_z7,[_Eb,_wV,function(_Fn){return [1,function(_Fo){return new F(function(){return A(_v3,[_Fo,function(_Fp){return E(new T(function(){return B(_wj(function(_Fq){var _Fr=E(_Fq);return _Fr[0]==2?!B(_md(_Fr[1],_Cy))?[2]:E([1,function(_Fs){return new F(function(){return A(_v3,[_Fs,function(_Ft){return E(new T(function(){return B(_wj(function(_Fu){var _Fv=E(_Fu);return _Fv[0]==3?!B(_md(_Fv[1],_EN))?[2]:E([1,function(_Fw){return new F(function(){return A(_v3,[_Fw,function(_Fx){return E(new T(function(){return B(_wj(function(_Fy){var _Fz=E(_Fy);return _Fz[0]==2?!B(_md(_Fz[1],_Cw))?[2]:E(new T(function(){return B(A(_z7,[_DN,_wV,function(_FA){return [1,function(_FB){return new F(function(){return A(_v3,[_FB,function(_FC){return E(new T(function(){return B(_wj(function(_FD){var _FE=E(_FD);return _FE[0]==2?!B(_md(_FE[1],_Cy))?[2]:E([1,function(_FF){return new F(function(){return A(_v3,[_FF,function(_FG){return E(new T(function(){return B(_wj(function(_FH){var _FI=E(_FH);return _FI[0]==3?!B(_md(_FI[1],_EQ))?[2]:E([1,function(_FJ){return new F(function(){return A(_v3,[_FJ,function(_FK){return E(new T(function(){return B(_wj(function(_FL){var _FM=E(_FL);return _FM[0]==2?!B(_md(_FM[1],_Cw))?[2]:E(new T(function(){return B(_y1(_yn,_wV,function(_FN){return [1,function(_FO){return new F(function(){return A(_v3,[_FO,function(_FP){return E(new T(function(){return B(_wj(function(_FQ){var _FR=E(_FQ);return _FR[0]==2?!B(_md(_FR[1],_Cy))?[2]:E([1,function(_FS){return new F(function(){return A(_v3,[_FS,function(_FT){return E(new T(function(){return B(_wj(function(_FU){var _FV=E(_FU);return _FV[0]==3?!B(_md(_FV[1],_EP))?[2]:E([1,function(_FW){return new F(function(){return A(_v3,[_FW,function(_FX){return E(new T(function(){return B(_wj(function(_FY){var _FZ=E(_FY);return _FZ[0]==2?!B(_md(_FZ[1],_Cw))?[2]:E(new T(function(){return B(_y1(_yn,_wV,function(_G0){return [1,function(_G1){return new F(function(){return A(_v3,[_G1,function(_G2){return E(new T(function(){return B(_wj(function(_G3){var _G4=E(_G3);return _G4[0]==2?!B(_md(_G4[1],_Cq))?[2]:E(new T(function(){return B(A(_ET,[[0,_Fa,_Fn,_FA,_FN,_G0]]));})):[2];}));}));}]);});}];}));})):[2];}));}));}]);});}]):[2];}));}));}]);});}]):[2];}));}));}]);});}];}));})):[2];}));}));}]);});}]):[2];}));}));}]);});}]):[2];}));}));}]);});}];}]));})):[2];}));}));}]);});}]):[2];}));}));}]);});}]):[2];}));}));}]);});}];}]));})):[2];}));}));}]);});}]):[2];}));}));}]);});}]):[2];}));}));}]);});}];}]));})):[2];}));}));}]);});}]):[2];}));}));}]);});}]):[2];}));}));}]);});}]):[2];}));}));}]);});}];},_G5=function(_G6,_G7){return new F(function(){return _ER(E(_G6)[1],_G7);});},_G8=function(_G9){return [1,function(_Ga){return new F(function(){return A(_v3,[_Ga,function(_Gb){return E([3,_G9,_oa]);}]);});}];},_Gc=new T(function(){return B(A(_z7,[_G5,_wV,_G8]));}),_Gd=[0],_Ge=[0,_d4,_DQ,_jp,_28,_28],_Gf=[1,_Ge,_d],_Gg=[0,_Gf,_Gd],_Gh=function(_Gi,_Gj,_Gk,_Gl,_){var _Gm=jsPushState(_Gl),_Gn=jsTranslate(_Gl,_Gi,_Gj),_Go=B(A(_Gk,[[0,_Gl],_])),_Gp=_Go,_Gq=jsPopState(_Gl);return _iG;},_Gr=function(_Gs,_Gt,_){while(1){var _Gu=E(_Gs);if(!_Gu[0]){return _iG;}else{var _Gv=B(A(_Gu[1],[_Gt,_])),_Gw=_Gv;_Gs=_Gu[2];continue;}}},_Gx=function(_Gy,_Gz,_GA,_){var _GB=E(_Gy);return new F(function(){return _Gh(E(_GB[1])[1],E(_GB[2])[1],_Gz,E(_GA)[1],_);});},_GC=function(_GD,_GE){var _GF=E(_GD);return _GF[0]==0?[0]:[1,function(_GG,_){return new F(function(){return _Gx(_GF[1],_GE,_GG,_);});},new T(function(){return B(_GC(_GF[2],_GE));})];},_GH=[0,30],_GI=function(_GJ,_GK,_GL,_){var _GM=jsPushState(_GL),_GN=jsRotate(_GL,_GJ),_GO=B(A(_GK,[[0,_GL],_])),_GP=_GO,_GQ=jsPopState(_GL);return _iG;},_GR=function(_GS){return [0, -E(_GS)[1]];},_GT=[0,0],_GU=function(_GV,_){return _iG;},_GW=function(_GX){var _GY=E(_GX);if(!_GY[0]){return E(_GU);}else{var _GZ=E(_GY[1]);return function(_H0,_){var _H1=E(_H0)[1],_H2=jsMoveTo(_H1,E(_GZ[1])[1],E(_GZ[2])[1]);return new F(function(){return (function(_H3,_){while(1){var _H4=E(_H3);if(!_H4[0]){return _iG;}else{var _H5=E(_H4[1]),_H6=jsLineTo(_H1,E(_H5[1])[1],E(_H5[2])[1]);_H3=_H4[2];continue;}}})(_GY[2],_);});};}},_H7=function(_H8,_H9,_){var _Ha=jsBeginPath(_H9),_Hb=B(A(_H8,[[0,_H9],_])),_Hc=_Hb,_Hd=jsStroke(_H9);return _iG;},_He=function(_Hf,_Hg,_){return new F(function(){return _H7(_Hf,E(_Hg)[1],_);});},_Hh=function(_Hi){var _Hj=new T(function(){return B(_GW([1,[0,_GT,new T(function(){return B(_GR(_Hi));})],[1,[0,_GT,_Hi],_d]]));});return function(_Hk,_){var _Hl=E(_Hk)[1],_Hm=jsBeginPath(_Hl),_Hn=B(A(_Hj,[[0,_Hl],_])),_Ho=_Hn,_Hp=jsStroke(_Hl),_Hq=jsPushState(_Hl),_Hr=jsRotate(_Hl,2.0943951023931953),_Hs=jsBeginPath(_Hl),_Ht=B(A(_Hj,[[0,_Hl],_])),_Hu=_Ht,_Hv=jsStroke(_Hl),_Hw=jsPopState(_Hl);return new F(function(){return _GI(4.1887902047863905,function(_GG,_){return new F(function(){return _He(_Hj,_GG,_);});},_Hl,_);});};},_Hx=new T(function(){return B(_Hh(_GH));}),_Hy=new T(function(){return B(_GC(_6u,_Hx));}),_Hz=[1,_d6,_d],_HA=function(_HB,_){return _iG;},_HC=new T(function(){return [0,"rgb("];}),_HD=new T(function(){return [0,"rgba("];}),_HE=new T(function(){return [0,toJSStr(_d)];}),_HF=[0,41],_HG=[1,_HF,_d],_HH=new T(function(){return [0,toJSStr(_HG)];}),_HI=[1,_HH,_d],_HJ=[0,44],_HK=[1,_HJ,_d],_HL=new T(function(){return [0,toJSStr(_HK)];}),_HM=function(_HN){var _HO=String(E(_HN)[1]),_HP=_HO;return [0,_HP];},_HQ=function(_HR){var _HS=E(_HR);if(!_HS[0]){var _HT=jsCat([1,_HC,[1,new T(function(){return B(_HM(_HS[1]));}),[1,_HL,[1,new T(function(){return B(_HM(_HS[2]));}),[1,_HL,[1,new T(function(){return B(_HM(_HS[3]));}),_HI]]]]]],E(_HE)[1]),_HU=_HT;return E(_HU);}else{var _HV=jsCat([1,_HD,[1,new T(function(){return B(_HM(_HS[1]));}),[1,_HL,[1,new T(function(){return B(_HM(_HS[2]));}),[1,_HL,[1,new T(function(){return B(_HM(_HS[3]));}),[1,_HL,[1,new T(function(){return B(_HM(_HS[4]));}),_HI]]]]]]]],E(_HE)[1]),_HW=_HV;return E(_HW);}},_HX=[0,228],_HY=[0,173],_HZ=[0,38],_I0=[0,_HZ,_HY,_HX],_I1=new T(function(){return [0,"fillStyle"];}),_I2=[0,81],_I3=[0,231],_I4=[0,209],_I5=[0,_I4,_I3,_I2],_I6=function(_I7,_I8,_I9,_Ia,_){var _Ib=jsMoveTo(_Ia,_I7+_I9,_I8),_Ic=jsArc(_Ia,_I7,_I8,_I9,0,6.283185307179586);return _iG;},_Id=function(_Ie,_){return new F(function(){return _I6(0,0,20,E(_Ie)[1],_);});},_If=function(_Ig){return function(_Ih,_){var _Ii=E(_Ih)[1],_Ij=jsSet(_Ii,E(_I1)[1],E(new T(function(){if(!E(_Ig)){var _Ik=[0,B(_HQ(_I0))];}else{var _Ik=[0,B(_HQ(_I5))];}return _Ik;}))[1]),_Il=jsBeginPath(_Ii),_Im=jsMoveTo(_Ii,20,0),_In=jsArc(_Ii,0,0,20,0,6.283185307179586),_Io=jsFill(_Ii);return new F(function(){return _H7(_Id,_Ii,_);});};},_Ip=function(_Iq){return function(_Ir,_){var _Is=jsSet(E(_Ir)[1],E(_I1)[1],E(new T(function(){return [0,B(_HQ(_Iq))];}))[1]);return _iG;};},_It=[0,255],_Iu=[0,_It,_It,_It],_Iv=new T(function(){return B(_Ip(_Iu));}),_Iw=[0,22],_Ix=new T(function(){return B(_Hh(_Iw));}),_Iy=function(_Iz,_IA){return function(_IB,_){var _IC=E(_IB),_ID=_IC[1],_IE=jsSet(_ID,E(_I1)[1],E(new T(function(){if(!E(_Iz)){var _IF=[0,B(_HQ(_I0))];}else{var _IF=[0,B(_HQ(_I5))];}return _IF;}))[1]),_IG=jsBeginPath(_ID),_IH=jsMoveTo(_ID,28,0),_II=jsArc(_ID,0,0,28,0,6.283185307179586),_IJ=jsFill(_ID),_IK=jsBeginPath(_ID),_IL=jsMoveTo(_ID,28,0),_IM=jsArc(_ID,0,0,28,0,6.283185307179586),_IN=jsStroke(_ID),_IO=B(A(_Iv,[_IC,_])),_IP=_IO,_IQ=jsBeginPath(_ID),_IR=jsMoveTo(_ID,22,0),_IS=jsArc(_ID,0,0,22,0,6.283185307179586),_IT=jsFill(_ID),_IU=jsBeginPath(_ID),_IV=jsMoveTo(_ID,22,0),_IW=jsArc(_ID,0,0,22,0,6.283185307179586),_IX=jsStroke(_ID);return !E(_IA)?_iG:B(A(_Ix,[_IC,_]));};},_IY=function(_IZ){return function(_J0,_){var _J1=B(_Gr(_Hy,_J0,_)),_J2=_J1;return new F(function(){return A(new T(function(){var _J3=function(_J4){var _J5=E(_J4);if(!_J5[0]){return E(_HA);}else{var _J6=_J5[1],_J7=function(_J8,_J9,_){var _Ja=E(_J8),_Jb=60*E(_Ja[1])[1];return new F(function(){return _Gh(0.5*Math.sqrt(3)*_Jb+300, -(60*E(_Ja[2])[1])+0.5*_Jb+315,new T(function(){return B(_Iy(_J6,_eX));}),E(_J9)[1],_);});},_Jc=function(_Jd,_Je,_){var _Jf=E(_Jd),_Jg=60*E(_Jf[1])[1];return new F(function(){return _Gh(0.5*Math.sqrt(3)*_Jg+300, -(60*E(_Jf[2])[1])+0.5*_Jg+315,new T(function(){return B(_If(_J6));}),E(_Je)[1],_);});};return function(_Jh,_){var _Ji=B(A(new T(function(){if(!E(_J6)){var _Jj=function(_oB,_Jk){return new F(function(){return (function(_Jl,_Jm,_){while(1){var _Jn=E(_Jl);if(!_Jn[0]){return _iG;}else{var _Jo=B(_Jc(_Jn[1],_Jm,_)),_Jp=_Jo;_Jl=_Jn[2];continue;}}})(E(_IZ)[4],_oB,_Jk);});};}else{var _Jj=function(_oB,_Jk){return new F(function(){return (function(_Jq,_Jr,_){while(1){var _Js=E(_Jq);if(!_Js[0]){return _iG;}else{var _Jt=B(_Jc(_Js[1],_Jr,_)),_Ju=_Jt;_Jq=_Js[2];continue;}}})(E(_IZ)[5],_oB,_Jk);});};}return _Jj;}),[_Jh,_])),_Jv=_Ji,_Jw=B(A(new T(function(){if(!E(_J6)){var _Jx=function(_oB,_Jk){return new F(function(){return (function(_Jy,_Jz,_){while(1){var _JA=E(_Jy);if(!_JA[0]){return _iG;}else{var _JB=B(_J7(_JA[1],_Jz,_)),_JC=_JB;_Jy=_JA[2];continue;}}})(E(_IZ)[2],_oB,_Jk);});};}else{var _Jx=function(_oB,_Jk){return new F(function(){return (function(_JD,_JE,_){while(1){var _JF=E(_JD);if(!_JF[0]){return _iG;}else{var _JG=B(_J7(_JF[1],_JE,_)),_JH=_JG;_JD=_JF[2];continue;}}})(E(_IZ)[3],_oB,_Jk);});};}return _Jx;}),[_Jh,_])),_JI=_Jw;return new F(function(){return A(new T(function(){return B(_J3(_J5[2]));}),[_Jh,_]);});};}};return B((function(_JJ){return function(_JK,_){var _JL=B(A(new T(function(){return function(_oB,_Jk){return new F(function(){return (function(_JM,_JN,_){while(1){var _JO=E(_JM);if(!_JO[0]){return _iG;}else{var _JP=E(_JO[1]),_JQ=60*E(_JP[1])[1],_JR=E(_JN),_JS=_JR[1],_JT=jsPushState(_JS),_JU=jsTranslate(_JS,0.5*Math.sqrt(3)*_JQ+300, -(60*E(_JP[2])[1])+0.5*_JQ+315),_JV=B(A(new T(function(){return B(_If(_d4));}),[[0,_JS],_])),_JW=_JV,_JX=jsPopState(_JS);_JM=_JO[2];_JN=_JR;continue;}}})(E(_IZ)[4],_oB,_Jk);});};}),[_JK,_])),_JY=_JL,_JZ=B(A(new T(function(){return function(_oB,_Jk){return new F(function(){return (function(_K0,_K1,_){while(1){var _K2=E(_K0);if(!_K2[0]){return _iG;}else{var _K3=E(_K2[1]),_K4=60*E(_K3[1])[1],_K5=E(_K1),_K6=_K5[1],_K7=jsPushState(_K6),_K8=jsTranslate(_K6,0.5*Math.sqrt(3)*_K4+300, -(60*E(_K3[2])[1])+0.5*_K4+315),_K9=B(A(new T(function(){return B(_Iy(_d4,_eX));}),[[0,_K6],_])),_Ka=_K9,_Kb=jsPopState(_K6);_K0=_K2[2];_K1=_K5;continue;}}})(E(_IZ)[2],_oB,_Jk);});};}),[_JK,_])),_Kc=_JZ;return new F(function(){return A(new T(function(){return B(_J3(_JJ));}),[_JK,_]);});};})(_Hz));}),[_J0,_]);});};},_Kd=function(_Ke,_Kf,_Kg){var _Kh=E(_Kg)==0?E(_Ke):E(_Kf),_Ki=function(_Kj){while(1){var _Kk=(function(_Kl){var _Km=E(_Kl);if(!_Km[0]){return E(_HA);}else{var _Kn=_Km[1],_Ko=_Km[2];if(!B(_fT(_Kh,_Kn))){_Kj=_Ko;return null;}else{return function(_Kp,_){var _Kq=E(_Kn),_Kr=E(_Kp),_Ks=_Kr[1],_Kt=jsPushState(_Ks),_Ku=60*E(_Kq[1])[1],_Kv=jsTranslate(_Ks,0.5*Math.sqrt(3)*_Ku+300, -(60*E(_Kq[2])[1])+0.5*_Ku+315),_Kw=jsBeginPath(_Ks),_Kx=jsMoveTo(_Ks,22,0),_Ky=jsArc(_Ks,0,0,22,0,6.283185307179586),_Kz=jsFill(_Ks),_KA=jsPopState(_Ks);return new F(function(){return A(new T(function(){return B(_Ki(_Ko));}),[_Kr,_]);});};}}})(_Kj);if(_Kk!=null){return _Kk;}}};return new F(function(){return _Ki(_Kh);});},_KB=function(_KC,_KD,_KE){return new F(function(){return A(_KC,[_KE,new T(function(){return B(_KB(_KC,_KD,new T(function(){return B(A(_KD,[_KE]));})));})]);});},_KF=[0,45],_KG=new T(function(){return [0,0.5*Math.sqrt(3)*(-300)+300];}),_KH=[0,_KG,_KF],_KI=[0,585],_KJ=new T(function(){return [0,0.5*Math.sqrt(3)*300+300];}),_KK=[0,_KJ,_KI],_KL=function(_KM,_KN){if(_KN>0){var _KO=function(_KP,_KQ,_KR,_KS,_){var _KT=jsPushState(_KS),_KU=jsTranslate(_KS,_KP,_KQ),_KV=B(A(new T(function(){return B(_Iy(_KM,_qA));}),[[0,_KS],_])),_KW=_KV,_KX=jsPopState(_KS);return new F(function(){return A(_KR,[[0,_KS],_]);});};return new F(function(){return A(_KB,[function(_KY,_KZ,_L0){return _L0>1?function(_L1,_){var _L2=E(_KY);return new F(function(){return _KO(E(_L2[1])[1],E(_L2[2])[1],new T(function(){return B(A(_KZ,[_L0-1|0]));}),E(_L1)[1],_);});}:function(_L3,_){var _L4=E(_KY);return new F(function(){return _KO(E(_L4[1])[1],E(_L4[2])[1],_HA,E(_L3)[1],_);});};},function(_L5){if(!E(_KM)){var _L6=E(_L5);return [0,new T(function(){return [0,E(_L6[1])[1]-20];}),_L6[2]];}else{var _L7=E(_L5);return [0,new T(function(){return [0,E(_L7[1])[1]+20];}),_L7[2]];}},new T(function(){return E(_KM)==0?E(_KK):E(_KH);}),_KN]);});}else{return E(_HA);}},_L8=function(_L9,_La,_Lb){return function(_Lc,_){var _Ld=jsDrawText(E(_Lc)[1],E(new T(function(){return [0,toJSStr(E(_Lb))];}))[1],E(_L9)[1],E(_La)[1]);return _iG;};},_Le=new T(function(){return [0,"font"];}),_Lf=function(_Lg,_Lh){return function(_Li,_){var _Lj=E(_Li),_Lk=_Lj[1],_Ll=E(_Le)[1],_Lm=jsGet(_Lk,_Ll),_Ln=_Lm,_Lo=jsSet(_Lk,_Ll,E(new T(function(){return [0,toJSStr(E(_Lg))];}))[1]),_Lp=B(A(_Lh,[_Lj,_])),_Lq=_Lp,_Lr=jsSet(_Lk,_Ll,_Ln);return _iG;};},_Ls=new T(function(){return B(unCStr("15px \'Open Sans\', sans-serif"));}),_Lt=new T(function(){return B(unCStr("Floyd is thinking ..."));}),_Lu=new T(function(){return [0,toJSStr(E(_Lt))];}),_Lv=function(_Lw,_){var _Lx=jsDrawText(E(_Lw)[1],E(_Lu)[1],420,20);return _iG;},_Ly=new T(function(){return B(_Lf(_Ls,_Lv));}),_Lz=new T(function(){return B(unCStr("Floyd wins!"));}),_LA=new T(function(){return [0,toJSStr(E(_Lz))];}),_LB=function(_LC,_){var _LD=jsDrawText(E(_LC)[1],E(_LA)[1],420,20);return _iG;},_LE=new T(function(){return B(_Lf(_Ls,_LB));}),_LF=new T(function(){return B(unCStr("You win!"));}),_LG=new T(function(){return [0,toJSStr(E(_LF))];}),_LH=function(_LI,_){var _LJ=jsDrawText(E(_LI)[1],E(_LG)[1],420,20);return _iG;},_LK=new T(function(){return B(_Lf(_Ls,_LH));}),_LL=[1,_2t,_d],_LM=[0,550],_LN=[0,620],_LO=new T(function(){return B(_7u(_7F,_7F));}),_LP=[0,0],_LQ=[0,_LP,_LP,_LP],_LR=new T(function(){return B(_Ip(_LQ));}),_LS=function(_LT,_LU,_LV,_){while(1){var _LW=E(_LT);if(!_LW[0]){return _iG;}else{var _LX=E(_LW[1]),_LY=jsPushState(_LU),_LZ=60*E(_LX[1])[1],_M0=jsTranslate(_LU,0.5*Math.sqrt(3)*_LZ+300, -(60*E(_LX[2])[1])+0.5*_LZ+315),_M1=B(A(_LR,[[0,_LU],_])),_M2=_M1,_M3=jsBeginPath(_LU),_M4=jsMoveTo(_LU,5,0),_M5=jsArc(_LU,0,0,5,0,6.283185307179586),_M6=jsFill(_LU),_M7=jsPopState(_LU);_LT=_LW[2];var _M8=_;_LV=_M8;continue;}}},_M9=function(_Ma,_Mb,_Mc,_){while(1){var _Md=E(_Ma);if(!_Md[0]){return _iG;}else{var _Me=E(_Md[1]),_Mf=jsPushState(_Mb),_Mg=60*E(_Me[1])[1],_Mh=jsTranslate(_Mb,0.5*Math.sqrt(3)*_Mg+300, -(60*E(_Me[2])[1])+0.5*_Mg+315),_Mi=jsBeginPath(_Mb),_Mj=jsMoveTo(_Mb,22,0),_Mk=jsArc(_Mb,0,0,22,0,6.283185307179586),_Ml=jsFill(_Mb),_Mm=jsPopState(_Mb);_Ma=_Md[2];var _Mn=_;_Mc=_Mn;continue;}}},_Mo=[0,0.5],_Mp=[1,_It,_LP,_LP,_Mo],_Mq=new T(function(){return B(_Ip(_Mp));}),_Mr=function(_Ms,_Mt,_Mu,_Mv){var _Mw=E(_Mt);switch(_Mw[0]){case 0:var _Mx=E(_Mu),_My=_Mx[1],_Mz=_Mx[2];if(!B(_el(_My,_Mz,E(_Ms)[1]))){var _MA=new T(function(){return [0,60*E(_My)[1]];});return function(_MB,_){return new F(function(){return _Gh(E(new T(function(){return [0,0.5*Math.sqrt(3)*E(_MA)[1]+300];}))[1],E(new T(function(){return [0, -(60*E(_Mz)[1])+0.5*E(_MA)[1]+315];}))[1],new T(function(){return B(_Iy(_Mv,_eX));}),E(_MB)[1],_);});};}else{return E(_HA);}break;case 1:return !B(_eC(_LO,_Mu,B(_gU(_Mv,_Ms))))?E(_HA):function(_MC,_){var _MD=E(new T(function(){var _ME=E(_Mu),_MF=new T(function(){return [0,60*E(_ME[1])[1]];});return [0,new T(function(){return [0,0.5*Math.sqrt(3)*E(_MF)[1]+300];}),new T(function(){return [0, -(60*E(_ME[2])[1])+0.5*E(_MF)[1]+315];})];}));return new F(function(){return _Gh(E(_MD[1])[1],E(_MD[2])[1],new T(function(){return B(_If(_Mv));}),E(_MC)[1],_);});};case 2:var _MG=new T(function(){return B(_gK(_Ms,_Mw[1]));}),_MH=new T(function(){if(!B(_eC(_LO,_Mu,_MG))){var _MI=E(_HA);}else{var _MI=function(_MJ,_){var _MK=E(new T(function(){var _ML=E(_Mu),_MM=new T(function(){return [0,60*E(_ML[1])[1]];});return [0,new T(function(){return [0,0.5*Math.sqrt(3)*E(_MM)[1]+300];}),new T(function(){return [0, -(60*E(_ML[2])[1])+0.5*E(_MM)[1]+315];})];}));return new F(function(){return _Gh(E(_MK[1])[1],E(_MK[2])[1],new T(function(){return B(_Iy(_Mv,_eX));}),E(_MJ)[1],_);});};}return _MI;});return function(_MN,_){var _MO=E(_MG);if(!_MO[0]){return new F(function(){return A(_MH,[_MN,_]);});}else{var _MP=E(_MO[1]),_MQ=E(_MN),_MR=_MQ[1],_MS=jsPushState(_MR),_MT=60*E(_MP[1])[1],_MU=jsTranslate(_MR,0.5*Math.sqrt(3)*_MT+300, -(60*E(_MP[2])[1])+0.5*_MT+315),_MV=B(A(_LR,[[0,_MR],_])),_MW=_MV,_MX=jsBeginPath(_MR),_MY=jsMoveTo(_MR,5,0),_MZ=jsArc(_MR,0,0,5,0,6.283185307179586),_N0=jsFill(_MR),_N1=jsPopState(_MR),_N2=B(_LS(_MO[2],_MR,_,_)),_N3=_N2;return new F(function(){return A(_MH,[_MQ,_]);});}};case 3:return function(_N4,_){var _N5=B(A(_Mq,[_N4,_])),_N6=_N5,_N7=E(new T(function(){return B(_gX(new T(function(){if(!E(_Mv)){var _N8=E(E(_Ms)[4]);}else{var _N8=E(E(_Ms)[5]);}return _N8;}),_Mu));}));if(!_N7[0]){return _iG;}else{var _N9=E(_N7[1]),_Na=E(_N4)[1],_Nb=jsPushState(_Na),_Nc=60*E(_N9[1])[1],_Nd=jsTranslate(_Na,0.5*Math.sqrt(3)*_Nc+300, -(60*E(_N9[2])[1])+0.5*_Nc+315),_Ne=jsBeginPath(_Na),_Nf=jsMoveTo(_Na,22,0),_Ng=jsArc(_Na,0,0,22,0,6.283185307179586),_Nh=jsFill(_Na),_Ni=jsPopState(_Na);return new F(function(){return _M9(_N7[2],_Na,_,_);});}};default:return E(_HA);}},_Nj=function(_Nk,_Nl){var _Nm=new T(function(){var _Nn=E(_Nl);if(!_Nn[0]){var _No=E(_HA);}else{var _Np=_Nn[1],_Nq=E(_Nk),_Nr=_Nq[1];if(E(E(_Nq[4])[1])==3){var _Ns=E(_HA);}else{if(E(E(_Nq[5])[1])==3){var _Nt=E(_HA);}else{var _Nt=function(_Nu,_){var _Nv=B(A(new T(function(){return B(_Lf(_Ls,new T(function(){return B(_L8(_LM,_LN,new T(function(){var _Nw=E(_Np);return [1,_2u,new T(function(){return B(A(_2H,[_29,[1,function(_Nx){return new F(function(){return _2v(0,E(_Nw[1])[1],_Nx);});},[1,function(_Ny){return new F(function(){return _2v(0,E(_Nw[2])[1],_Ny);});},_d]],_LL]));})];})));})));}),[_Nu,_])),_Nz=_Nv,_NA=B(A(new T(function(){return B(_Mr(_Nq[3],_Nq[2],_Np,_Nr));}),[_Nu,_])),_NB=_NA;return E(_Nr)==0?_iG:B(A(_Ly,[_Nu,_]));};}var _NC=_Nt,_Ns=_NC;}var _ND=_Ns,_NE=_ND,_No=_NE;}return _No;}),_NF=new T(function(){var _NG=E(_Nk),_NH=_NG[3];if(E(_NG[2])[0]==3){var _NI=function(_NJ){var _NK=E(_NJ);return _NK[0]==0?E(_HA):function(_NL,_){var _NM=B(A(new T(function(){var _NN=E(_NH);return B(_Kd(_NN[4],_NN[5],_NK[1]));}),[_NL,_])),_NO=_NM;return new F(function(){return A(new T(function(){return B(_NI(_NK[2]));}),[_NL,_]);});};},_NP=B((function(_NQ,_NR){return function(_NS,_){var _NT=B(A(new T(function(){var _NU=E(_NH);return B(_Kd(_NU[4],_NU[5],_NQ));}),[_NS,_])),_NV=_NT;return new F(function(){return A(new T(function(){return B(_NI(_NR));}),[_NS,_]);});};})(_d4,_Hz));}else{var _NP=E(_HA);}var _NW=_NP;return _NW;}),_NX=new T(function(){return B(_KL(_d6,E(E(_Nk)[5])[1]));}),_NY=new T(function(){return B(_KL(_d4,E(E(_Nk)[4])[1]));}),_NZ=new T(function(){return B(_IY(new T(function(){return E(E(_Nk)[3]);})));});return function(_O0,_){var _O1=E(_Nk);if(E(E(_O1[4])[1])==3){var _O2=B(A(_LK,[_O0,_])),_O3=_O2,_O4=B(A(_NZ,[_O0,_])),_O5=_O4,_O6=B(A(_NY,[_O0,_])),_O7=_O6,_O8=B(A(_NX,[_O0,_])),_O9=_O8,_Oa=B(A(_NF,[_O0,_])),_Ob=_Oa;return new F(function(){return A(_Nm,[_O0,_]);});}else{if(E(E(_O1[5])[1])==3){var _Oc=B(A(_LE,[_O0,_])),_Od=_Oc,_Oe=B(A(_NZ,[_O0,_])),_Of=_Oe,_Og=B(A(_NY,[_O0,_])),_Oh=_Og,_Oi=B(A(_NX,[_O0,_])),_Oj=_Oi,_Ok=B(A(_NF,[_O0,_])),_Ol=_Ok;return new F(function(){return A(_Nm,[_O0,_]);});}else{var _Om=B(A(_NZ,[_O0,_])),_On=_Om,_Oo=B(A(_NY,[_O0,_])),_Op=_Oo,_Oq=B(A(_NX,[_O0,_])),_Or=_Oq,_Os=B(A(_NF,[_O0,_])),_Ot=_Os;return new F(function(){return A(_Nm,[_O0,_]);});}}};},_Ou=function(_Ov){while(1){var _Ow=(function(_Ox){var _Oy=E(_Ox);if(!_Oy[0]){return [0];}else{var _Oz=_Oy[2],_OA=E(_Oy[1]);if(!E(_OA[2])[0]){return [1,_OA[1],new T(function(){return B(_Ou(_Oz));})];}else{_Ov=_Oz;return null;}}})(_Ov);if(_Ow!=null){return _Ow;}}},_OB=function(_OC,_){return _OC;},_OD=function(_){var _=0,_OE=jsMkStdout(),_OF=_OE;return [0,_OF];},_OG=function(_OH){var _OI=B(A(_OH,[_])),_OJ=_OI;return E(_OJ);},_OK=new T(function(){return B(_OG(_OD));}),_OL=function(_OM,_ON,_){var _OO=B(A(_OM,[_])),_OP=_OO;return new F(function(){return A(_ON,[_]);});},_OQ=function(_,_OR){var _OS=E(_OR);if(!_OS[0]){return new F(function(){return _1S(_jK,_);});}else{var _OT=jsFind("canvas"),_OU=_OT,_OV=E(_OU);if(!_OV[0]){return new F(function(){return _1S(_jL,_);});}else{var _OW=nMV(_Gg),_OX=_OW,_OY=E(_OS[1]),_OZ=_OY[1],_P0=E(_OY[2])[1],_P1=jsResetCanvas(_P0),_P2=B(A(_IY,[_jp,_OZ,_])),_P3=_P2,_P4=E(_OV[1])[1],_P5=jsSetCB(_P4,E(_jr)[1],function(_P6,_){var _P7=rMV(_OX),_P8=_P7,_P9=E(_P8),_Pa=E(_P9[1]);if(!_Pa[0]){return new F(function(){return _1S(_jf,_);});}else{switch(E(_P9[2])[0]){case 0:var _Pb=jsResetCanvas(_P0);return new F(function(){return A(_Nj,[_Pa[1],[1,new T(function(){return B(_jm(_P6));})],_OZ,_]);});break;case 1:return _iG;case 2:return _iG;default:return _iG;}}}),_Pc=_P5,_Pd=jsSetCB(_P4,E(_jq)[1],function(_Pe,_){switch(E(E(_Pe)[1])){case 37:var _Pf=rMV(_OX),_Pg=_Pf,_Ph=E(_Pg),_Pi=_Ph[1],_Pj=_Ph[2],_Pk=B(_6V(_Pi,0));if(_Pk<=1){return _iG;}else{var _Pl=function(_Pm){var _=wMV(_OX,[0,_Pi,_jN]),_Pn=jsResetCanvas(_P0);return new F(function(){return A(_Nj,[new T(function(){return B(_5(_Pi,1));}),_0,_OZ,_]);});},_Po=function(_Pp){var _Pq=E(_Pj);if(_Pq[0]==3){var _Pr=E(_Pq[1])[1];if((_Pr+1|0)>=_Pk){return _iG;}else{var _=wMV(_OX,[0,_Pi,[3,[0,_Pr+1|0]]]),_Ps=jsResetCanvas(_P0),_Pt=new T(function(){var _Pu=_Pr+1|0;return _Pu>=0?B(_5(_Pi,_Pu)):E(_2);}),_Pv=B(A(_Nj,[_Pt,_0,_OZ,_])),_Pw=_Pv;return new F(function(){return _jz(_OK,B(unAppCStr("DEBUG: let gs = ",new T(function(){var _Px=E(_Pt);return B(A(_4F,[0,_Px[1],_Px[2],_Px[3],_Px[4],_Px[5],_d]));}))),_);});}}else{return _iG;}};switch(E(_Pj)[0]){case 0:var _Py=B(_Pl(_)),_Pz=_Py;return _iG;case 1:var _PA=B(_Po(_)),_PB=_PA;return _iG;case 2:var _PC=B(_Pl(_)),_PD=_PC;return _iG;default:var _PE=B(_Po(_)),_PF=_PE;return _iG;}}break;case 39:var _PG=rMV(_OX),_PH=_PG,_PI=E(_PH),_PJ=_PI[1],_PK=E(_PI[2]);if(_PK[0]==3){var _PL=_PK[1],_=wMV(_OX,[0,_PJ,new T(function(){var _PM=E(E(_PL)[1]);if(_PM==1){var _PN=[0];}else{var _PN=[3,[0,_PM-1|0]];}var _PO=_PN;return _PO;})]),_PP=jsResetCanvas(_P0),_PQ=new T(function(){var _PR=E(_PL)[1]-1|0;return _PR>=0?B(_5(_PJ,_PR)):E(_2);}),_PS=B(A(_Nj,[_PQ,_0,_OZ,_])),_PT=_PS;return new F(function(){return _jz(_OK,B(unAppCStr("DEBUG: let gs = ",new T(function(){var _PU=E(_PQ);return B(A(_4F,[0,_PU[1],_PU[2],_PU[3],_PU[4],_PU[5],_d]));}))),_);});}else{return _iG;}break;default:return _iG;}}),_PV=_Pd,_PW=jsSetCB(_P4,E(_js)[1],function(_PX,_PY,_){var _PZ=rMV(_OX),_Q0=_PZ,_Q1=E(_Q0),_Q2=E(_Q1[1]);if(!_Q2[0]){return new F(function(){return _1S(_jg,_);});}else{switch(E(_Q1[2])[0]){case 0:var _Q3=jsResetCanvas(_P0),_Q4=new T(function(){var _Q5=E(_Q2[1]),_Q6=B(_hd(_Q5[1],_Q5[2],_Q5[3],_Q5[4],_Q5[5],new T(function(){return B(_jm(_PY));})));return _Q6[0]==0?E(_Q5):E(_Q6[1]);}),_Q7=[1,new T(function(){var _Q8=E(_PY);return B(_6B(_Q8[1],_Q8[2]));})],_Q9=B(A(_Nj,[_Q4,_Q7,_OZ,_])),_Qa=_Q9,_Qb=B(_jz(_OK,B(unAppCStr("DEBUG: let gs = ",new T(function(){var _Qc=E(_Q4);return B(A(_4F,[0,_Qc[1],_Qc[2],_Qc[3],_Qc[4],_Qc[5],_d]));}))),_)),_Qd=_Qb,_Qe=E(_Q4),_Qf=_Qe[4],_Qg=_Qe[5];if(!E(_Qe[1])){var _=wMV(_OX,[0,[1,_Qe,_Q2],new T(function(){if(E(E(_Qf)[1])==3){var _Qh=[2];}else{var _Qh=E(E(_Qg)[1])==3?[2]:[0];}var _Qi=_Qh;return _Qi;})]);return _iG;}else{var _Qj=E(_Qf);if(E(_Qj[1])==3){var _=wMV(_OX,[0,[1,_Qe,_Q2],_jd]);return _iG;}else{var _Qk=E(_Qg);if(E(_Qk[1])==3){var _=wMV(_OX,[0,[1,_Qe,_Q2],_jd]);return _iG;}else{var _=wMV(_OX,[0,[1,_Qe,_Q2],_je]),_Ql=B(_iS(_jh,_OL,_OB,_1W,_jG,_jc,_jI,[1,[0,_jJ,new T(function(){return B(A(_4F,[0,_d6,_Qe[2],_Qe[3],_Qj,_Qk,_d]));})],_d],function(_Qm,_){var _Qn=E(_Qm);if(!_Qn[0]){return _iG;}else{var _Qo=jsResetCanvas(_P0),_Qp=new T(function(){var _Qq=B(_Ou(B(_nm(_Gc,_Qn[1]))));return _Qq[0]==0?E(_jP):E(_Qq[2])[0]==0?E(_Qq[1]):E(_jR);}),_Qr=B(A(_Nj,[_Qp,_Q7,_OZ,_])),_Qs=_Qr,_=wMV(_OX,[0,[1,_Qp,[1,_Qe,_Q2]],new T(function(){var _Qt=E(_Qp);if(E(E(_Qt[4])[1])==3){var _Qu=[2];}else{var _Qu=E(E(_Qt[5])[1])==3?[2]:[0];}var _Qv=_Qu,_Qw=_Qv;return _Qw;})]);return _iG;}})),_Qx=jsSetTimeout(0,_Ql);return _iG;}}}break;case 1:return _iG;case 2:return _iG;default:return _iG;}}}),_Qy=_PW;return _iG;}}},_Qz=function(_){var _QA=jsFind("canvas"),_QB=_QA,_QC=E(_QB);if(!_QC[0]){return new F(function(){return _OQ(_,_0);});}else{var _QD=E(_QC[1])[1],_QE=jsHasCtx2D(_QD),_QF=_QE;if(!E(_QF)){return new F(function(){return _OQ(_,_0);});}else{var _QG=jsGetCtx2D(_QD),_QH=_QG;return new F(function(){return _OQ(_,[1,[0,[0,_QH],[0,_QD]]]);});}}},_QI=function(_){return new F(function(){return _Qz(_);});};
-var hasteMain = function() {B(A(_QI, [0]));};window.onload = hasteMain;
diff --git a/frontend/Main.hs b/frontend/Main.hs
deleted file mode 100644
index 0282ae4..0000000
--- a/frontend/Main.hs
+++ /dev/null
@@ -1,323 +0,0 @@
-module Main where
-
-import Data.List (minimumBy)
-import Data.Ord (comparing)
-import Data.IORef
-import Control.Monad (when, forM_)
-import Data.Maybe (isJust, fromJust, fromMaybe)
-import Haste (elemById, onEvent, Event(..), setTimeout)
-import Haste.Ajax (textRequest, Method(..))
-import Haste.Graphics.Canvas
-
-import Yinsh
-import AI
-import Floyd
-
--- testing:
--- import AIHistory
-
--- | Current state of the user interface
-data DisplayState = WaitUser | WaitAI | ViewBoard | ViewHistory Int
- deriving (Show, Eq)
-
--- | Pixel coordinate on the screen
-type ScreenCoord = (Int, Int)
-
--- Color theme (http://www.colourlovers.com/palette/15/tech_light)
-green = RGB 209 231 81
-blue = RGB 38 173 228
-white = RGB 255 255 255
-hl = RGBA 255 0 0 0.5
-black = RGB 0 0 0
-
--- Font
-frontendFont = "15px 'Open Sans', sans-serif"
-
--- Dimensions
-spacing = 60 :: Double
-markerWidth = 20 :: Double
-ringInnerRadius = 22 :: Double
-ringWidth = 6 :: Double
-originX = 600 / 2 :: Double -- Half the canvas size
-originY = 630 / 2 :: Double
-
--- Keyboard codes
-keyLeft = 37
-keyRight = 39
-
--- | Translate hex coordinates to screen coordinates
-screenPoint :: YCoord -> Point
-screenPoint (ya, yb) = (0.5 * sqrt 3 * x' + originX, - y' + 0.5 * x' + originY)
- where x' = spacing * fromIntegral ya
- y' = spacing * fromIntegral yb
-
--- | All grid points as screen coordinates
-points :: [Point]
-points = map screenPoint coords
-
--- | Translate by hex coordinate
-translateC :: YCoord -> Picture () -> Picture ()
-translateC = translate . screenPoint
-
--- | Get the board coordinate which is closest to the given screen coordinate.
-closestCoord :: ScreenCoord -> YCoord
-closestCoord (xi, yi) = coords !! snd lsort
- where lind = zipWith (\p i -> (dist p, i)) points [0..]
- lsort = minimumBy (comparing fst) lind
- dist (x', y') = (x - x')^2 + (y - y')^2
- x = fromIntegral xi
- y = fromIntegral yi
-
--- | Marker and ring color for each player.
-playerColor :: Player -> Color
-playerColor B = blue
-playerColor W = green
-
--- | Update the game state after interacting at a certain coordinate. If this
--- is an illegal action, @newGameState@ returns @Nothing@ and the state is left
--- unchanged.
-updateState :: GameState -- ^ old state
- -> YCoord -- ^ clicked coordinate
- -> GameState -- ^ new state
-updateState gs cc = fromMaybe gs (newGameState gs cc)
-
--- | Specify the AI player for the frontend
-frontendAI :: AIFunction
-frontendAI = aiFloyd 3 mhNumber rhZero
-
--- | Get new game state after AI turn. This also resolves @WaitRemoveRun@ and
--- @WaitAddMarker@ turns for the *human* player.
-aiTurn' :: GameState -> GameState
-aiTurn' gs = let gs' = frontendAI gs in
- case turnMode gs' of
- (WaitRemoveRun _) -> frontendAI $ fromJust $ newGameState gs' (0, 0)
- WaitAddMarker -> frontendAI $ fromJust $ newGameState gs' (0, 0)
- _ -> gs'
-
--- Monadic code
-
-pSetPlayerColor :: Player -> Picture ()
-pSetPlayerColor = setFillColor . playerColor
-
--- | Draw ring.
-pRing :: Player -- ^ Player (for color)
- -> Bool -- ^ Draw the grid lines inside the ring?
- -> Picture ()
-pRing p drawCross = do
- -- Draw filled circle in player color
- pSetPlayerColor p
- fill circL
- stroke circL
-
- -- Draw white inner circle
- setFillColor white
- fill circS
- stroke circS
-
- -- Redraw the grid lines inside
- when drawCross $ pCross ringInnerRadius
-
- where circL = circle (0, 0) (ringInnerRadius + ringWidth)
- circS = circle (0, 0) ringInnerRadius
-
--- | Draw marker.
-pMarker :: Player -> Picture ()
-pMarker p = do
- pSetPlayerColor p
- fill circ
- stroke circ
- where circ = circle (0, 0) markerWidth
-
--- | Draw board element (ring or marker)
-pElement :: Element -> YCoord -> Picture ()
-pElement (Ring p) c = translateC c $ pRing p True
-pElement (Marker p) c = translateC c $ pMarker p
-
--- | Draw three crossing grid lines at current position
-pCross :: Double -- ^ Length of grid lines
- -> Picture ()
-pCross len = do
- l
- rotate (2 * pi / 3) l
- rotate (4 * pi / 3) l
- where l = stroke $ line (0, -len) (0, len)
-
--- | Highlight a marker on the board with a ring around it.
-pHighlightRing = fill $ circle (0, 0) (markerWidth + 2)
-
--- | Highlight markers making up a run.
-pHighlight :: Board -> Player -> Picture ()
-pHighlight b p = mapM_ (`translateC` pHighlightRing) mcH
- where mc = markers p b
- mcH = filter (partOfRun mc) mc
-
--- | Draw small black dot at current position to indicate a valid ring move.
-pDot :: Picture ()
-pDot = do setFillColor black
- fill $ circle (0, 0) 5
-
--- | Draw the whole board including the board elements.
-pBoard :: Board -> Picture ()
-pBoard b = do
- -- Draw grid
- sequence_ $ mapM translate points (pCross (0.5 * spacing))
-
- -- Draw board elements
- forM_ [B, W] $ \p -> do
- mapM_ (pElement (Marker p)) $ markers p b
- mapM_ (pElement (Ring p)) $ rings p b
-
--- | Draw elements which are specific to the current turn mode.
-pAction :: Board -- ^ Current board
- -> TurnMode -- ^ turn mode
- -> YCoord -- ^ coordinate closest to the mouse
- -> Player -- ^ active player
- -> Picture ()
-pAction b AddRing mc p = when (freeCoord b mc) $ pElement (Ring p) mc
-pAction b AddMarker mc p = when (mc `elem` rings p b) $ pElement (Marker p) mc
-pAction b (MoveRing start) mc p = do
- let allowed = ringMoves b start
- mapM_ (`translateC` pDot) allowed
- when (mc `elem` allowed) $ pElement (Ring p) mc
-pAction b (RemoveRun _) mc p = do
- let runC = runCoords (markers p b) mc
- setFillColor hl
- mapM_ (`translateC` pHighlightRing) runC
-pAction _ _ _ _ = return ()
-
--- | Draw rings which are already removed from the board
-pRings :: Player -> Int -> Picture ()
-pRings p rw =
- mapM_ ringAt cs
- where cs = take rw $ iterate (diff p) (initial p)
- initial B = screenPoint (5, -2)
- initial W = screenPoint (-5, 2)
- diff B (x, y) = (x - 20, y)
- diff W (x, y) = (x + 20, y)
- ringAt point = translate point $ pRing p False
-
--- | Render all screen elements.
-pDisplay :: GameState
- -> Maybe YCoord -- ^ Coordinate close to mouse cursor
- -> Picture ()
-pDisplay gs mmc = do
- when (terminalState gs) $
- font frontendFont $
- text (420, 20) message
-
- pBoard (board gs)
-
- pRings B (pointsB gs)
- pRings W (pointsW gs)
-
- -- Draw thick borders for markers which are part of a run
- case turnMode gs of
- (RemoveRun _) -> mapM_ (pHighlight (board gs)) [B, W]
- _ -> return ()
-
- -- Render screen action
- when (isJust mmc && not (terminalState gs)) $ do
- let (Just mc) = mmc
- -- TODO: just debugging:
- font frontendFont $
- text (550, 620) $ show mc
-
- pAction (board gs) (turnMode gs) mc (activePlayer gs)
-
- when (activePlayer gs == W) $
- font frontendFont $
- text (420, 20) "Floyd is thinking ..."
-
- where message | pointsB gs == pointsForWin = "You win!"
- | otherwise = "Floyd wins!"
-
--- | Draw on canvas.
-renderCanvas :: Canvas -> GameState -> Maybe ScreenCoord -> IO ()
-renderCanvas can gs mAction = render can $ pDisplay gs (closestCoord `fmap` mAction)
-
--- | Register IO events.
-main :: IO ()
-main = do
- Just can <- getCanvasById "canvas"
- Just ce <- elemById "canvas"
-
- let initGS = initialGameState
- -- let initGS = testGameState
- let initBoard = board initGS
-
- -- 'ioState' holds a chronological list of game states and the display
- -- state.
- let initHistory = [initGS]
- ioState <- newIORef (initHistory, WaitUser)
-
- -- draw initial board
- render can (pBoard initBoard)
-
- _ <- ce `onEvent` OnMouseMove $ \point -> do
- (gs:_, ds) <- readIORef ioState
- when (ds == WaitUser) $
- renderCanvas can gs (Just point)
-
- _ <- ce `onEvent` OnKeyDown $ \key -> do
- when (key == keyLeft) $ do
- (gslist, ds) <- readIORef ioState
- let numGS = length gslist
- when (numGS > 1) $
- if ds == WaitUser || ds == ViewBoard then do
- writeIORef ioState (gslist, ViewHistory 1)
- renderCanvas can (gslist !! 1) Nothing
- else
- case ds of
- ViewHistory h ->
- when (h + 1 < numGS) $ do
- writeIORef ioState (gslist, ViewHistory (h + 1))
- let gs = gslist !! (h + 1)
- renderCanvas can gs Nothing
- putStrLn $ "DEBUG: let gs = " ++ show gs
- _ -> return ()
- when (key == keyRight) $ do
- (gslist, ds) <- readIORef ioState
- case ds of
- ViewHistory h -> do
- let newDS = if h == 1 then WaitUser else ViewHistory (h - 1)
- writeIORef ioState (gslist, newDS)
- let gs = gslist !! (h - 1)
- renderCanvas can gs Nothing
- putStrLn $ "DEBUG: let gs = " ++ show gs
- _ -> return ()
-
- _ <- ce `onEvent` OnClick $ \_ point -> do
- (oldGS:gslist, ds) <- readIORef ioState
- when (ds == WaitUser) $ do
- let gs = updateState oldGS (closestCoord point)
- let gameover = terminalState gs
- renderCanvas can gs (Just point)
-
- putStrLn $ "DEBUG: let gs = " ++ show gs
-
- if activePlayer gs == W && not gameover
- then do -- AI turn
- writeIORef ioState (gs:oldGS:gslist, WaitAI)
- -- setTimeout 0 $ do
- -- let gs' = aiTurn' gs
- -- let gameover' = terminalState gs'
- -- let ds' = if gameover' then ViewBoard else WaitUser
- -- renderCanvas can gs' (Just point)
- -- writeIORef ioState (gs':gs:oldGS:gslist, ds')
- setTimeout 0 $
- textRequest GET "http://localhost:8000/" [("gamestate", show gs)] $
- -- textRequest GET "http://yinsh-backend.herokuapp.com/" [("gamestate", show gs)] $
- \mResponse ->
- case mResponse of
- Nothing -> return () -- TODO !!
- (Just strGS) -> do
- let gs' = read strGS
- let gameover' = terminalState gs'
- let ds' = if gameover' then ViewBoard else WaitUser
- renderCanvas can gs' (Just point)
- writeIORef ioState (gs':gs:oldGS:gslist, ds')
- else do -- users turn or game over
- let ds' = if gameover then ViewBoard else WaitUser
- writeIORef ioState (gs:oldGS:gslist, ds')
-
- return ()
diff --git a/index.html b/index.html
deleted file mode 100644
index 83e34f0..0000000
--- a/index.html
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
- Yinsh
-
-
-
-
-
-
-
-
- YINSH
-
-
-
-
- You can use the left/right arrow keys to view the history of the game.
-
- This game is written in Haskell. The code is open source .
- David Peter , 2014-2023
-
-
-
-
-
diff --git a/info/screenshot.png b/info/screenshot.png
deleted file mode 100644
index 50dadfd..0000000
Binary files a/info/screenshot.png and /dev/null differ
diff --git a/info/turn-structure.svg b/info/turn-structure.svg
deleted file mode 100644
index f42b7a0..0000000
--- a/info/turn-structure.svg
+++ /dev/null
@@ -1,675 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- image/svg+xml
-
-
-
-
-
-
-
-
- RemoveRun + RemoveRing
-
-
-
- AddMarker + MoveRing
-
-
-
- WaitRemoveRun
-
-
-
-
- 5x
-
-
-
- 5x
-
-
-
- 5x
-
-
-
- RemoveRun + RemoveRing
-
-
-
-
-
-
- AddMarker + MoveRing
-
-
-
-
- WaitRemoveRun
-
-
-
- WaitAddMarker
-
-
-
-
- 5x
-
-
-
-
-
- 5x
-
-
-
-
-
-
-
- 5x
-
-
-
-
-
-
- 5x
-
-
-
-
-
- 5x
-
-
-
-
-
-
diff --git a/main.css b/main.css
deleted file mode 100644
index f4e98f0..0000000
--- a/main.css
+++ /dev/null
@@ -1,50 +0,0 @@
-body {
- margin: 0;
- padding: 0;
-}
-
-h1 {
- margin: 0;
- padding: 0;
- text-align: center;
-
- font-family: 'Open Sans', sans-serif;
- font-weight: 800;
- font-size: 80px;
-
- color: #fff;
- text-shadow: 0 0 10px #26ADE4;
-
- background-color: #4DBCE9;
- border-bottom: 4px solid #26ADE4;
-}
-
-.small {
- font-size: 70px;
-}
-
-#center {
- margin-top: 40px;
- margin-bottom: 40px;
- text-align: center;
-}
-
-canvas {
- cursor: crosshair;
- outline: none;
-}
-
-.disclaimer {
- text-align: center;
-
- font-family: 'Open Sans', sans-serif;
- font-weight: 400;
- font-size: 15px;
- color: #aaa;
- text-align: center;
-}
-
-a {
- color: #26ADE4;
- text-decoration: none;
-}
diff --git a/src/AI.hs b/src/AI.hs
deleted file mode 100644
index 31cf34f..0000000
--- a/src/AI.hs
+++ /dev/null
@@ -1,97 +0,0 @@
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE UndecidableInstances #-}
-
-module AI where
-
-import qualified Data.Tree.Game_tree.Game_tree as GT
-import qualified Data.Tree.Game_tree.Negascout as NS
-
-import Data.Maybe (fromJust)
-import Data.List (nubBy, sort)
-
-import Yinsh
-
--- | Every AI should provide a function @ai..@, returning an AIFunction.
-type AIFunction = GameState -> GameState
-
--- | Result of an heuristic evaluation function
-type AIValue = Int
-
--- | Wrapper class for AI players which encapsules the current game state.
-class AIPlayer a where
- -- | Heuristic evaluation function for a game state. Everything is calulated
- -- from the perspective of the white player. This is sufficient since
- -- Yinsh is a zero sum game.
- valueForWhite :: a -> AIValue
-
- -- | Number of turns to look ahead in the game tree.
- getPlies :: a -> Int
-
- -- | Unwrap the gamestate inside the AI.
- getGamestate :: a -> GameState
-
- -- | Update AI with new gamestate
- update :: a -> GameState -> a
-
--- | Make the GameState (wrapped in the AIPlayer) an instance of Game_tree
--- (actually rather an instance of a node in the game tree).
-instance (AIPlayer a) => GT.Game_tree a where
- is_terminal = terminalState . getGamestate
- children ai = map (update ai) (gamestates (getGamestate ai))
- node_value ai = sign * valueForWhite ai
- where sign | activePlayer gs == W = 1
- | otherwise = -1
- gs = getGamestate ai
-
--- | Possible new game states. The input and output game states are guaranteed
--- to be in turn mode AddRing, AddMarker, RemoveRun or Wait*.
-gamestates :: GameState -> [GameState]
-gamestates gs | terminalState gs = []
- | otherwise =
- case turnMode gs of
- AddRing -> freeCoords >>= newGS gs
- AddMarker -> rings' >>= newGS gs
- (RemoveRun _) -> runCoords' >>= newGS gs
- (WaitRemoveRun _) -> [fromJust (newGameState gs (0, 0))]
- WaitAddMarker -> [fromJust (newGameState gs (0, 0))]
- (MoveRing _) -> error "This is not supposed to happen"
- (RemoveRing _) -> error "This is not supposed to happen"
- where freeCoords = filter (freeCoord (board gs)) coords -- TODO: factor out, optimize
- rings' = rings (activePlayer gs) (board gs)
- runCoords' = removeDups $ filter (partOfRun markers') coords
- markers' = markers (activePlayer gs) (board gs)
- removeDups = nubBy (\c1 c2 -> sort (runCoords markers' c1) == sort (runCoords markers' c2))
- newGS gs' c = case turnMode nextGS of
- AddRing -> [nextGS]
- AddMarker -> [nextGS]
- (RemoveRun _) -> [nextGS]
- (WaitRemoveRun _) -> [nextGS]
- WaitAddMarker -> [nextGS]
- (MoveRing start) -> ringMoves (board nextGS) start >>= newGS nextGS
- (RemoveRing _) -> rings (activePlayer nextGS) (board gs') >>= newGS nextGS
- where nextGS = fromJust $ newGameState gs' c
-
--- | Get new game state after the AI turn.
-aiTurn :: (AIPlayer ai) => ai -> GameState
-aiTurn ai = case turnMode gs of
- (WaitRemoveRun _) -> fromJust $ newGameState gs (0, 0)
- WaitAddMarker -> fromJust $ newGameState gs (0, 0)
- _ -> pv !! 1
- where pv = aiPV ai
- gs = getGamestate ai
-
--- | Get the whole principal variation.
-aiPV :: (AIPlayer ai) => ai -> [GameState]
-aiPV ai = map getGamestate gss
- where (gss, _) = NS.negascout ai ply
- ply = getPlies ai
--- TODO: negascout really seems to be the fastest. But test this for more game states
--- NS.alpha_beta_search gs ply
--- NS.principal_variation_search gs ply
-
--- | A large number for symbolizing a win.
--- Very ugly: if this number is higher than 2^31, there is an integer overflow
--- in haste/javascript, resulting in the AI playing *very* bad.
--- So 2^31 - 1 ~ 2 * 10^9 is our hardcoded magic 'huge' number.
-hugeNumber :: Int
-hugeNumber = 2147483647
diff --git a/src/Floyd.hs b/src/Floyd.hs
deleted file mode 100644
index 8535af7..0000000
--- a/src/Floyd.hs
+++ /dev/null
@@ -1,95 +0,0 @@
--- | Simply the best AI for Yinsh, seriously.
-
-module Floyd ( aiFloyd
- , mhNumber
- , rhRingMoves
- , rhConnected
- , rhControlledMarkers
- , rhCombined
- , rhZero
- )
- where
-
-import AI
-import Yinsh
-
--- TODO: adjust numbers: 5, 10
-floydHeuristic :: Floyd -> AIValue
-floydHeuristic ai | points' W >= pointsForWin = hugeNumber
- | points' B >= pointsForWin = -hugeNumber
- | otherwise = value W - value B
- where gs' = getGamestate ai
- board' = board gs'
- ap' = activePlayer gs'
- tm' = turnMode gs'
- points W = pointsW gs'
- points B = pointsB gs'
-
- points' p = points p + futurePoints p
- -- If we are in RemoveRun phase, already include the point for the
- -- active player.
- -- If we are is WaitRemoveRun, the *opponent* of the current player
- -- will necessarily have one more point next turn.
- futurePoints p = case tm' of
- (RemoveRun _) -> if ap' == p then 1 else 0
- (WaitRemoveRun _) -> if ap' == p then 0 else 1
- _ -> 0
-
- valuePoints p = 100000 * points' p
- valueMarkers = markerH ai
- valueRings = ringH ai
-
- value p = valuePoints p
- + valueMarkers board' p
- + valueRings board' p
-
-
-type MarkerHeuristic = Board -> Player -> AIValue
-type RingHeuristic = Board -> Player -> AIValue
-
-mhNumber :: MarkerHeuristic
-mhNumber b p = (10 *) $ length $ markers p b
-
-rhRingMoves :: RingHeuristic
-rhRingMoves b p = (1 *) $ sum $ map (length . ringMoves b) $ rings p b
-
-rhConnected :: RingHeuristic
-rhConnected b p = (1 *) $ length $ filter connectedToRings coords
- where connectedToRings c = any (c `connected`) (rings p b)
-
-rhControlledMarkers :: RingHeuristic
-rhControlledMarkers b p = sum $ map controlledM (rings p b)
- where controlledM :: YCoord -> Int
- controlledM start = sum $ map markersBetween endPos
- where endPos = ringMoves b start
- markersBetween end = length $ filter (isMarker b) $ coordLine start end
-
-rhCombined :: [(Int, RingHeuristic)] -> RingHeuristic
-rhCombined list b p = sum $ zipWith (*) points vs
- where (vs, hs) = unzip list
- points = map (\h -> h b p) hs
-
-rhZero :: RingHeuristic
-rhZero _ _ = 0
-
-data Floyd = Floyd { gs :: GameState
- , plies :: Int
- , markerH :: MarkerHeuristic
- , ringH :: RingHeuristic
- }
-
-instance AIPlayer Floyd where
- valueForWhite = floydHeuristic
- getGamestate = gs
- getPlies = plies
- update ai gs' = ai { gs = gs' }
-
-mkFloyd :: Int -> MarkerHeuristic -> RingHeuristic -> GameState -> Floyd
-mkFloyd plies' mh' rh' gs' = Floyd { gs = gs'
- , plies = plies'
- , markerH = mh'
- , ringH = rh'
- }
-
-aiFloyd :: Int -> MarkerHeuristic -> RingHeuristic -> AIFunction
-aiFloyd plies' mh' rh' gs' = aiTurn $ mkFloyd plies' mh' rh' gs'
diff --git a/src/Pink.hs b/src/Pink.hs
deleted file mode 100644
index 3cc7380..0000000
--- a/src/Pink.hs
+++ /dev/null
@@ -1,70 +0,0 @@
--- | Trying to complete with the best
-
-module Pink ( aiPink
- -- , mhNumber
- -- , rhRingMoves
- -- , rhConnected
- -- , rhZero
- )
- where
-
-import AI
-import Yinsh
-
-pinkHeuristic :: Pink -> AIValue
-pinkHeuristic ai | points W >= pointsForWin = hugeNumber
- | points B >= pointsForWin = -hugeNumber
- | otherwise = value W - value B
- where gs' = getGamestate ai
- points W = pointsW gs'
- points B = pointsB gs'
- board' = board gs'
-
- valueMarkers = markerH ai
- valueRings = ringH ai
-
- value p = valuePoints p
- + valueMarkers board' p
- + valueRings board' p
-
- valuePoints p = 100000 * points p
-
-type MarkerHeuristic = Board -> Player -> AIValue
-type RingHeuristic = Board -> Player -> AIValue
-
-mhNumber :: MarkerHeuristic
-mhNumber b p = (10 *) $ length $ markers p b
-
-rhRingMoves :: RingHeuristic
-rhRingMoves b p = (1 *) $ sum $ map (length . ringMoves b) $ rings p b
-
-rhConnected :: RingHeuristic
-rhConnected b p = (1 *) $ length $ filter connectedToRings coords
- where connectedToRings c = any (c `connected`) (rings p b)
-
--- rhControlledMarkers :: RingHeuristic
--- rhControlledMarkers b p = length $ nub $ controlledM =<< (rings p b)
--- controlledM :: YCoord -> [YCoord]
--- controlledM c = ringMoves b c
-
-rhZero :: RingHeuristic
-rhZero _ _ = 0
-
-data Pink = Pink { gs :: GameState
- , plies :: Int
- , markerH :: MarkerHeuristic
- , ringH :: RingHeuristic
- }
-
-instance AIPlayer Pink where
- valueForWhite = pinkHeuristic
- getGamestate = gs
- getPlies = plies
- update ai gs' = ai { gs = gs' }
-
-aiPink :: Int -> MarkerHeuristic -> RingHeuristic -> AIFunction
-aiPink plies' mh' rh' gs' = aiTurn Pink { gs = gs'
- , plies = plies'
- , markerH = mh'
- , ringH = rh'
- }
diff --git a/src/RaiCharles.hs b/src/RaiCharles.hs
deleted file mode 100644
index f6117c2..0000000
--- a/src/RaiCharles.hs
+++ /dev/null
@@ -1,22 +0,0 @@
--- | Random AI (RAI) Charles, playing not really randomly.
-
-module RaiCharles (aiRaiCharles) where
-
-import AI
-import Yinsh
-
-heuristic :: RaiCharles -> AIValue
-heuristic _ = 42 -- yeah..
-
-data RaiCharles = RaiCharles { gs :: GameState
- , pl :: Int
- }
-
-instance AIPlayer RaiCharles where
- valueForWhite = heuristic
- getGamestate = gs
- getPlies = pl
- update ai gs' = ai { gs = gs' }
-
-aiRaiCharles :: Int -> AIFunction
-aiRaiCharles plies' gs' = aiTurn RaiCharles { gs = gs', pl = plies' }
diff --git a/src/Yinsh.hs b/src/Yinsh.hs
deleted file mode 100644
index f06aabb..0000000
--- a/src/Yinsh.hs
+++ /dev/null
@@ -1,431 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
--- |
--- Data structures and functions for playing the board game Yinsh.
-
-module Yinsh where
-
-import Control.Monad (guard)
-import qualified Data.Map.Lazy as M
-import Data.List (delete, foldl', sortBy)
-import Data.Ord (comparing)
-
--- $setup
--- >>> import Data.List (sort, nub)
--- >>> import Test.QuickCheck hiding (vector)
--- >>> let boardCoords = elements coords
--- >>> instance Arbitrary Direction where arbitrary = elements directions
-
--- | Yinsh hex coordinates.
-type YCoord = (Int, Int)
-
--- | The six hex directions.
-data Direction = N | NE | SE | S | SW | NW
- deriving (Eq, Enum, Bounded, Show, Read)
-
--- | Board element (ring or marker).
-data Element = Ring Player
- | Marker Player
- deriving (Show, Eq, Read)
-
--- | Status of the game (required action). The two modes @WaitRemoveRun@ and
--- @WaitAddMarker@ are introduced to preserve the alternating turn structure
--- for the minmax-algorithm. The full structure is explained in the figure:
---
--- <>
-data TurnMode = AddRing -- ^ place a ring on a free field
- | AddMarker -- ^ place a marker in one of your rings
- | MoveRing YCoord -- ^ move the ring at the given position
- | RemoveRun Player -- ^ remove (one of your) run(s).
- -- the parameter holds the last player who
- -- moved a ring
- | RemoveRing Player -- ^ remove one of your rings
- | WaitRemoveRun Player -- ^ do nothing
- | WaitAddMarker -- ^ do nothing
- deriving (Eq, Show, Read)
-
--- | Player types: black & white (or blue & green).
-data Player = B | W
- deriving (Eq, Enum, Bounded, Show, Read)
-
--- | Efficient data structure for the board with two-way access.
--- The Map is used to get log(n) access to the element at a certain
--- coordinate while the lists are used to get direct access to the
--- coordinates of the markers and rings (which would need a reverse
--- lookup otherwise). This comes at the cost of complete redundancy.
--- Either bmap or the other four fields would be enough to reconstruct
--- the whole board.
-data Board = Board { bmap :: M.Map YCoord Element
- , ringsB :: [YCoord]
- , ringsW :: [YCoord]
- , markersB :: [YCoord]
- , markersW :: [YCoord]
- } deriving (Eq, Show, Read)
-
--- | Yinsh game state.
-data GameState = GameState
- { activePlayer :: Player -- ^ player which has to move next
- , turnMode :: TurnMode -- ^ required action
- , board :: Board -- ^ current Yinsh board
- , pointsB :: Int -- ^ number of runs / rings removed (black)
- , pointsW :: Int -- ^ number of runs / rings removed (white)
- } deriving (Eq, Show, Read)
-
--- | Get all marker coordinates of one player.
-markers :: Player -> Board -> [YCoord]
-markers B = markersB
-markers W = markersW
-
--- | Get all ring coordinates of one player.
-rings :: Player -> Board -> [YCoord]
-rings B = ringsB
-rings W = ringsW
-
--- | Returns (Just) the element at a certain position or Nothing if the
--- coordinate is free (or invalid).
-elementAt :: Board -> YCoord -> Maybe Element
-elementAt b c = M.lookup c (bmap b)
-
--- | Returns True if the element at the given point is a marker of any color.
-isMarker :: Board -> YCoord -> Bool
-isMarker b c = case elementAt b c of
- (Just (Marker _)) -> True
- _ -> False
-
--- | Returns True if the element at the given point is a ring of any color.
-isRing :: Board -> YCoord -> Bool
-isRing b c = case elementAt b c of
- (Just (Ring _)) -> True
- _ -> False
-
--- | Returns True if a certain point on the board is free. Does not check the
--- validity of the coordinate.
-freeCoord :: Board -> YCoord -> Bool
-freeCoord b c = not $ M.member c (bmap b)
-
--- | Returns a new board with the specified element added at the given
--- coordinate.
-addElement :: Board -> YCoord -> Element -> Board
-addElement b c e = case e of
- Ring B -> b { bmap = bmap'
- , ringsB = c : ringsB b }
- Ring W -> b { bmap = bmap'
- , ringsW = c : ringsW b }
- Marker B -> b { bmap = bmap'
- , markersB = c : markersB b }
- Marker W -> b { bmap = bmap'
- , markersW = c : markersW b }
- where bmap' = M.insert c e (bmap b)
-
--- | Returns a new board with the element at the given point removed.
-removeElement :: Board -> YCoord -> Board
-removeElement b c = case e of
- Ring B -> b { bmap = bmap'
- , ringsB = delete c (ringsB b) }
- Ring W -> b { bmap = bmap'
- , ringsW = delete c (ringsW b) }
- Marker B -> b { bmap = bmap'
- , markersB = delete c (markersB b) }
- Marker W -> b { bmap = bmap'
- , markersW = delete c (markersW b) }
- where bmap' = M.delete c (bmap b)
- e = bmap b M.! c
-
--- | Returns a new board with the element at the given point replaced.
-modifyElement :: Board -> YCoord -> Element -> Board
-modifyElement b c = addElement (removeElement b c) c
--- TODO: this can certainly be optimizied:
-
--- | Yinsh board without any elements.
-emptyBoard :: Board
-emptyBoard = Board { bmap = M.empty
- , ringsB = []
- , ringsW = []
- , markersB = []
- , markersW = []
- }
-
--- | Required runs for a win.
-pointsForWin = 3
-pointsForWin :: Int
-
--- | Similar to Enum's succ, but for cyclic data structures.
--- Wraps around to the beginning when it reaches the 'last' element.
-next :: (Eq a, Enum a, Bounded a) => a -> a
-next x | x == maxBound = minBound
- | otherwise = succ x
-
--- | All six directions on the board.
-directions :: [Direction]
-directions = [minBound .. maxBound]
-
--- | Opposite direction (rotated by 180°).
---
--- prop> (opposite . opposite) d == d
-opposite :: Direction -> Direction
-opposite = next . next . next
-
--- | Vector to the next point on the board in a given direction.
-vector :: Direction -> YCoord
-vector N = ( 0, 1)
-vector NE = ( 1, 1)
-vector SE = ( 1, 0)
-vector S = ( 0, -1)
-vector SW = (-1, -1)
-vector NW = (-1, 0)
-
--- | Check if the point is within the boundaries of the board.
--- All Yinsh coordinates lie on a hexagonal grid within a circle of radius 4.6.
-validCoord :: YCoord -> Bool
-validCoord (x', y') = (0.5 * sqrt 3 * x)**2 + (0.5 * x - y)**2 <= 4.6**2
- where x = fromIntegral x'
- y = fromIntegral y'
-
--- | All points on the board.
---
--- >>> length coords
--- 85
---
-coords :: [YCoord]
-coords = sortCoords [ (x, y) | x <- [-5 .. 5]
- , y <- [-5 .. 5]
- , validCoord (x, y) ]
-
--- | Sort the coords once with respect to the distance from the center
--- for a better move ordering in the game tree.
-sortCoords :: [YCoord] -> [YCoord]
-sortCoords = sortBy (comparing norm2)
-
--- | Check if two points are connected by a line.
---
--- >>> connected (3, 4) (8, 4)
--- True
---
--- prop> connected c1 c2 == connected c2 c1
---
-connected :: YCoord -> YCoord -> Bool
-connected (x, y) (a, b) = x == a
- || y == b
- || x - y == a - b
-
--- | Vectorially add two coordinates.
-add :: YCoord -> YCoord -> YCoord
-add (!x1, !y1) (!x2, !y2) = (x1 + x2, y1 + y2)
-
--- | Vectorially subtract two coordinates.
-sub :: YCoord -> YCoord -> YCoord
-sub (!x1, !y1) (!x2, !y2) = (x1 - x2, y1 - y2)
-
--- | Squared norm.
-norm2 :: YCoord -> Int
-norm2 (x, y) = x * x + y * y
-
--- | Get a line of points from a given coordinate to the edge of the board.
-ray :: YCoord -> Direction -> [YCoord]
-ray s d = takeWhile validCoord $ adjacent s d
-
--- | All coordinates for a ring move in a given direction.
-ringMovesD :: Board -> YCoord -> Direction -> [YCoord]
-ringMovesD b s d = free ++ freeAfterJump
- where line = tail (ray s d)
- (free, rest) = span (freeCoord b) line
- freeAfterJump = jumpPos rest
- jumpPos [] = []
- jumpPos (c:cs) = case elementAt b c of
- (Just (Ring _)) -> []
- (Just (Marker _)) -> jumpPos cs
- Nothing -> [c]
-
--- | Get all valid ring moves starting from a given point.
-ringMoves :: Board -> YCoord -> [YCoord]
-ringMoves b start = ringMovesD b start =<< directions
-
--- | Check if a player has a run of five in a row.
-hasRun :: Board -> Player -> Bool
-hasRun b p = any (hasRunD b p) [NW, N, NE]
-
-isMarkerOf :: Board -> Player -> YCoord -> Bool
-isMarkerOf b p c = case elementAt b c of
- Just (Marker x) -> x == p
- _ -> False
-
-hasRunD :: Board -> Player -> Direction -> Bool
-hasRunD b p d = any middleOfRun ms
- where ms = markers p b
- middleOfRun c = all (isMarkerOf b p) surrounding
- where surrounding = left ++ right
- left = take 2 $ tail $ adjacent c d
- right = take 2 $ tail $ adjacent c $ opposite d
--- TODO: this can be improved.. we are checking for every marker if it sits
--- in the middle of a run
-
--- | Check if a coordinate is one of five in a row.
---
--- prop> partOfRun (take 5 $ adjacent c d) c == True
-partOfRun :: [YCoord] -> YCoord -> Bool
-partOfRun ms start = any partOfRunD [NW, N, NE]
- where partOfRunD :: Direction -> Bool
- partOfRunD dir = length (runCoordsD ms start dir) == 5
-
--- | Return the coordinates of the markers making up a run.
-runCoords :: [YCoord] -> YCoord -> [YCoord]
-runCoords ms start = if null cs then [] else head cs
- where cs = filter ((== 5) . length) $ map (runCoordsD ms start) [NW, N, NE]
-
--- | Combine two lists by taking elements alternatingly. If one list is longer,
--- append the rest.
---
--- prop> zipAlternate [] l == l
--- prop> zipAlternate l [] == l
--- prop> zipAlternate l l == (l >>= (\x -> [x, x]))
-zipAlternate :: [a] -> [a] -> [a]
-zipAlternate [] ys = ys
-zipAlternate (x:xs) ys = x : zipAlternate ys xs
-
--- | Get adjacent coordinates in a given direction which could belong to a run.
---
--- prop> runCoordsD (take 7 $ adjacent c d) c d == (take 5 $ adjacent c d)
-runCoordsD :: [YCoord] -> YCoord -> Direction -> [YCoord]
-runCoordsD ms start dir = if start `elem` ms
- then take 5 $ zipAlternate right left
- else []
- where right = takeAvailable dir
- left = tail $ takeAvailable (opposite dir) -- use tail to avoid taking the start twice
- takeAvailable d = takeWhile (`elem` ms) $ adjacent start d
-
--- | Get the adjacent (including start) coordinates in a given direction.
-adjacent :: YCoord -> Direction -> [YCoord]
-adjacent start dir = iterate step start
- where step = add (vector dir)
-
--- | Get all coordinates connecting two points.
-coordLine :: YCoord -> YCoord -> [YCoord]
-coordLine p1 p2 = take (num - 1) . tail $ iterate (add step) p1
- where (!dx, !dy) = p2 `sub` p1
- !num = max (abs dx) (abs dy)
- !step = (dx `div` num, dy `div` num)
-
--- | Flip all markers between two given coordinates.
-flippedMarkers :: Board -> YCoord -> YCoord -> Board
-flippedMarkers b s e = foldl' flipMaybe b (coordLine s e)
- where flipMaybe b' c = case elementAt b' c of
- Nothing -> b'
- (Just (Marker B)) -> modifyElement b' c (Marker W)
- (Just (Marker W)) -> modifyElement b' c (Marker B)
- _ -> error "trying to flip something that is not a marker (invalid ring move?)"
-
--- | Check whether one player has won the game.
-terminalState :: GameState -> Bool
-terminalState gs = pointsB gs == pointsForWin || pointsW gs == pointsForWin
--- TODO: we need to support draws (if no moves are possible)
-
--- | Get new game state after 'interacting' at a certain coordinate. Returns
--- @Nothing@ if the action leads to an invalid turn. For details, see
--- documentation of the @TurnMode@ type.
-newGameState :: GameState -> YCoord -> Maybe GameState
--- TODO: the guards should be (?) unnecessary when calling this function
--- from 'gamestates'.. (for AI-only matches). unless the AI tries to cheat..
-newGameState gs cc =
- case turnMode gs of
- AddRing -> do
- guard (freeCoord board' cc)
- Just gs { activePlayer = nextPlayer
- , turnMode = if numRings < 9 then AddRing else AddMarker
- , board = addElement board' cc (Ring activePlayer')
- }
- where numRings = length (ringsB board') + length (ringsW board')
- AddMarker -> do
- guard (cc `elem` rings activePlayer' board')
- Just gs { turnMode = MoveRing cc
- , board = addElement removedRing cc (Marker activePlayer')
- }
- (MoveRing start) -> do
- guard (cc `elem` ringMoves board' start)
- Just gs { activePlayer = nextPlayer
- , turnMode = nextTurnMode
- , board = addElement flippedBoard cc (Ring activePlayer')
- }
- where nextTurnMode | hasRun flippedBoard activePlayer' = WaitRemoveRun activePlayer'
- | hasRun flippedBoard nextPlayer = RemoveRun activePlayer'
- | otherwise = AddMarker
- flippedBoard = flippedMarkers board' start cc
- (RemoveRun lastRingMove) -> do
- guard (partOfRun playerMarkers cc)
- Just gs { turnMode = RemoveRing lastRingMove
- , board = removedRun
- }
- (RemoveRing lastRingMove) -> do
- guard (cc `elem` rings activePlayer' board')
- Just gs { activePlayer = nextPlayer
- , turnMode = nextTurnMode
- , board = removedRing
- , pointsB = if activePlayer' == B then pointsB gs + 1 else pointsB gs
- , pointsW = if activePlayer' == W then pointsW gs + 1 else pointsW gs
- }
- where nextTurnMode | hasRun removedRing activePlayer'
- = WaitRemoveRun lastRingMove -- player has a second run
- | hasRun removedRing nextPlayer
- = RemoveRun lastRingMove -- opponent also has a run
- | otherwise
- = if lastRingMove == activePlayer'
- then AddMarker
- else WaitAddMarker
- (WaitRemoveRun lastRingMove) ->
- Just gs { activePlayer = nextPlayer
- , turnMode = RemoveRun lastRingMove
- }
- WaitAddMarker ->
- Just gs { activePlayer = nextPlayer
- , turnMode = AddMarker
- }
- where activePlayer' = activePlayer gs
- nextPlayer = next activePlayer'
- removedRing = removeElement board' cc
- removedRun = foldl' removeElement board' (runCoords playerMarkers cc)
- board' = board gs
- playerMarkers = markers activePlayer' board'
-
-initialGameState :: GameState
-initialGameState = GameState { activePlayer = B
- , turnMode = AddRing
- , board = emptyBoard
- , pointsW = 0
- , pointsB = 0
- }
-
--- Testing stuff
-
-testBoard :: Board
-testBoard = foldl' (\b (c, e) -> addElement b c e) emptyBoard
- [ ((3 - 6, 4 - 6), Ring B)
- , ((4 - 6, 9 - 6), Ring B)
- , ((7 - 6, 9 - 6), Ring B)
- , ((8 - 6, 9 - 6), Ring B)
- , ((7 - 6, 10 - 6), Ring B)
- , ((8 - 6, 7 - 6), Ring W)
- , ((6 - 6, 3 - 6), Ring W)
- , ((4 - 6, 8 - 6), Ring W)
- , ((4 - 6, 2 - 6), Ring W)
- , ((2 - 6, 5 - 6), Ring W)
- , ((6 - 6, 4 - 6), Marker W)
- , ((6 - 6, 5 - 6), Marker W)
- , ((6 - 6, 7 - 6), Marker W)
- , ((5 - 6, 5 - 6), Marker W)
- , ((4 - 6, 5 - 6), Marker W)
- , ((3 - 6, 5 - 6), Marker W)
- , ((6 - 6, 6 - 6), Marker B)]
-
-testGameState = GameState { activePlayer = B
- , turnMode = AddMarker
- , board = testBoard
- , pointsW = 0
- , pointsB = 0
- }
-
-testGameStateW = GameState { activePlayer = W
- , turnMode = AddMarker
- , board = testBoard
- , pointsW = 0
- , pointsB = 0
- }
-
diff --git a/src/ai/evaluator.rs b/src/ai/evaluator.rs
new file mode 100644
index 0000000..5a146ad
--- /dev/null
+++ b/src/ai/evaluator.rs
@@ -0,0 +1,51 @@
+use minimax::{Evaluation, Evaluator};
+
+use crate::{GameState, Move, Player, TurnMode};
+
+use super::game::Yinsh;
+
+pub trait Heuristic {
+ fn identifier(&self) -> String;
+
+ fn evaluate_for_player_a(&self, state: &GameState) -> Evaluation;
+}
+
+pub struct YinshEvaluator<'a, H: Heuristic> {
+ heuristic: &'a H,
+}
+
+impl<'a, H: Heuristic> YinshEvaluator<'a, H> {
+ pub fn new(heuristic: &'a H) -> Self {
+ Self { heuristic }
+ }
+}
+
+impl<'a, H: Heuristic> Evaluator for YinshEvaluator<'a, H> {
+ type G = Yinsh;
+
+ fn evaluate(&self, state: &GameState) -> Evaluation {
+ match state.turn_mode {
+ TurnMode::WaitForRingMovement(_)
+ | TurnMode::WaitForRunRemoval(_)
+ | TurnMode::WaitForRingRemoval(_)
+ | TurnMode::WaitForMarkerPlacement => {
+ // Look one move ahead if we are in a waiting state.
+ let mut state_copy = state.clone();
+ state_copy.perform_move(&Move::Wait);
+ return -self.evaluate(&state_copy);
+ }
+ TurnMode::RingPlacement
+ | TurnMode::MarkerPlacement
+ | TurnMode::RingMovement(_)
+ | TurnMode::RunRemoval(_)
+ | TurnMode::RingRemoval(_) => {}
+ }
+
+ let score = self.heuristic.evaluate_for_player_a(state);
+
+ match state.active_player {
+ Player::A => score,
+ Player::B => -score,
+ }
+ }
+}
diff --git a/src/ai/game.rs b/src/ai/game.rs
new file mode 100644
index 0000000..e89d475
--- /dev/null
+++ b/src/ai/game.rs
@@ -0,0 +1,67 @@
+use std::iter;
+
+use minimax::{Game, Winner};
+
+use crate::{GameState, Move, TurnMode};
+
+pub struct Yinsh;
+
+impl Game for Yinsh {
+ type S = GameState;
+
+ type M = Move;
+
+ fn generate_moves(state: &Self::S, moves: &mut Vec) {
+ moves.extend(possible_moves(state));
+ }
+
+ fn apply(state: &mut Self::S, m: Self::M) -> Option {
+ let mut new_state = state.clone(); // TODO: is this necessary?
+ new_state.perform_move(&m);
+ Some(new_state) // TODO: we can avoid cloning here by returning None and implementing undo
+ }
+
+ fn get_winner(state: &Self::S) -> Option {
+ match state.winner() {
+ Some(p) if p == state.active_player => Some(Winner::PlayerToMove),
+ Some(_) => Some(Winner::PlayerJustMoved),
+ None => None,
+ }
+ }
+}
+
+pub fn possible_moves<'a>(state: &'a GameState) -> Box + 'a> {
+ match state.turn_mode {
+ TurnMode::RingPlacement => Box::new(state.board.free_coords().map(Move::PlaceRing)),
+ TurnMode::MarkerPlacement => Box::new(
+ state
+ .board
+ .marker_moves(state.active_player)
+ .map(Move::PlaceMarker),
+ ),
+ TurnMode::RingMovement(start) => Box::new(
+ state
+ .board
+ .ring_moves(start)
+ .into_iter()
+ .map(move |end| Move::MoveRing(start, end)),
+ ),
+ TurnMode::RunRemoval(_) => Box::new(
+ state
+ .board
+ .run_coords(state.active_player)
+ .into_iter()
+ .map(Move::RemoveRun), // TODO: this produces too many moves
+ ),
+ TurnMode::RingRemoval(_) => Box::new(
+ state
+ .board
+ .ring_coords(state.active_player)
+ .map(Move::RemoveRing),
+ ),
+ TurnMode::WaitForRunRemoval(_)
+ | TurnMode::WaitForMarkerPlacement
+ | TurnMode::WaitForRingMovement(_)
+ | TurnMode::WaitForRingRemoval(_) => Box::new(iter::once(Move::Wait)),
+ }
+}
diff --git a/src/ai/heuristics.rs b/src/ai/heuristics.rs
new file mode 100644
index 0000000..dfcfbe1
--- /dev/null
+++ b/src/ai/heuristics.rs
@@ -0,0 +1,90 @@
+use minimax::Evaluation;
+
+use super::evaluator::Heuristic;
+
+use crate::{yinsh::GameState, Board, Coord, Player};
+
+#[derive(Debug, Clone, Copy, Default)]
+struct RingPositionStatistics {
+ controlled_markers_own: usize,
+ controlled_markers_opponent: usize,
+ accessible_fields: usize,
+}
+
+fn ring_position_statistics(board: &Board, player: Player) -> RingPositionStatistics {
+ let mut statistics = RingPositionStatistics::default();
+
+ for ring in board.ring_coords(player) {
+ for move_end in board.ring_moves(ring) {
+ statistics.accessible_fields += 1;
+
+ Coord::between(ring, move_end).iter().for_each(|coord| {
+ // TODO: We double-count here, but maybe that's not a problem (since it's good to control a marker with multiple rings?)
+ match board.element_color_at(*coord) {
+ Some(p) if p == player => statistics.controlled_markers_own += 1,
+ Some(_) => statistics.controlled_markers_opponent += 1,
+ None => {}
+ }
+ });
+ }
+ }
+
+ statistics
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct SimpleHeuristic {
+ pub f_points: Evaluation,
+ pub f_markers: Evaluation,
+ pub f_controlled_markers_own: Evaluation,
+ pub f_controlled_markers_opponent: Evaluation,
+ pub f_accessible_fields: Evaluation,
+}
+
+impl Default for SimpleHeuristic {
+ fn default() -> Self {
+ Self {
+ f_points: 10_000,
+ f_markers: 100,
+ f_controlled_markers_own: 5,
+ f_controlled_markers_opponent: 10,
+ f_accessible_fields: 1,
+ // f_controlled_markers_own: 3,
+ // f_controlled_markers_opponent: 10,
+ // f_accessible_fields: 1,
+ }
+ }
+}
+
+impl Heuristic for SimpleHeuristic {
+ fn evaluate_for_player_a(&self, state: &GameState) -> Evaluation {
+ type Score = Evaluation;
+
+ let score_points = (state.points_a as Score) - (state.points_b as Score);
+
+ let score_markers = (state.board.num_markers(Player::A) as Score)
+ - (state.board.num_markers(Player::B) as Score);
+
+ let rps_a = ring_position_statistics(&state.board, Player::A);
+ let rps_b = ring_position_statistics(&state.board, Player::B);
+
+ let score_rings = self.f_controlled_markers_own
+ * (Score::try_from(rps_a.controlled_markers_own).unwrap()
+ - Score::try_from(rps_b.controlled_markers_own).unwrap())
+ + self.f_controlled_markers_opponent
+ * (Score::try_from(rps_a.controlled_markers_opponent).unwrap()
+ - Score::try_from(rps_b.controlled_markers_opponent).unwrap())
+ + self.f_accessible_fields
+ * (Score::try_from(rps_a.accessible_fields).unwrap()
+ - Score::try_from(rps_b.accessible_fields).unwrap());
+
+ let score = self.f_points * score_points + self.f_markers * score_markers + score_rings;
+
+ score
+ }
+
+ fn identifier(&self) -> String {
+ format!("SimpleHeuristic {{ f_points: {}, f_markers: {}, f_controlled_markers_own: {}, f_controlled_markers_opponent: {}, f_accessible_fields: {} }}",
+ self.f_points, self.f_markers, self.f_controlled_markers_own, self.f_controlled_markers_opponent, self.f_accessible_fields)
+ }
+}
diff --git a/src/ai/mod.rs b/src/ai/mod.rs
new file mode 100644
index 0000000..c1cf5af
--- /dev/null
+++ b/src/ai/mod.rs
@@ -0,0 +1,76 @@
+mod evaluator;
+mod game;
+mod heuristics;
+
+pub use evaluator::Heuristic;
+pub use game::possible_moves;
+pub use heuristics::SimpleHeuristic;
+
+use evaluator::YinshEvaluator;
+use minimax::{Negamax, Strategy};
+
+use crate::yinsh::{GameState, Move, TurnMode};
+
+pub struct YinshAi {
+ heuristic: H,
+ search_depth: usize,
+}
+
+impl YinshAi {
+ pub fn new(heuristic: H, search_depth: usize) -> Self {
+ Self {
+ heuristic,
+ search_depth,
+ }
+ }
+}
+
+pub trait YinshAiPlayer: Sync {
+ fn identifier(&self) -> String;
+
+ fn search_depth(&self) -> usize;
+
+ fn choose_move(&self, state: &GameState) -> Move;
+}
+
+impl YinshAiPlayer for YinshAi {
+ fn identifier(&self) -> String {
+ self.heuristic.identifier()
+ }
+
+ fn search_depth(&self) -> usize {
+ self.search_depth
+ }
+
+ fn choose_move(&self, state: &GameState) -> Move {
+ // Early return if the only thing we can do is wait. Would be great
+ // if this could be handled by 'minimax' itself (if there is only one
+ // possible move in choose_move, return that immediately).
+ match state.turn_mode {
+ TurnMode::WaitForRunRemoval(_)
+ | TurnMode::WaitForRingMovement(_)
+ | TurnMode::WaitForRingRemoval(_)
+ | TurnMode::WaitForMarkerPlacement => {
+ return Move::Wait;
+ }
+ _ => {}
+ }
+
+ let depth: u8 = if matches!(state.turn_mode, TurnMode::RingPlacement) {
+ 3
+ } else {
+ self.search_depth.try_into().unwrap()
+ };
+
+ let mut strategy = Negamax::new(YinshEvaluator::new(&self.heuristic), depth);
+ let player_move = strategy.choose_move(&state).unwrap();
+
+ // dbg!(strategy.root_value());
+
+ player_move
+ }
+}
+
+pub fn get_ai_move(search_depth: usize, state: &GameState) -> Move {
+ YinshAi::new(SimpleHeuristic::default(), search_depth).choose_move(state)
+}
diff --git a/src/gui/ai.rs b/src/gui/ai.rs
new file mode 100644
index 0000000..ddc3c24
--- /dev/null
+++ b/src/gui/ai.rs
@@ -0,0 +1,76 @@
+use bevy::prelude::*;
+
+use bevy_async_task::{AsyncTaskRunner, AsyncTaskStatus};
+use yinsh::{GameState, Move, Player};
+
+use super::state_update::{PlayerMoveEvent, StateUpdateSet};
+
+#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
+pub struct AiSet;
+
+#[derive(Resource)]
+pub struct AiPlayerStrength(pub usize);
+
+#[derive(Event)]
+pub enum AiComputationEvent {
+ Start(Player, GameState),
+
+ #[allow(unused)]
+ Cancel,
+}
+
+fn perform_ai_moves(
+ mut task_runner: AsyncTaskRunner>,
+ mut events: EventReader,
+ strength: Res,
+ mut player_move_events: EventWriter,
+) {
+ for event in events.read() {
+ match event {
+ AiComputationEvent::Start(player, ref game_state) => {
+ let player = *player;
+ let game_state = game_state.clone();
+ let search_depth = strength.0;
+ task_runner.start(async move {
+ // TODO! This is a hack to make sure the AI takes at least as long as
+ // the animation.
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ use super::graphics::ANIMATION_DURATION;
+ use yinsh::TurnMode;
+
+ if matches!(game_state.turn_mode, TurnMode::MarkerPlacement) {
+ std::thread::sleep(ANIMATION_DURATION);
+ }
+ }
+
+ Some((player, yinsh::get_ai_move(search_depth, &game_state)))
+ });
+ }
+ AiComputationEvent::Cancel => {
+ // Replace current computation with dummy task
+ task_runner.start(async move { None });
+ }
+ }
+ }
+
+ match task_runner.poll() {
+ AsyncTaskStatus::Idle | AsyncTaskStatus::Pending | AsyncTaskStatus::Finished(None) => {}
+ AsyncTaskStatus::Finished(Some((player, player_move))) => {
+ player_move_events.send(PlayerMoveEvent(player, player_move));
+ }
+ }
+}
+
+pub fn plugin(app: &mut App) {
+ app.insert_resource(AiPlayerStrength(if cfg!(debug_assertions) {
+ 9
+ } else {
+ 15
+ }))
+ .add_event::()
+ .add_systems(
+ Update,
+ (perform_ai_moves).in_set(AiSet).after(StateUpdateSet),
+ );
+}
diff --git a/src/gui/board.rs b/src/gui/board.rs
new file mode 100644
index 0000000..aba8924
--- /dev/null
+++ b/src/gui/board.rs
@@ -0,0 +1,12 @@
+use bevy::prelude::*;
+
+use yinsh::{Coord, Player};
+
+#[derive(Component)]
+pub struct BoardElement(pub Coord, pub Player);
+
+#[derive(Component)]
+pub struct Ring;
+
+#[derive(Component)]
+pub struct Marker;
diff --git a/src/gui/board_update_event.rs b/src/gui/board_update_event.rs
new file mode 100644
index 0000000..8dbb060
--- /dev/null
+++ b/src/gui/board_update_event.rs
@@ -0,0 +1,13 @@
+use bevy::prelude::Event;
+
+use yinsh::{Coord, Player};
+
+#[derive(Event)]
+pub enum BoardUpdateEvent {
+ AddRing(Coord, Player),
+ AddMarker(Coord, Player),
+ MoveRing(Coord, Coord),
+ RemoveRing(Coord),
+ RemoveRun(Vec),
+ FlipMarkers(Coord, Coord, Vec),
+}
diff --git a/src/gui/graphics.rs b/src/gui/graphics.rs
new file mode 100644
index 0000000..17a2c57
--- /dev/null
+++ b/src/gui/graphics.rs
@@ -0,0 +1,266 @@
+use std::time::Duration;
+
+use bevy::{
+ core_pipeline::bloom::BloomSettings,
+ prelude::*,
+ render::view::RenderLayers,
+ sprite::{MaterialMesh2dBundle, Mesh2dHandle},
+ window::PrimaryWindow,
+};
+
+use yinsh::{Coord, Player};
+
+use super::{
+ board::{BoardElement, Marker, Ring},
+ grid::draw_grid,
+ PLAYER_HUMAN,
+};
+
+pub const FOREGROUND_RENDER_LAYER: RenderLayers = RenderLayers::layer(2);
+pub const BACKGROUND_RENDER_LAYER: RenderLayers = RenderLayers::layer(1);
+
+pub const COLOR_GRID: Color = Color::hsl(0.0, 0.0, 0.2);
+
+pub const COLOR_BACKGROUND: Color = Color::hsl(0.0, 0.0, 0.05);
+
+pub const COLOR_RING_MOVEMENT_INDICATOR: Color = Color::hsla(0.0, 0.0, 1.5, 0.1);
+
+pub const COLOR_HUMAN_R: f32 = 255.;
+pub const COLOR_HUMAN_G: f32 = 226.;
+pub const COLOR_HUMAN_B: f32 = 55.;
+
+pub const COLOR_HUMAN_BLOOM: f32 = 1.5;
+
+pub const COLOR_HUMAN: Color = Color::srgba(
+ COLOR_HUMAN_BLOOM * COLOR_HUMAN_R / 255.,
+ COLOR_HUMAN_BLOOM * COLOR_HUMAN_G / 255.,
+ COLOR_HUMAN_BLOOM * COLOR_HUMAN_B / 255.,
+ 1.0,
+);
+pub const COLOR_HUMAN_HIGHLIGHTED: Color = Color::srgba(
+ 4.0 * COLOR_HUMAN_R / 255.,
+ 4.0 * COLOR_HUMAN_G / 255.,
+ 4.0 * COLOR_HUMAN_B / 255.,
+ 1.0,
+);
+pub const COLOR_HUMAN_TRANSPARENT: Color = Color::srgba(
+ COLOR_HUMAN_BLOOM * COLOR_HUMAN_R / 255.,
+ COLOR_HUMAN_BLOOM * COLOR_HUMAN_G / 255.,
+ COLOR_HUMAN_BLOOM * COLOR_HUMAN_B / 255.,
+ 0.2,
+);
+
+pub const COLOR_AI_R: f32 = 81.;
+pub const COLOR_AI_G: f32 = 151.;
+pub const COLOR_AI_B: f32 = 242.;
+
+pub const COLOR_AI_BLOOM: f32 = 1.3;
+
+pub const COLOR_AI: Color = Color::srgba(
+ COLOR_AI_BLOOM * COLOR_AI_R / 255.,
+ COLOR_AI_BLOOM * COLOR_AI_G / 255.,
+ COLOR_AI_BLOOM * COLOR_AI_B / 255.,
+ 1.0,
+);
+
+pub fn color_for_player(player: Player) -> Color {
+ if player == PLAYER_HUMAN {
+ COLOR_HUMAN
+ } else {
+ COLOR_AI
+ }
+}
+
+pub const ANIMATION_DURATION: Duration = Duration::from_millis(500);
+
+#[derive(Component)]
+pub struct MainCamera;
+
+#[derive(Resource, Debug, Clone, Copy)]
+pub struct ScaleFactor {
+ pub spacing: f32,
+ pub factor: f32,
+}
+
+impl ScaleFactor {
+ pub fn screen_point(&self, coord: Coord) -> Vec3 {
+ Vec3::new(
+ self.spacing * (0.5 * 3_f32.sqrt() * coord.x as f32),
+ self.spacing * (-coord.y as f32 + 0.5 * coord.x as f32),
+ 0.,
+ )
+ }
+}
+
+impl Default for ScaleFactor {
+ fn default() -> Self {
+ Self {
+ spacing: 80.0,
+ factor: 1.0,
+ }
+ }
+}
+
+#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
+pub struct ScaleFactorSet;
+
+#[derive(Resource)]
+pub struct PlayerColors {
+ pub human: Handle,
+ pub human_highlighted: Handle,
+ pub human_transparent: Handle,
+ pub ai: Handle,
+ pub animated_markers: [Handle; 8],
+}
+
+pub fn ring_mesh(
+ meshes: &mut Assets,
+ color_material: Handle,
+ visibility: Visibility,
+) -> MaterialMesh2dBundle {
+ MaterialMesh2dBundle {
+ mesh: Mesh2dHandle(meshes.add(Annulus::new(20., 25.))),
+ material: color_material,
+ visibility,
+ ..default()
+ }
+}
+
+pub fn marker_mesh(
+ meshes: &mut Assets,
+ color_material: Handle,
+ visibility: Visibility,
+) -> MaterialMesh2dBundle {
+ MaterialMesh2dBundle {
+ mesh: Mesh2dHandle(meshes.add(Circle::new(16.))),
+ material: color_material,
+ visibility,
+ ..default()
+ }
+}
+
+pub fn spawn_ring(
+ commands: &mut Commands,
+ meshes: &mut Assets,
+ player_colors: &PlayerColors,
+ coord: Coord,
+ player: Player,
+) {
+ let color = if player == PLAYER_HUMAN {
+ player_colors.human.clone()
+ } else {
+ player_colors.ai.clone()
+ };
+
+ commands.spawn((
+ ring_mesh(meshes, color, Visibility::Visible),
+ BoardElement(coord, player),
+ Ring,
+ FOREGROUND_RENDER_LAYER,
+ ));
+}
+
+pub fn spawn_marker(
+ commands: &mut Commands,
+ meshes: &mut Assets,
+ player_colors: &PlayerColors,
+ coord: Coord,
+ player: Player,
+) {
+ let color = if player == PLAYER_HUMAN {
+ player_colors.human.clone()
+ } else {
+ player_colors.ai.clone()
+ };
+
+ commands.spawn((
+ marker_mesh(meshes, color, Visibility::Visible),
+ BoardElement(coord, player),
+ Marker,
+ FOREGROUND_RENDER_LAYER,
+ ));
+}
+
+fn setup_graphics(
+ mut commands: Commands,
+ mut config_store: ResMut,
+ mut materials: ResMut>,
+) {
+ commands.insert_resource(ClearColor(COLOR_BACKGROUND));
+
+ // Render layer 1 is for the grid
+ commands.spawn((
+ Camera2dBundle {
+ camera: Camera {
+ hdr: true,
+ order: 1,
+ ..default()
+ },
+ ..default()
+ },
+ BloomSettings::default(),
+ BACKGROUND_RENDER_LAYER,
+ ));
+
+ // Render layer 2 is for the board elements
+ commands.spawn((
+ Camera2dBundle {
+ camera: Camera {
+ hdr: true,
+ order: 2,
+ ..default()
+ },
+ ..default()
+ },
+ BloomSettings::default(),
+ FOREGROUND_RENDER_LAYER,
+ MainCamera,
+ ));
+
+ commands.insert_resource(PlayerColors {
+ human: materials.add(COLOR_HUMAN),
+ human_highlighted: materials.add(COLOR_HUMAN_HIGHLIGHTED),
+ human_transparent: materials.add(COLOR_HUMAN_TRANSPARENT),
+ ai: materials.add(COLOR_AI),
+ animated_markers: [
+ materials.add(COLOR_AI),
+ materials.add(COLOR_AI),
+ materials.add(COLOR_AI),
+ materials.add(COLOR_AI),
+ materials.add(COLOR_AI),
+ materials.add(COLOR_AI),
+ materials.add(COLOR_AI),
+ materials.add(COLOR_AI),
+ ],
+ });
+
+ let (config, _) = config_store.config_mut::();
+ config.render_layers = BACKGROUND_RENDER_LAYER;
+}
+
+pub fn set_scale_factor(
+ mut scale_factor: ResMut,
+ window: Query<&Window, With>,
+) {
+ const BASE_SPACING_AT_800_PIXELS: f32 = 80.0;
+
+ let window = window.single();
+ let height = (window.physical_height() as f32) / (window.scale_factor() as f32);
+ let width = (window.physical_width() as f32) / (window.scale_factor() as f32);
+ let factor = ((height.min(width)) / 800.0).min(1.5);
+ scale_factor.factor = factor;
+ scale_factor.spacing = BASE_SPACING_AT_800_PIXELS * factor;
+}
+
+pub fn plugin(app: &mut App) {
+ app.insert_resource(Msaa::Sample8)
+ .insert_resource(ScaleFactor::default())
+ .add_systems(PreStartup, (setup_graphics, set_scale_factor))
+ .add_systems(
+ Update,
+ (
+ set_scale_factor.in_set(ScaleFactorSet),
+ draw_grid.after(ScaleFactorSet),
+ ),
+ );
+}
diff --git a/src/gui/grid.rs b/src/gui/grid.rs
new file mode 100644
index 0000000..9273912
--- /dev/null
+++ b/src/gui/grid.rs
@@ -0,0 +1,54 @@
+use bevy::prelude::*;
+
+use yinsh::Coord;
+
+use super::graphics::{ScaleFactor, COLOR_GRID};
+
+pub fn draw_grid(mut gizmos: Gizmos, scale_factor: Res) {
+ let grid_line_color = COLOR_GRID;
+
+ // Draw lines parallel to y-axis
+ for x in -5i8..=5i8 {
+ let coords: Vec<_> = (-5..=5)
+ .map(|y| Coord { x, y })
+ .filter(|c| c.is_inside_board())
+ .collect();
+
+ let min_y = coords.iter().map(|c| c.y).min().unwrap();
+ let max_y = coords.iter().map(|c| c.y).max().unwrap();
+
+ let start = scale_factor.screen_point(Coord { x, y: min_y });
+ let end = scale_factor.screen_point(Coord { x, y: max_y });
+ gizmos.line(start, end, grid_line_color);
+ }
+
+ // Draw lines parallel to x-axis
+ for y in -5i8..=5i8 {
+ let coords: Vec<_> = (-5..=5)
+ .map(|x| Coord { x, y })
+ .filter(|c| c.is_inside_board())
+ .collect();
+
+ let min_x = coords.iter().map(|c| c.x).min().unwrap();
+ let max_x = coords.iter().map(|c| c.x).max().unwrap();
+
+ let start = scale_factor.screen_point(Coord { x: min_x, y });
+ let end = scale_factor.screen_point(Coord { x: max_x, y });
+ gizmos.line(start, end, grid_line_color);
+ }
+
+ // Draw lines parallel to y = x
+ for d in -5i8..=5i8 {
+ let coords: Vec<_> = (-5..=5)
+ .map(|x| Coord { x, y: x + d })
+ .filter(|c| c.is_inside_board())
+ .collect();
+
+ let min = coords.iter().map(|c| c.x).min().unwrap();
+ let max = coords.iter().map(|c| c.x).max().unwrap();
+
+ let start = scale_factor.screen_point(Coord { x: min, y: min + d });
+ let end = scale_factor.screen_point(Coord { x: max, y: max + d });
+ gizmos.line(start, end, grid_line_color);
+ }
+}
diff --git a/src/gui/history.rs b/src/gui/history.rs
new file mode 100644
index 0000000..bc10b5b
--- /dev/null
+++ b/src/gui/history.rs
@@ -0,0 +1,72 @@
+use bevy::prelude::*;
+use yinsh::{Player, TurnMode};
+
+use super::{
+ ai::AiComputationEvent,
+ board::BoardElement,
+ board_update_event::BoardUpdateEvent,
+ graphics::ScaleFactorSet,
+ interaction::CursorElement,
+ state_update::{GameState, StateUpdateSet},
+};
+
+pub fn save_and_load_game_state(
+ keyboard: Res>,
+ mut commands: Commands,
+ mut game_state: ResMut,
+ mut ai_computation_events: EventWriter,
+ mut board_update_events: EventWriter,
+ q_board_elements: Query, Without)>,
+) {
+ let filename = "gamestate.yml";
+
+ if keyboard.just_pressed(KeyCode::KeyS) {
+ println!("Saving game state to {}", filename);
+ if matches!(
+ game_state.turn_mode,
+ TurnMode::RingPlacement | TurnMode::MarkerPlacement
+ ) {
+ game_state.save_to(filename);
+ } else {
+ println!(
+ "Cannot save game state in turn mode {:?}",
+ game_state.turn_mode
+ );
+ }
+ } else if keyboard.just_pressed(KeyCode::KeyL) || keyboard.just_pressed(KeyCode::KeyR) {
+ println!("Loading game state from {}", filename);
+ *game_state.as_deref_mut() = yinsh::GameState::load_from(filename);
+
+ assert!(matches!(
+ game_state.turn_mode,
+ TurnMode::RingPlacement | TurnMode::MarkerPlacement
+ ));
+
+ ai_computation_events.send(AiComputationEvent::Cancel);
+
+ // Despawn all board elements
+ for entity in q_board_elements.iter() {
+ commands.entity(entity).despawn();
+ }
+
+ // Respawn board elements
+ for p in [Player::A, Player::B] {
+ for coord in game_state.board.ring_coords(p) {
+ board_update_events.send(BoardUpdateEvent::AddRing(coord, p));
+ }
+
+ for coord in game_state.board.marker_coords(p) {
+ board_update_events.send(BoardUpdateEvent::AddMarker(coord, p));
+ }
+ }
+ }
+}
+
+pub fn plugin(app: &mut App) {
+ app.add_systems(
+ Update,
+ save_and_load_game_state
+ .before(StateUpdateSet)
+ .after(ScaleFactorSet),
+ );
+}
diff --git a/src/gui/information_display.rs b/src/gui/information_display.rs
new file mode 100644
index 0000000..de63a39
--- /dev/null
+++ b/src/gui/information_display.rs
@@ -0,0 +1,66 @@
+use bevy::prelude::*;
+
+use yinsh::Player;
+
+use super::{
+ ai::AiPlayerStrength,
+ graphics::BACKGROUND_RENDER_LAYER,
+ interaction::CursorCoord,
+ state_update::{GameState, InteractionState},
+};
+
+#[derive(Component)]
+struct GameStateInformation;
+
+fn setup_information_display(mut commands: Commands) {
+ commands.spawn((
+ TextBundle::from_section(
+ "",
+ TextStyle {
+ font_size: 18.0,
+ color: Color::hsl(0., 0., 0.3),
+ ..default()
+ },
+ )
+ .with_style(Style {
+ position_type: PositionType::Absolute,
+ top: Val::Px(10.),
+ left: Val::Px(10.),
+ ..default()
+ }),
+ GameStateInformation,
+ BACKGROUND_RENDER_LAYER,
+ ));
+}
+
+fn update_information_display(
+ game_state: Res,
+ mut q_text: Query<&mut Text, With>,
+ cursor_coord: Res,
+ interaction_state: Res,
+ ai_player_strength: Res,
+) {
+ q_text.single_mut().sections[0].value =
+ format!(
+ "Score: {points_a}:{points_b}\nMode: {mode}\nAI strength: {strength} [weaker: J, stronger: K]\n{coord}",
+ points_a=game_state.points_a,
+ points_b=game_state.points_b,
+ mode=match *interaction_state {
+ InteractionState::RingPlacement(_) => "Place a ring on the board",
+ InteractionState::MarkerPlacement(_) => "Place a marker in one of your rings",
+ InteractionState::RingMovement(_, _) => "Move the selected ring",
+ InteractionState::RunRemoval { .. } => "Select a run of five markers to remove",
+ InteractionState::RingRemoval(_) => "Select one of your rings to remove it",
+ InteractionState::AutoMove | InteractionState::WaitForAI => "Floyd is thinking...",
+ InteractionState::Winner(Player::A) => "Game over. You win!",
+ InteractionState::Winner(Player::B) => "Game over. Floyd wins!",
+ },
+ strength=ai_player_strength.0,
+ coord=if let Some (coord) = cursor_coord.0 { format!("({x}, {y})", x=coord.x, y=coord.y) } else { "".to_string() },
+ );
+}
+
+pub fn plugin(app: &mut App) {
+ app.add_systems(Startup, setup_information_display)
+ .add_systems(Update, update_information_display.ambiguous_with_all());
+}
diff --git a/src/gui/interaction.rs b/src/gui/interaction.rs
new file mode 100644
index 0000000..2816a9b
--- /dev/null
+++ b/src/gui/interaction.rs
@@ -0,0 +1,443 @@
+use std::time::Duration;
+
+use bevy::prelude::*;
+
+use bevy::window::PrimaryWindow;
+
+use bevy_tweening::lens::ColorMaterialColorLens;
+use bevy_tweening::{lens::TransformPositionLens, Animator, EaseFunction, Tween, TweeningPlugin};
+use bevy_tweening::{AnimationSystem, AssetAnimator, Delay, EaseMethod};
+use yinsh::{all_coords, Coord, Move};
+
+use super::ai::AiSet;
+use super::board::{BoardElement, Marker, Ring};
+use super::board_update_event::BoardUpdateEvent;
+use super::graphics::{
+ color_for_player, marker_mesh, ring_mesh, spawn_marker, spawn_ring, MainCamera, PlayerColors,
+ ScaleFactor, ScaleFactorSet, ANIMATION_DURATION, FOREGROUND_RENDER_LAYER,
+};
+use super::state_update::{GameState, PlayerMoveEvent, StateUpdateSet};
+use super::PLAYER_HUMAN;
+use super::{graphics::COLOR_RING_MOVEMENT_INDICATOR, state_update::InteractionState};
+
+#[derive(Component)]
+pub struct CursorElement;
+
+#[derive(Resource)]
+pub struct CursorCoord(pub Option);
+
+fn setup_interaction_cursors(
+ mut commands: Commands,
+ mut meshes: ResMut>,
+ player_colors: Res,
+) {
+ commands.spawn((
+ ring_mesh(
+ &mut meshes,
+ player_colors.human_transparent.clone(),
+ Visibility::Hidden,
+ ),
+ BoardElement(Coord::new(0, 0), PLAYER_HUMAN),
+ Ring,
+ CursorElement,
+ FOREGROUND_RENDER_LAYER,
+ ));
+
+ commands.spawn((
+ marker_mesh(
+ &mut meshes,
+ player_colors.human_transparent.clone(),
+ Visibility::Hidden,
+ ),
+ BoardElement(Coord::new(0, 0), PLAYER_HUMAN),
+ Marker,
+ CursorElement,
+ FOREGROUND_RENDER_LAYER,
+ ));
+}
+
+fn draw_ring_move_indicators(
+ scale_factor: Res,
+ mut gizmos: Gizmos,
+ interaction_state: Res,
+) {
+ let indicator_color = COLOR_RING_MOVEMENT_INDICATOR;
+
+ if let InteractionState::RingMovement(_, ref possible_moves) = *interaction_state {
+ for coord in possible_moves {
+ let screen_pos = scale_factor.screen_point(*coord);
+ gizmos.circle(
+ screen_pos,
+ Dir3::Z,
+ scale_factor.spacing / 8.,
+ indicator_color,
+ );
+ }
+ }
+}
+
+fn update_board_elements(
+ mut board_update_events: EventReader,
+ mut commands: Commands,
+ scale_factor: Res,
+ mut meshes: ResMut>,
+ player_colors: Res,
+ mut q_rings: Query<
+ (Entity, &mut BoardElement),
+ (With, (Without, Without)),
+ >,
+ mut q_markers: Query<
+ (Entity, &mut BoardElement, &mut Handle),
+ (With, Without),
+ >,
+) {
+ for event in board_update_events.read() {
+ match *event {
+ BoardUpdateEvent::AddRing(coord, player) => {
+ spawn_ring(&mut commands, &mut meshes, &player_colors, coord, player);
+ }
+ BoardUpdateEvent::AddMarker(coord, player) => {
+ spawn_marker(&mut commands, &mut meshes, &player_colors, coord, player);
+ }
+ BoardUpdateEvent::MoveRing(old_coord, new_coord) => {
+ for (entity, mut ring) in q_rings.iter_mut() {
+ if ring.0 == old_coord {
+ let tween = Tween::new(
+ EaseFunction::QuadraticInOut,
+ ANIMATION_DURATION,
+ TransformPositionLens {
+ start: scale_factor.screen_point(old_coord),
+ end: scale_factor.screen_point(new_coord),
+ },
+ );
+
+ commands.entity(entity).insert(Animator::new(tween));
+
+ ring.0 = new_coord; // To make the change permanent
+
+ break;
+ }
+ }
+ }
+ BoardUpdateEvent::RemoveRun(ref run_coords) => {
+ for (entity, element, _) in q_markers.iter_mut() {
+ if run_coords.contains(&element.0) {
+ commands.entity(entity).despawn();
+ }
+ }
+ }
+ BoardUpdateEvent::RemoveRing(coord) => {
+ for (entity, element) in q_rings.iter_mut() {
+ if element.0 == coord {
+ commands.entity(entity).despawn();
+ break;
+ }
+ }
+ }
+ BoardUpdateEvent::FlipMarkers(start, end, ref marker_coords) => {
+ let mut i = 0;
+ let total_distance = (end - start).norm();
+
+ for (entity, mut element, mut color_material) in q_markers.iter_mut() {
+ if marker_coords.contains(&element.0) {
+ let distance_from_start = (element.0 - start).norm();
+
+ let delay =
+ ANIMATION_DURATION.mul_f32(distance_from_start / total_distance);
+
+ let tween = Tween::new(
+ EaseMethod::Linear,
+ Duration::from_secs_f32(1e-9),
+ ColorMaterialColorLens {
+ start: color_for_player(element.1),
+ end: color_for_player(element.1),
+ },
+ )
+ .then(Delay::new(delay).then(Tween::new(
+ EaseMethod::Linear,
+ ANIMATION_DURATION.div_f32(5.0),
+ ColorMaterialColorLens {
+ start: color_for_player(element.1),
+ end: color_for_player(element.1.next()),
+ },
+ )));
+
+ *color_material = player_colors.animated_markers[i].clone();
+ commands.entity(entity).insert(AssetAnimator::new(tween));
+
+ element.1.flip(); // To make the change permanent
+
+ i += 1;
+ }
+ }
+ }
+ }
+ }
+}
+
+fn clear_animators(mut q: Query<(Entity, &Animator)>, mut commands: Commands) {
+ for (entity, animator) in q.iter_mut() {
+ if animator.tweenable().times_completed() == 1 {
+ commands.entity(entity).remove::>();
+ }
+ }
+}
+
+fn clear_asset_animators(
+ mut q: Query<(Entity, &AssetAnimator)>,
+ mut commands: Commands,
+) {
+ for (entity, animator) in q.iter_mut() {
+ if animator.tweenable().times_completed() == 1 {
+ commands
+ .entity(entity)
+ .remove::>();
+ }
+ }
+}
+
+fn move_board_elements(
+ scale_factor: Res,
+ mut query: Query<(&BoardElement, &mut Transform), Without>>,
+) {
+ for (BoardElement(coord, _), mut transform) in query.iter_mut() {
+ transform.translation = scale_factor.screen_point(*coord);
+ }
+}
+
+fn scale_board_elements(
+ scale_factor: Res,
+ mut query: Query<&mut Transform, With>,
+) {
+ for mut transform in query.iter_mut() {
+ transform.scale = Vec3::splat(scale_factor.factor);
+ }
+}
+
+fn colorize_board_elements(
+ mut query: Query<(
+ &BoardElement,
+ &mut Handle,
+ Option<&Ring>,
+ Option<&Marker>,
+ Option<&CursorElement>,
+ Option<&AssetAnimator>,
+ )>,
+ interaction_state: Res,
+ player_colors: Res,
+ mouse_cursor_coord: Res,
+) {
+ for (BoardElement(coord, player), mut color_material, ring, marker, cursor_element, animated) in
+ query.iter_mut()
+ {
+ if animated.is_some() {
+ continue;
+ }
+
+ *color_material = if player == &PLAYER_HUMAN {
+ if cursor_element.is_some() {
+ player_colors.human_transparent.clone()
+ } else {
+ match *interaction_state {
+ InteractionState::RingMovement(start, _) => {
+ if *coord == start && ring.is_some() {
+ player_colors.human_highlighted.clone()
+ } else {
+ player_colors.human.clone()
+ }
+ }
+ InteractionState::RunRemoval {
+ ref all_run_coords,
+ ref run_from_seed,
+ } => match mouse_cursor_coord.0 {
+ Some(cursor_coord) if all_run_coords.contains(&cursor_coord) => {
+ let run_from_cursor = run_from_seed.get(&cursor_coord).unwrap();
+ if run_from_cursor.contains(coord) {
+ player_colors.human_highlighted.clone()
+ } else {
+ player_colors.human.clone()
+ }
+ }
+ _ => {
+ if marker.is_some() && all_run_coords.contains(coord) {
+ player_colors.human_highlighted.clone()
+ } else {
+ player_colors.human.clone()
+ }
+ }
+ },
+ _ => player_colors.human.clone(),
+ }
+ }
+ } else {
+ player_colors.ai.clone()
+ };
+ }
+}
+
+fn grid_cursor_system(
+ scale_factor: Res,
+ mut q_window: Query<&mut Window, With>,
+ q_camera: Query<(&Camera, &GlobalTransform), With>,
+ mut cursor_ring: Query<
+ (&mut BoardElement, &mut Visibility),
+ (With, Without, With),
+ >,
+ mut cursor_marker: Query<
+ (&mut BoardElement, &mut Visibility),
+ (With, Without, With),
+ >,
+ interaction_state: Res,
+ mut mouse_cursor_coord: ResMut,
+) {
+ let Ok(mut window) = q_window.get_single_mut() else {
+ return;
+ };
+
+ window.cursor.icon = match *interaction_state {
+ InteractionState::WaitForAI => CursorIcon::Progress,
+ InteractionState::Winner(_) => CursorIcon::Default,
+ _ => CursorIcon::Pointer,
+ };
+
+ if let Some(cursor_position) = window.cursor_position() {
+ let (camera, camera_transform) = q_camera.single();
+
+ if let Some(cursor_position) = camera
+ .viewport_to_world(camera_transform, cursor_position)
+ .map(|ray| ray.origin.truncate())
+ {
+ let cursor_coord = yinsh::all_coords()
+ .into_iter()
+ .min_by_key(|c| {
+ let screen_pos = scale_factor.screen_point(*c);
+ let cursor_pos = Vec3::new(cursor_position.x, cursor_position.y, 0.0);
+ let diff = screen_pos - cursor_pos;
+ diff.length_squared() as i32
+ })
+ .unwrap();
+
+ let (mut cursor_ring_coord, mut cursor_ring_visibility) = cursor_ring.single_mut();
+ *cursor_ring_visibility = Visibility::Hidden;
+
+ let (mut cursor_marker_coord, mut cursor_marker_visibility) =
+ cursor_marker.single_mut();
+ *cursor_marker_visibility = Visibility::Hidden;
+
+ match *interaction_state {
+ InteractionState::RingPlacement(ref free_coords) => {
+ if free_coords.contains(&cursor_coord) {
+ *cursor_ring_visibility = Visibility::Visible;
+ cursor_ring_coord.0 = cursor_coord;
+ }
+ }
+ InteractionState::MarkerPlacement(ref ring_coords) => {
+ if ring_coords.contains(&cursor_coord) {
+ *cursor_marker_visibility = Visibility::Visible;
+ cursor_marker_coord.0 = cursor_coord;
+ }
+ }
+ InteractionState::RingMovement(_, ref possible_ring_moves) => {
+ if possible_ring_moves.contains(&cursor_coord) {
+ *cursor_ring_visibility = Visibility::Visible;
+ cursor_ring_coord.0 = cursor_coord;
+ }
+ }
+ InteractionState::RunRemoval { .. } => {}
+ InteractionState::RingRemoval(_) => {}
+ InteractionState::AutoMove => {}
+ InteractionState::WaitForAI => {}
+ InteractionState::Winner(_) => {}
+ }
+
+ mouse_cursor_coord.0 = Some(cursor_coord);
+ }
+ }
+}
+
+fn mouse_interaction_system(
+ buttons: Res>,
+ interaction_state: Res,
+ cursor_coord: Res,
+ mut player_move_events: EventWriter,
+) {
+ if matches!(*interaction_state, InteractionState::AutoMove) {
+ // TODO
+ player_move_events.send(PlayerMoveEvent(PLAYER_HUMAN, Move::Wait));
+ }
+
+ if let Some(cursor_coord) = cursor_coord.0 {
+ if buttons.just_pressed(MouseButton::Left) {
+ match *interaction_state {
+ InteractionState::RingPlacement(ref free_coords) => {
+ if free_coords.contains(&cursor_coord) {
+ player_move_events
+ .send(PlayerMoveEvent(PLAYER_HUMAN, Move::PlaceRing(cursor_coord)));
+ }
+ }
+ InteractionState::MarkerPlacement(ref ring_coords) => {
+ if ring_coords.contains(&cursor_coord) {
+ player_move_events.send(PlayerMoveEvent(
+ PLAYER_HUMAN,
+ Move::PlaceMarker(cursor_coord),
+ ));
+ }
+ }
+ InteractionState::RingMovement(start, ref possible_ring_moves) => {
+ if possible_ring_moves.contains(&cursor_coord) {
+ player_move_events.send(PlayerMoveEvent(
+ PLAYER_HUMAN,
+ Move::MoveRing(start, cursor_coord),
+ ));
+ }
+ }
+ InteractionState::WaitForAI => {}
+ InteractionState::RunRemoval {
+ ref all_run_coords, ..
+ } => {
+ if all_run_coords.contains(&cursor_coord) {
+ player_move_events
+ .send(PlayerMoveEvent(PLAYER_HUMAN, Move::RemoveRun(cursor_coord)));
+ }
+ }
+ InteractionState::RingRemoval(ref ring_coords) => {
+ if ring_coords.contains(&cursor_coord) {
+ player_move_events.send(PlayerMoveEvent(
+ PLAYER_HUMAN,
+ Move::RemoveRing(cursor_coord),
+ ));
+ }
+ }
+ InteractionState::AutoMove => {}
+ InteractionState::Winner(_) => {}
+ }
+ }
+ }
+}
+
+pub fn plugin(app: &mut App) {
+ app.add_plugins(TweeningPlugin)
+ .insert_resource(InteractionState::RingPlacement(all_coords()))
+ .insert_resource(CursorCoord(None))
+ .insert_resource(GameState::initial())
+ .add_systems(Startup, setup_interaction_cursors)
+ .add_systems(
+ Update,
+ (
+ draw_ring_move_indicators,
+ (
+ clear_animators,
+ clear_asset_animators,
+ grid_cursor_system,
+ update_board_elements,
+ move_board_elements.ambiguous_with(AnimationSystem::AnimationUpdate),
+ scale_board_elements.ambiguous_with(AnimationSystem::AnimationUpdate),
+ colorize_board_elements.ambiguous_with(AnimationSystem::AnimationUpdate),
+ mouse_interaction_system.ambiguous_with(AiSet),
+ )
+ .chain(),
+ )
+ .after(StateUpdateSet)
+ .after(ScaleFactorSet),
+ );
+}
diff --git a/src/gui/keyboard_control.rs b/src/gui/keyboard_control.rs
new file mode 100644
index 0000000..e02067a
--- /dev/null
+++ b/src/gui/keyboard_control.rs
@@ -0,0 +1,30 @@
+use bevy::prelude::*;
+
+use super::{
+ ai::{AiComputationEvent, AiPlayerStrength, AiSet},
+ state_update::GameState,
+ PLAYER_HUMAN,
+};
+
+fn keyboard_control(
+ keyboard: Res>,
+ mut exit: EventWriter,
+ mut ai_player_strength: ResMut,
+ mut ai_computation_events: EventWriter,
+ game_state: Res,
+) {
+ if keyboard.just_pressed(KeyCode::Escape) || keyboard.just_pressed(KeyCode::KeyQ) {
+ exit.send(AppExit::Success);
+ } else if keyboard.just_pressed(KeyCode::KeyK) {
+ ai_player_strength.0 = ai_player_strength.0 + 1;
+ } else if keyboard.just_pressed(KeyCode::KeyJ) {
+ ai_player_strength.0 = (ai_player_strength.0 - 1).max(1);
+ } else if keyboard.just_pressed(KeyCode::KeyA) {
+ ai_computation_events.send(AiComputationEvent::Start(PLAYER_HUMAN, game_state.clone()));
+ // TODO: remove this feature, or implement it properly
+ }
+}
+
+pub fn plugin(app: &mut App) {
+ app.add_systems(Update, keyboard_control.ambiguous_with(AiSet));
+}
diff --git a/src/gui/mod.rs b/src/gui/mod.rs
new file mode 100644
index 0000000..abb1b59
--- /dev/null
+++ b/src/gui/mod.rs
@@ -0,0 +1,17 @@
+use yinsh::Player;
+
+pub mod ai;
+pub mod board;
+pub mod board_update_event;
+pub mod graphics;
+pub mod grid;
+#[cfg(not(target_arch = "wasm32"))]
+pub mod history;
+pub mod information_display;
+pub mod interaction;
+pub mod keyboard_control;
+pub mod resources;
+pub mod state_update;
+
+pub const PLAYER_HUMAN: Player = Player::A;
+pub const PLAYER_AI: Player = Player::B;
diff --git a/src/gui/resources.rs b/src/gui/resources.rs
new file mode 100644
index 0000000..e69de29
diff --git a/src/gui/state_update.rs b/src/gui/state_update.rs
new file mode 100644
index 0000000..380df0c
--- /dev/null
+++ b/src/gui/state_update.rs
@@ -0,0 +1,150 @@
+use std::ops::{Deref, DerefMut};
+
+use yinsh::{Coord, Move, Player, TurnMode};
+
+use bevy::{prelude::*, utils::HashMap};
+
+use crate::gui::PLAYER_HUMAN;
+
+use super::{ai::AiComputationEvent, board_update_event::BoardUpdateEvent, PLAYER_AI};
+
+#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
+pub struct StateUpdateSet;
+
+#[derive(Resource)]
+pub struct GameState(yinsh::GameState);
+
+impl Deref for GameState {
+ type Target = yinsh::GameState;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for GameState {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl GameState {
+ pub fn initial() -> Self {
+ Self(yinsh::GameState::initial())
+ }
+}
+
+#[derive(Resource)]
+pub enum InteractionState {
+ RingPlacement(Vec),
+ MarkerPlacement(Vec),
+ RingMovement(Coord, Vec),
+ RunRemoval {
+ all_run_coords: Vec,
+ run_from_seed: HashMap>,
+ },
+ RingRemoval(Vec),
+ AutoMove,
+ WaitForAI,
+ Winner(Player),
+}
+
+impl InteractionState {
+ pub fn from_game_state(game_state: &yinsh::GameState) -> Self {
+ if let Some(winner) = game_state.winner() {
+ Self::Winner(winner)
+ } else if game_state.active_player == PLAYER_AI {
+ Self::WaitForAI
+ } else {
+ match game_state.turn_mode {
+ TurnMode::RingPlacement => {
+ Self::RingPlacement(game_state.board.free_coords().collect())
+ }
+ TurnMode::MarkerPlacement => {
+ Self::MarkerPlacement(game_state.board.marker_moves(PLAYER_HUMAN).collect())
+ }
+ TurnMode::RingMovement(start) => {
+ Self::RingMovement(start, game_state.board.ring_moves(start))
+ }
+ TurnMode::RunRemoval(_) => {
+ let all_run_coords = game_state.board.run_coords(PLAYER_HUMAN);
+ Self::RunRemoval {
+ all_run_coords: all_run_coords.clone(),
+ run_from_seed: all_run_coords
+ .into_iter()
+ .map(|seed| (seed, game_state.board.run_coords_from(seed).unwrap()))
+ .collect(),
+ }
+ }
+ TurnMode::RingRemoval(_) => {
+ Self::RingRemoval(game_state.board.ring_coords(PLAYER_HUMAN).collect())
+ }
+ TurnMode::WaitForRunRemoval(_)
+ | TurnMode::WaitForMarkerPlacement
+ | TurnMode::WaitForRingMovement(_)
+ | TurnMode::WaitForRingRemoval(_) => Self::AutoMove,
+ }
+ }
+ }
+}
+
+#[derive(Event)]
+pub struct PlayerMoveEvent(pub Player, pub Move);
+
+fn state_update(
+ mut player_move_events: EventReader,
+ mut game_state: ResMut,
+ mut interaction_state: ResMut,
+ mut ai_computation_events: EventWriter,
+ mut board_update_events: EventWriter,
+) {
+ for PlayerMoveEvent(player, player_move) in player_move_events.read() {
+ assert!(player == &game_state.active_player);
+
+ match player_move {
+ Move::PlaceRing(coord) => {
+ board_update_events.send(BoardUpdateEvent::AddRing(*coord, *player));
+ }
+ Move::PlaceMarker(coord) => {
+ board_update_events.send(BoardUpdateEvent::AddMarker(*coord, *player));
+ }
+ Move::MoveRing(start, end) => {
+ board_update_events.send(BoardUpdateEvent::MoveRing(*start, *end));
+ board_update_events.send(BoardUpdateEvent::FlipMarkers(
+ *start,
+ *end,
+ Coord::between(*start, *end)
+ .into_iter()
+ .filter(|&coord| game_state.board.has_marker_at(coord))
+ .collect(),
+ ));
+ }
+ Move::RemoveRun(seed) => {
+ board_update_events.send(BoardUpdateEvent::RemoveRun(
+ game_state.board.run_coords_from(*seed).unwrap(),
+ ));
+ }
+ Move::RemoveRing(coord) => {
+ board_update_events.send(BoardUpdateEvent::RemoveRing(*coord));
+ }
+ Move::Wait => {}
+ }
+
+ game_state.perform_move(player_move);
+
+ if game_state.active_player == PLAYER_AI && game_state.winner().is_none() {
+ ai_computation_events.send(AiComputationEvent::Start(PLAYER_AI, game_state.clone()));
+ }
+ }
+
+ *interaction_state = InteractionState::from_game_state(&game_state);
+}
+
+pub fn plugin(app: &mut App) {
+ let initial_game_state = GameState::initial();
+ app.insert_resource(InteractionState::from_game_state(&initial_game_state))
+ .insert_resource(initial_game_state)
+ .add_event::()
+ .add_event::()
+ .add_systems(Update, state_update.in_set(StateUpdateSet));
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..c51aca1
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,5 @@
+mod ai;
+mod yinsh;
+
+pub use ai::{get_ai_move, possible_moves, Heuristic, SimpleHeuristic, YinshAi, YinshAiPlayer};
+pub use yinsh::*;
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..cf14e81
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,38 @@
+mod gui;
+
+use bevy::{
+ prelude::*,
+ window::{WindowMode, WindowResolution},
+};
+
+fn main() {
+ App::new()
+ .add_plugins((
+ DefaultPlugins.set(WindowPlugin {
+ primary_window: Some(Window {
+ title: "Yinsh".into(),
+ name: Some("yinsh".into()),
+ resolution: WindowResolution::new(760., 760.),
+ mode: WindowMode::Windowed,
+ canvas: Some("#yinsh-canvas".into()),
+ ..default()
+ }),
+ ..default()
+ }),
+ gui::state_update::plugin,
+ gui::ai::plugin,
+ gui::graphics::plugin,
+ gui::interaction::plugin,
+ gui::information_display::plugin,
+ gui::keyboard_control::plugin,
+ #[cfg(not(target_arch = "wasm32"))]
+ gui::history::plugin,
+ ))
+ // .edit_schedule(Update, |schedule| {
+ // schedule.set_build_settings(ScheduleBuildSettings {
+ // ambiguity_detection: LogLevel::Warn,
+ // ..default()
+ // });
+ // })
+ .run();
+}
diff --git a/src/yinsh/board.rs b/src/yinsh/board.rs
new file mode 100644
index 0000000..86395b6
--- /dev/null
+++ b/src/yinsh/board.rs
@@ -0,0 +1,511 @@
+use std::iter;
+
+use itertools::Itertools;
+use serde::{Deserialize, Serialize};
+
+use crate::yinsh::core::AXES;
+
+use super::{core::all_coords, Coord, Player, DIRECTIONS};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum Element {
+ Ring(Player),
+ Marker(Player),
+ Empty,
+}
+
+impl Element {
+ pub fn is_marker(&self) -> bool {
+ matches!(self, Element::Marker(_))
+ }
+
+ fn is_ring(&self) -> bool {
+ matches!(self, Element::Ring(_))
+ }
+
+ fn is_empty(&self) -> bool {
+ matches!(self, Element::Empty)
+ }
+
+ fn player(&self) -> Option {
+ match self {
+ Element::Ring(player) => Some(*player),
+ Element::Marker(player) => Some(*player),
+ Element::Empty => None,
+ }
+ }
+}
+
+impl Default for Element {
+ fn default() -> Self {
+ Element::Empty
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct CheckRunResult {
+ a_has_run: bool,
+ b_has_run: bool,
+}
+
+impl CheckRunResult {
+ fn or(&mut self, other: &Self) {
+ self.a_has_run |= other.a_has_run;
+ self.b_has_run |= other.b_has_run;
+ }
+
+ pub fn has_run(&self, player: Player) -> bool {
+ match player {
+ Player::A => self.a_has_run,
+ Player::B => self.b_has_run,
+ }
+ }
+
+ pub fn no_runs(&self) -> bool {
+ !self.a_has_run && !self.b_has_run
+ }
+}
+
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Board {
+ #[serde(skip)]
+ board: [[Element; 11]; 11],
+
+ rings_a: Vec,
+ rings_b: Vec,
+ markers_a: Vec,
+ markers_b: Vec,
+}
+
+impl Board {
+ pub fn empty() -> Self {
+ Self::default()
+ }
+
+ /// Returns the element at a certain position or None if the coordinate is free (or invalid).
+ /// Does not check for validity.
+ fn element_at_unchecked(&self, coord: Coord) -> Element {
+ self.board[(coord.y + 5) as usize][(coord.x + 5) as usize]
+ }
+
+ /// Returns the element at a certain position or None if the coordinate is free (or invalid)
+ fn element_at(&self, coord: Coord) -> Element {
+ if coord.is_inside_board() {
+ self.board[(coord.y + 5) as usize][(coord.x + 5) as usize]
+ } else {
+ Element::Empty
+ }
+ }
+
+ /// Returns the element at a certain position or None if the coordinate is free. Does not
+ /// check for validity.
+ fn element_at_mut_unchecked(&mut self, coord: Coord) -> &mut Element {
+ &mut self.board[(coord.y + 5) as usize][(coord.x + 5) as usize]
+ }
+
+ fn insert_board_element_at(&mut self, coord: Coord, element: Element) {
+ debug_assert!(coord.is_inside_board());
+
+ *self.element_at_mut_unchecked(coord) = element;
+ }
+
+ fn remove_board_element_at(&mut self, coord: &Coord) {
+ debug_assert!(coord.is_inside_board());
+
+ *self.element_at_mut_unchecked(*coord) = Element::Empty;
+ }
+
+ /// Returns true if a certain point on the board is free. Does not check for validity.
+ pub fn is_free(&self, coord: Coord) -> bool {
+ self.element_at(coord).is_empty()
+ }
+
+ /// Returns true if the element at the given point is a ring of any color.
+ pub fn has_ring_at(&self, coord: Coord, player: Player) -> bool {
+ self.check_invariants();
+
+ self.element_at(coord) == Element::Ring(player)
+ }
+
+ /// Returns true if the element at the given point is a marker of any color.
+ pub fn has_marker_at(&self, coord: Coord) -> bool {
+ self.check_invariants();
+
+ self.element_at(coord).is_marker()
+ }
+
+ /// Returns the color/player of the board element at the given coordinate.
+ pub fn element_color_at(&self, coord: Coord) -> Option {
+ self.check_invariants();
+
+ self.element_at(coord).player()
+ }
+
+ pub fn add_ring(&mut self, player: Player, coord: Coord) {
+ self.check_invariants();
+ debug_assert!(self.is_free(coord));
+
+ self.insert_board_element_at(coord, Element::Ring(player));
+ match player {
+ Player::A => self.rings_a.push(coord),
+ Player::B => self.rings_b.push(coord),
+ }
+ }
+
+ pub fn remove_ring(&mut self, coord: Coord) {
+ self.check_invariants();
+ debug_assert!(self.element_at(coord).is_ring());
+
+ self.remove_board_element_at(&coord);
+ self.rings_a.retain(|&x| x != coord);
+ self.rings_b.retain(|&x| x != coord);
+ }
+
+ pub fn num_rings(&self) -> usize {
+ self.check_invariants();
+
+ self.rings_a.len() + self.rings_b.len()
+ }
+
+ pub fn add_marker(&mut self, player: Player, coord: Coord) {
+ self.check_invariants();
+ debug_assert!(self.is_free(coord));
+
+ self.insert_board_element_at(coord, Element::Marker(player));
+ match player {
+ Player::A => self.markers_a.push(coord),
+ Player::B => self.markers_b.push(coord),
+ }
+ }
+
+ pub fn remove_marker(&mut self, coord: Coord) {
+ self.check_invariants();
+ debug_assert!(self.has_marker_at(coord));
+
+ self.remove_board_element_at(&coord);
+ self.markers_a.retain(|&x| x != coord);
+ self.markers_b.retain(|&x| x != coord);
+ }
+
+ pub fn num_markers(&self, player: Player) -> usize {
+ self.check_invariants();
+
+ match player {
+ Player::A => self.markers_a.len(),
+ Player::B => self.markers_b.len(),
+ }
+ }
+
+ pub fn free_coords(&self) -> impl Iterator- + '_ {
+ self.check_invariants();
+
+ all_coords()
+ .into_iter()
+ .filter(|&coord| self.is_free(coord))
+ }
+
+ pub fn ring_coords(&self, player: Player) -> impl Iterator
- + '_ {
+ self.check_invariants();
+
+ match player {
+ Player::A => self.rings_a.iter().copied(),
+ Player::B => self.rings_b.iter().copied(),
+ }
+ }
+
+ pub fn marker_coords(&self, player: Player) -> impl Iterator
- + '_ {
+ self.check_invariants();
+
+ match player {
+ Player::A => self.markers_a.iter().copied(),
+ Player::B => self.markers_b.iter().copied(),
+ }
+ }
+
+ pub fn ring_moves(&self, start: Coord) -> Vec
{
+ self.check_invariants();
+
+ let mut moves = Vec::new();
+
+ for d in DIRECTIONS {
+ let mut current = start + d.direction();
+
+ // Skip over arbitrarily many free spaces
+ while current.is_inside_board() && self.is_free(current) {
+ moves.push(current);
+ current = current + d.direction();
+ }
+
+ // Skip over arbitrarily many markers, but stop immediately after
+ while self.has_marker_at(current) && current.is_inside_board() {
+ current = current + d.direction();
+ }
+
+ if current.is_inside_board() && self.is_free(current) {
+ moves.push(current);
+ }
+ }
+
+ moves
+ }
+
+ pub fn is_valid_ring_move(&self, start: Coord, end: Coord) -> bool {
+ self.check_invariants();
+
+ self.ring_moves(start).contains(&end)
+ }
+
+ fn can_place_marker_at(&self, coord: Coord, player: Player) -> bool {
+ self.check_invariants();
+
+ // TODO: is this logic correct?
+ (self.element_at(coord) == Element::Ring(player)) && !self.ring_moves(coord).is_empty()
+ }
+
+ pub fn marker_moves(&self, player: Player) -> impl Iterator- + '_ {
+ self.check_invariants();
+
+ self.ring_coords(player)
+ .filter(move |c| self.can_place_marker_at(*c, player))
+ }
+
+ pub fn flip_markers_between(&mut self, start: Coord, end: Coord) {
+ self.check_invariants();
+
+ debug_assert!(start.is_inside_board());
+ debug_assert!(end.is_inside_board());
+
+ for coord in Coord::between(start, end) {
+ if let Element::Marker(player) = self.element_at_mut_unchecked(coord) {
+ player.flip();
+
+ if *player == Player::A {
+ self.markers_a.push(coord);
+ self.markers_b.retain(|&x| x != coord);
+ } else {
+ self.markers_b.push(coord);
+ self.markers_a.retain(|&x| x != coord);
+ }
+ }
+ }
+ }
+
+ /// Returns the coordinates of a run (if one exists), starting at the given seed.
+ pub fn run_coords_from(&self, start: Coord) -> Option
> {
+ self.check_invariants();
+
+ let seed = self.element_at(start);
+ let player = seed.player().unwrap();
+
+ debug_assert!(seed.is_marker());
+
+ for a in AXES {
+ let markers_positive = (1i8..=4i8)
+ .map(|i| start + a.direction() * i)
+ .take_while(|c| self.element_at(*c) == Element::Marker(player));
+ let markers_negative = (1i8..=4i8)
+ .map(|i| start - a.direction() * i)
+ .take_while(|c| self.element_at(*c) == Element::Marker(player));
+
+ let run_coords: Vec<_> = iter::once(start)
+ .chain(markers_positive.interleave(markers_negative))
+ .take(5)
+ .collect();
+
+ if run_coords.len() == 5 {
+ return Some(run_coords);
+ }
+ }
+
+ None
+ }
+
+ fn check_run_along_line(&self, start: Coord, direction: Coord, steps: i8) -> CheckRunResult {
+ let mut num_consecutive = 0;
+ let mut player = None;
+
+ let mut a_has_run = false;
+ let mut b_has_run = false;
+
+ for n in 0..steps {
+ let coord = start + direction * n;
+
+ match self.element_at_unchecked(coord) {
+ Element::Marker(p) => {
+ if Some(p) == player {
+ num_consecutive += 1;
+
+ if num_consecutive == 5 {
+ if p == Player::A {
+ a_has_run = true;
+ } else {
+ b_has_run = true;
+ }
+ }
+ } else {
+ player = Some(p);
+ num_consecutive = 1;
+ }
+ }
+ _ => {
+ player = None;
+ num_consecutive = 0;
+ }
+ }
+ }
+
+ CheckRunResult {
+ a_has_run,
+ b_has_run,
+ }
+ }
+
+ /// Returns true if the player has a run
+ pub fn check_run(&self) -> CheckRunResult {
+ self.check_invariants();
+
+ let mut result = CheckRunResult {
+ a_has_run: false,
+ b_has_run: false,
+ };
+
+ // This is not pretty, but it's 10x faster than the naive implementation using run_coords_from
+ // and 5x faster than running across the [-5..5] x [-5..5] grid and checking for valid coordinates.
+
+ // x direction
+ let x = Coord::new(1, 0);
+ result.or(&self.check_run_along_line(Coord::new(-1, 4), x, 7));
+ result.or(&self.check_run_along_line(Coord::new(-2, 3), x, 8));
+ result.or(&self.check_run_along_line(Coord::new(-3, 2), x, 9));
+ result.or(&self.check_run_along_line(Coord::new(-4, 1), x, 10));
+ result.or(&self.check_run_along_line(Coord::new(-4, 0), x, 9));
+ result.or(&self.check_run_along_line(Coord::new(-5, -1), x, 10));
+ result.or(&self.check_run_along_line(Coord::new(-5, -2), x, 9));
+ result.or(&self.check_run_along_line(Coord::new(-5, -3), x, 8));
+ result.or(&self.check_run_along_line(Coord::new(-5, -4), x, 7));
+
+ // y direction
+ let y = Coord::new(0, 1);
+ result.or(&self.check_run_along_line(Coord::new(-4, -5), y, 7));
+ result.or(&self.check_run_along_line(Coord::new(-3, -5), y, 8));
+ result.or(&self.check_run_along_line(Coord::new(-2, -5), y, 9));
+ result.or(&self.check_run_along_line(Coord::new(-1, -5), y, 10));
+ result.or(&self.check_run_along_line(Coord::new(0, -4), y, 9));
+ result.or(&self.check_run_along_line(Coord::new(1, -4), y, 10));
+ result.or(&self.check_run_along_line(Coord::new(2, -3), y, 9));
+ result.or(&self.check_run_along_line(Coord::new(3, -2), y, 8));
+ result.or(&self.check_run_along_line(Coord::new(4, -1), y, 7));
+
+ // diagonal direction
+ let d = Coord::new(1, 1);
+ result.or(&self.check_run_along_line(Coord::new(-5, -1), d, 7));
+ result.or(&self.check_run_along_line(Coord::new(-5, -2), d, 8));
+ result.or(&self.check_run_along_line(Coord::new(-5, -3), d, 9));
+ result.or(&self.check_run_along_line(Coord::new(-5, -4), d, 10));
+ result.or(&self.check_run_along_line(Coord::new(-4, -4), d, 9));
+ result.or(&self.check_run_along_line(Coord::new(-4, -5), d, 10));
+ result.or(&self.check_run_along_line(Coord::new(-3, -5), d, 9));
+ result.or(&self.check_run_along_line(Coord::new(-2, -5), d, 8));
+ result.or(&self.check_run_along_line(Coord::new(-1, -5), d, 7));
+
+ result
+ }
+
+ /// Return coordinates that belong to a run. Multiple runs can exist at the same time.
+ pub fn run_coords(&self, player: Player) -> Vec {
+ self.check_invariants();
+
+ let mut run_coords = Vec::new();
+ for coord in self.marker_coords(player) {
+ if let Some(coords) = self.run_coords_from(coord) {
+ run_coords.extend(coords);
+ }
+ }
+ run_coords
+ }
+
+ pub fn remove_run(&mut self, seed: Coord) {
+ self.check_invariants();
+
+ let run_coords = self
+ .run_coords_from(seed)
+ .expect("remove_run called with invalid seed");
+ for coord in run_coords {
+ self.remove_marker(coord);
+ }
+ }
+
+ #[cfg(debug_assertions)]
+ fn check_invariants(&self) {
+ for coord in all_coords() {
+ match self.element_at(coord) {
+ Element::Ring(Player::A) => {
+ debug_assert!(self.rings_a.contains(&coord));
+ debug_assert!(!self.rings_b.contains(&coord));
+ }
+ Element::Ring(Player::B) => {
+ debug_assert!(self.rings_b.contains(&coord));
+ debug_assert!(!self.rings_a.contains(&coord));
+ }
+ Element::Marker(Player::A) => {
+ debug_assert!(self.markers_a.contains(&coord));
+ debug_assert!(!self.markers_b.contains(&coord));
+ }
+ Element::Marker(Player::B) => {
+ debug_assert!(self.markers_b.contains(&coord));
+ debug_assert!(!self.markers_a.contains(&coord));
+ }
+ Element::Empty => {
+ debug_assert!(!self.rings_a.contains(&coord));
+ debug_assert!(!self.rings_b.contains(&coord));
+ debug_assert!(!self.markers_a.contains(&coord));
+ debug_assert!(!self.markers_b.contains(&coord));
+ }
+ }
+ }
+
+ for coord in self.rings_a.iter().copied() {
+ debug_assert!(self.element_at(coord) == Element::Ring(Player::A));
+ }
+
+ for coord in self.rings_b.iter().copied() {
+ debug_assert!(self.element_at(coord) == Element::Ring(Player::B));
+ }
+
+ for coord in self.markers_a.iter().copied() {
+ debug_assert!(self.element_at(coord) == Element::Marker(Player::A));
+ }
+
+ for coord in self.markers_b.iter().copied() {
+ debug_assert!(self.element_at(coord) == Element::Marker(Player::B));
+ }
+ }
+
+ pub fn fill_board_from_lists(&mut self) {
+ let rings_a = self.rings_a.clone();
+ let rings_b = self.rings_b.clone();
+ let markers_a = self.markers_a.clone();
+ let markers_b = self.markers_b.clone();
+
+ for coord in rings_a {
+ self.insert_board_element_at(coord, Element::Ring(Player::A));
+ }
+
+ for coord in rings_b {
+ self.insert_board_element_at(coord, Element::Ring(Player::B));
+ }
+
+ for coord in markers_a {
+ self.insert_board_element_at(coord, Element::Marker(Player::A));
+ }
+
+ for coord in markers_b {
+ self.insert_board_element_at(coord, Element::Marker(Player::B));
+ }
+
+ self.check_invariants();
+ }
+
+ #[cfg(not(debug_assertions))]
+ fn check_invariants(&self) {}
+}
diff --git a/src/yinsh/core.rs b/src/yinsh/core.rs
new file mode 100644
index 0000000..03d5f37
--- /dev/null
+++ b/src/yinsh/core.rs
@@ -0,0 +1,164 @@
+use std::ops::{Add, Mul, Sub};
+
+use serde::{Deserialize, Serialize};
+
+/// All Yinsh coordinates lie on a hexagonal grid within a circle of radius 4.6.
+const BOARD_RADIUS_SQUARED: f32 = 4.6_f32 * 4.6_f32;
+
+/// Yinsh hex coordinates
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct Coord {
+ pub x: i8,
+ pub y: i8,
+}
+
+impl Coord {
+ pub fn new(x: i8, y: i8) -> Self {
+ Coord { x, y }
+ }
+
+ /// Check if the point lies within the boundaries of the board.
+ pub fn is_inside_board(&self) -> bool {
+ let sqrt_3 = 3.0_f32.sqrt();
+ let x = self.x as f32;
+ let y = self.y as f32;
+ (0.5 * sqrt_3 * x).powi(2) + (0.5 * x - y).powi(2) <= BOARD_RADIUS_SQUARED
+ }
+
+ pub fn is_on_same_line_as(&self, other: Coord) -> bool {
+ (self.x == other.x) || (self.y == other.y) || ((self.x - self.y) == (other.x - other.y))
+ }
+
+ fn shorten(&self) -> Coord {
+ Coord {
+ x: self.x.max(-1).min(1),
+ y: self.y.max(-1).min(1),
+ }
+ }
+
+ pub fn between(a: Coord, b: Coord) -> Vec {
+ let mut coords = Vec::new();
+ let delta = (b - a).shorten();
+ let mut current = a + delta;
+ while current != b {
+ coords.push(current);
+ current = current + delta;
+ }
+ coords
+ }
+
+ pub fn norm(&self) -> f32 {
+ ((self.x as f32).powi(2) + (self.y as f32).powi(2)).sqrt()
+ }
+}
+
+impl Add for Coord {
+ type Output = Coord;
+
+ fn add(self, other: Coord) -> Coord {
+ Coord {
+ x: self.x + other.x,
+ y: self.y + other.y,
+ }
+ }
+}
+
+impl Sub for Coord {
+ type Output = Coord;
+
+ fn sub(self, rhs: Coord) -> Self::Output {
+ Coord {
+ x: self.x - rhs.x,
+ y: self.y - rhs.y,
+ }
+ }
+}
+
+impl Mul for Coord {
+ type Output = Coord;
+
+ fn mul(self, rhs: i8) -> Self::Output {
+ Coord {
+ x: self.x * rhs,
+ y: self.y * rhs,
+ }
+ }
+}
+
+/// The six hex directions
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Direction {
+ N,
+ NE,
+ SE,
+ S,
+ SW,
+ NW,
+}
+
+impl Direction {
+ pub fn direction(&self) -> Coord {
+ match self {
+ Direction::N => Coord { x: -1, y: 0 },
+ Direction::S => Coord { x: 1, y: 0 },
+ Direction::NE => Coord { x: -1, y: -1 },
+ Direction::SW => Coord { x: 1, y: 1 },
+ Direction::NW => Coord { x: 0, y: 1 },
+ Direction::SE => Coord { x: 0, y: -1 },
+ }
+ }
+}
+
+pub const DIRECTIONS: &'static [Direction; 6] = &[
+ Direction::N,
+ Direction::NE,
+ Direction::SE,
+ Direction::S,
+ Direction::SW,
+ Direction::NW,
+];
+
+pub const AXES: &'static [Direction; 3] = &[Direction::N, Direction::NE, Direction::NW];
+
+/// Player types (white and black)
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum Player {
+ A,
+ B,
+}
+
+impl Player {
+ pub fn next(self) -> Self {
+ match self {
+ Player::A => Player::B,
+ Player::B => Player::A,
+ }
+ }
+
+ pub fn flip(&mut self) {
+ *self = self.next();
+ }
+}
+
+pub fn all_coords() -> Vec {
+ let mut coords = Vec::new();
+ for x in -5..=5 {
+ for y in -5..=5 {
+ let coord = Coord { x, y };
+ if coord.is_inside_board() {
+ coords.push(coord);
+ }
+ }
+ }
+ coords
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn number_of_grid_intersections() {
+ assert_eq!(all_coords().len(), 85);
+ }
+}
diff --git a/src/yinsh/game_state.rs b/src/yinsh/game_state.rs
new file mode 100644
index 0000000..8dfa2e6
--- /dev/null
+++ b/src/yinsh/game_state.rs
@@ -0,0 +1,181 @@
+use std::path::Path;
+
+use serde::{Deserialize, Serialize};
+
+use super::{Board, Coord, Player};
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum TurnMode {
+ /// place a ring on a free field
+ RingPlacement,
+
+ /// place a marker in one of your rings
+ MarkerPlacement,
+
+ /// wait for ring movement
+ WaitForRingMovement(Coord),
+
+ /// move the ring at the given position
+ RingMovement(Coord),
+
+ /// Remove (one of your) run(s). The parameter
+ /// holds the last player who moved a ring.
+ RunRemoval(Player),
+
+ /// Wait for run removal
+ WaitForRunRemoval(Player),
+
+ /// Remove one of your rings
+ RingRemoval(Player),
+
+ WaitForRingRemoval(Player),
+
+ /// Do nothing
+ WaitForMarkerPlacement,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Move {
+ PlaceRing(Coord),
+ PlaceMarker(Coord),
+ MoveRing(Coord, Coord),
+ RemoveRun(Coord),
+ RemoveRing(Coord),
+ Wait,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GameState {
+ pub active_player: Player,
+ pub turn_mode: TurnMode,
+ pub board: Board,
+ pub points_a: usize,
+ pub points_b: usize,
+}
+
+impl GameState {
+ pub fn initial() -> Self {
+ Self {
+ active_player: Player::A,
+ turn_mode: TurnMode::RingPlacement,
+ board: Board::empty(),
+ points_a: 0,
+ points_b: 0,
+ }
+ }
+
+ pub fn perform_move(&mut self, player_move: &Move) {
+ // println!();
+ // println!("Current turn mode: {:?}", self.turn_mode);
+ // println!("Current active player: {:?}", self.active_player);
+ // println!("Move: {:?}", player_move);
+
+ match (&self.turn_mode, player_move) {
+ (TurnMode::RingPlacement, Move::PlaceRing(coord)) => {
+ self.board.add_ring(self.active_player, *coord);
+
+ self.turn_mode = if self.board.num_rings() <= 9 {
+ TurnMode::RingPlacement
+ } else {
+ TurnMode::MarkerPlacement
+ };
+ }
+ (TurnMode::MarkerPlacement, Move::PlaceMarker(coord)) => {
+ self.board.remove_ring(*coord);
+ self.board.add_marker(self.active_player, *coord);
+
+ self.turn_mode = TurnMode::WaitForRingMovement(*coord);
+ }
+ (TurnMode::WaitForRingMovement(start), Move::Wait) => {
+ self.turn_mode = TurnMode::RingMovement(*start);
+ }
+ (TurnMode::RingMovement(_), Move::MoveRing(start, end)) => {
+ self.board.add_ring(self.active_player, *end);
+
+ self.board.flip_markers_between(*start, *end);
+
+ let result = self.board.check_run();
+
+ if result.has_run(self.active_player) {
+ self.turn_mode = TurnMode::WaitForRunRemoval(self.active_player);
+ } else if result.has_run(self.active_player.next()) {
+ self.turn_mode = TurnMode::RunRemoval(self.active_player);
+ } else {
+ self.turn_mode = TurnMode::MarkerPlacement;
+ }
+ }
+ (TurnMode::WaitForRunRemoval(player_last_ring_move), Move::Wait) => {
+ self.turn_mode = TurnMode::RunRemoval(*player_last_ring_move);
+ }
+ (TurnMode::RunRemoval(player_last_ring_move), Move::RemoveRun(coord)) => {
+ self.board.remove_run(*coord);
+
+ if self.active_player == Player::A {
+ self.points_a += 1;
+ } else {
+ self.points_b += 1;
+ }
+
+ self.turn_mode = TurnMode::WaitForRingRemoval(*player_last_ring_move);
+ }
+ (TurnMode::WaitForRingRemoval(player_last_ring_move), Move::Wait) => {
+ self.turn_mode = TurnMode::RingRemoval(*player_last_ring_move);
+ }
+ (TurnMode::RingRemoval(player_last_ring_move), Move::RemoveRing(coord)) => {
+ self.board.remove_ring(*coord);
+
+ let result = self.board.check_run();
+
+ self.turn_mode = if result.has_run(self.active_player) {
+ // Active player has a second run, other player needs to wait
+ TurnMode::WaitForRunRemoval(*player_last_ring_move)
+ } else if result.has_run(self.active_player.next()) {
+ // Other player has a run, active player needs to remove it
+ TurnMode::RunRemoval(*player_last_ring_move)
+ } else if self.active_player == *player_last_ring_move {
+ TurnMode::MarkerPlacement
+ } else {
+ TurnMode::WaitForMarkerPlacement
+ };
+ }
+ (TurnMode::WaitForMarkerPlacement, Move::Wait) => {
+ self.turn_mode = TurnMode::MarkerPlacement;
+ }
+ (turn_mode, player_move) => {
+ unreachable!(
+ "Received unexpected player move {player_move:?} in mode {turn_mode:?}"
+ )
+ }
+ }
+
+ self.active_player.flip();
+
+ // println!("New turn mode: {:?}", self.turn_mode);
+ // println!("New active player: {:?}", self.active_player);
+ }
+
+ pub fn winner(&self) -> Option {
+ if self.points_a >= 3 {
+ Some(Player::A)
+ } else if self.points_b >= 3 {
+ Some(Player::B)
+ } else {
+ None
+ }
+ }
+
+ pub fn save_to>(&self, path: P) {
+ let file = std::fs::File::create(path).unwrap();
+ serde_yaml::to_writer(file, &self).unwrap();
+ }
+
+ pub fn load_from>(path: P) -> Self {
+ let file = std::fs::File::open(path).unwrap();
+ let mut game_state: GameState = serde_yaml::from_reader(file).unwrap();
+
+ // Patch up the 2D board, which is not serialized/deserialized
+ game_state.board.fill_board_from_lists();
+
+ game_state
+ }
+}
diff --git a/src/yinsh/mod.rs b/src/yinsh/mod.rs
new file mode 100644
index 0000000..4be0fd0
--- /dev/null
+++ b/src/yinsh/mod.rs
@@ -0,0 +1,8 @@
+mod board;
+mod core;
+mod game_state;
+
+pub use board::Board;
+pub use core::all_coords;
+pub use core::{Coord, Player, DIRECTIONS};
+pub use game_state::{Move, GameState, TurnMode};
diff --git a/tests/ai.rs b/tests/ai.rs
new file mode 100644
index 0000000..41a0344
--- /dev/null
+++ b/tests/ai.rs
@@ -0,0 +1,56 @@
+use yinsh::{Coord, GameState, Heuristic, Move, Player, SimpleHeuristic};
+
+#[test]
+fn midgame_1() {
+ let heuristic = SimpleHeuristic {
+ f_points: 1_000,
+ f_markers: 10,
+ f_controlled_markers_own: 0,
+ f_controlled_markers_opponent: 0,
+ f_accessible_fields: 0,
+ };
+
+ let mut game_state = GameState::load_from("tests/midgame_1.yml");
+
+ assert_eq!(game_state.active_player, Player::A);
+
+ game_state.perform_move(&Move::PlaceMarker(Coord::new(2, 1)));
+ game_state.perform_move(&Move::Wait);
+ game_state.perform_move(&Move::MoveRing(Coord::new(2, 1), Coord::new(2, 2)));
+
+ assert_eq!(game_state.active_player, Player::B);
+
+ game_state.perform_move(&Move::PlaceMarker(Coord::new(-3, -4)));
+ game_state.perform_move(&Move::Wait);
+ game_state.perform_move(&Move::MoveRing(Coord::new(-3, -4), Coord::new(-4, -4)));
+
+ assert_eq!(game_state.active_player, Player::A);
+
+ game_state.perform_move(&Move::PlaceMarker(Coord::new(2, 2)));
+ game_state.perform_move(&Move::Wait);
+ game_state.perform_move(&Move::MoveRing(Coord::new(2, 2), Coord::new(2, 3)));
+
+ assert_eq!(game_state.active_player, Player::B);
+
+ game_state.perform_move(&Move::Wait);
+
+ assert_eq!(heuristic.evaluate_for_player_a(&game_state), -1080);
+
+ game_state.perform_move(&Move::RemoveRun(Coord::new(2, 2)));
+ game_state.perform_move(&Move::Wait);
+
+ assert_eq!(
+ heuristic.evaluate_for_player_a(&game_state),
+ -1080 + 1000 - 5 * 10
+ );
+
+ game_state.perform_move(&Move::RemoveRing(Coord::new(0, -3)));
+ game_state.perform_move(&Move::PlaceMarker(Coord::new(-1, 1)));
+ game_state.perform_move(&Move::Wait);
+ game_state.perform_move(&Move::MoveRing(Coord::new(-1, 1), Coord::new(-1, 2)));
+
+ assert_eq!(
+ heuristic.evaluate_for_player_a(&game_state),
+ -1080 + 1000 - 5 * 10 - 10
+ );
+}
diff --git a/tests/board.rs b/tests/board.rs
new file mode 100644
index 0000000..25ef418
--- /dev/null
+++ b/tests/board.rs
@@ -0,0 +1,90 @@
+use yinsh::{Board, Coord, Player, DIRECTIONS};
+
+#[test]
+fn basic_marker_placement() {
+ let mut board = Board::empty();
+
+ for c in yinsh::all_coords() {
+ assert!(board.is_free(c));
+ }
+
+ let marker_coord = Coord::new(3, -2);
+ board.add_marker(Player::A, marker_coord);
+
+ assert!(!board.is_free(marker_coord));
+ assert_eq!(board.num_markers(Player::A), 1);
+ assert_eq!(board.num_markers(Player::B), 0);
+
+ board.remove_marker(marker_coord);
+
+ assert!(board.is_free(marker_coord));
+
+ assert_eq!(board, Board::empty());
+}
+
+#[test]
+fn basic_ring_placement() {
+ let mut board = Board::empty();
+
+ let ring_coord = Coord::new(3, -2);
+ board.add_ring(Player::A, ring_coord);
+
+ assert!(!board.is_free(ring_coord));
+
+ assert!(board.has_ring_at(ring_coord, Player::A));
+
+ board.remove_ring(ring_coord);
+
+ assert!(board.is_free(ring_coord));
+
+ assert_eq!(board, Board::empty());
+}
+
+#[test]
+fn check_run_basic() {
+ let mut board = Board::empty();
+
+ assert!(board.check_run().no_runs());
+
+ board.add_marker(Player::A, Coord::new(0, -2));
+ board.add_marker(Player::A, Coord::new(0, -1));
+ board.add_marker(Player::A, Coord::new(0, 0));
+ board.add_marker(Player::A, Coord::new(0, 1));
+
+ assert!(board.check_run().no_runs());
+
+ board.add_marker(Player::A, Coord::new(0, 2));
+
+ assert!(board.check_run().has_run(Player::A));
+ assert!(!board.check_run().has_run(Player::B));
+
+ board.add_marker(Player::B, Coord::new(-1, -1));
+ board.add_marker(Player::B, Coord::new(-1, 0));
+ board.add_marker(Player::B, Coord::new(-1, 1));
+ board.add_marker(Player::B, Coord::new(-1, 2));
+
+ assert!(!board.check_run().has_run(Player::B));
+
+ board.add_marker(Player::B, Coord::new(-1, -2));
+
+ assert!(board.check_run().has_run(Player::A));
+ assert!(board.check_run().has_run(Player::B));
+}
+
+#[test]
+fn check_run_exhaustive() {
+ for c in yinsh::all_coords() {
+ for d in DIRECTIONS {
+ if (c + d.direction() * 4).is_inside_board() {
+ dbg!(c, d.direction());
+ let mut board = Board::empty();
+
+ for i in 0..=4i8 {
+ board.add_marker(Player::A, c + d.direction() * i);
+ }
+
+ assert!(board.check_run().has_run(Player::A));
+ }
+ }
+ }
+}
diff --git a/tests/midgame_1.yml b/tests/midgame_1.yml
new file mode 100644
index 0000000..42907ab
--- /dev/null
+++ b/tests/midgame_1.yml
@@ -0,0 +1,81 @@
+active_player: A
+turn_mode: MarkerPlacement
+board:
+ rings_a:
+ - x: -1
+ y: -2
+ - x: 0
+ y: -3
+ - x: 0
+ y: 1
+ - x: -2
+ y: -4
+ - x: 2
+ y: 1
+ rings_b:
+ - x: -1
+ y: 1
+ - x: -3
+ y: -4
+ - x: -2
+ y: -1
+ - x: 1
+ y: -1
+ markers_a:
+ - x: -2
+ y: -2
+ - x: 1
+ y: -2
+ - x: -5
+ y: -4
+ - x: -2
+ y: -3
+ - x: -3
+ y: -3
+ - x: 1
+ y: -3
+ - x: 2
+ y: -2
+ - x: 2
+ y: -1
+ - x: 2
+ y: 0
+ markers_b:
+ - x: 3
+ y: 4
+ - x: 1
+ y: 4
+ - x: 3
+ y: 1
+ - x: -4
+ y: -5
+ - x: 3
+ y: 3
+ - x: -2
+ y: -5
+ - x: 1
+ y: 2
+ - x: 1
+ y: 3
+ - x: 0
+ y: 2
+ - x: -4
+ y: -2
+ - x: -4
+ y: -1
+ - x: -4
+ y: -3
+ - x: -1
+ y: 0
+ - x: 1
+ y: 0
+ - x: -3
+ y: 0
+ - x: -3
+ y: -2
+ - x: -1
+ y: -3
+ - x: 0
+ y: -2
+points_a: 0
+points_b: 1
diff --git a/web/.gitignore b/web/.gitignore
new file mode 100644
index 0000000..3c68218
--- /dev/null
+++ b/web/.gitignore
@@ -0,0 +1,2 @@
+/yinsh_bg.wasm
+/yinsh.js
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..0f0c06f
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/yinsh.cabal b/yinsh.cabal
deleted file mode 100644
index 4190a19..0000000
--- a/yinsh.cabal
+++ /dev/null
@@ -1,41 +0,0 @@
-cabal-version: 2.4
-name: yinsh
-version: 0.2.0.0
-synopsis: The board game Yinsh, including AI opponent
-bug-reports: https://github.com/sharkdp/yinsh
-license: MIT
-author: David Peter
-maintainer: mail@david-peter.de
-category: Game
-extra-source-files:
- README.md
-
-library
- exposed-modules:
- Yinsh
- AI
- Floyd
-
- other-extensions: BangPatterns FlexibleInstances UndecidableInstances
- build-depends:
- base ^>=4.16.4.0,
- containers ^>=0.6.5.1,
- game-tree ^>=0.1.0.0
-
- hs-source-dirs: src
- default-language: Haskell2010
-
-executable yinsh-backend
- main-is: Main.hs
-
- other-extensions: BangPatterns FlexibleInstances UndecidableInstances
- build-depends:
- base ^>=4.16.4.0,
- containers ^>=0.6.5.1,
- happstack-server ^>=7.8.0.2,
- happstack-server-tls ^>=7.2.1.3,
- yinsh
-
- hs-source-dirs: backend
- default-language: Haskell2010
- ghc-options: -threaded -rtsopts