Connect4 ⚫⚫⚫⚫

en.wikipedia.org/wiki/connect_four

This is a Turtletoyed port of Sebastian Macke's Connect4: simulationcorner.net/index.php?page=connectfour

#game

Log in to post a comment.

const seed = 'Change this text = change computer behaviour (do not change during a game!)'; //type=string
const moves = ''; //type=string

const moveArr = moves.match(/([1234567])/g)
const legalMoves = moveArr? moveArr.join(''): '';

// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(.7);

// Global code will be evaluated once.
init();
loadHatcheryNamespace();
const turtle = new Turtle();
const polygons = new Polygons();

// Seedable random number generator by David Bau: http://davidbau.com/archives/2010/01/30/random_seeds_coded_hints_and_quintillions.html
!function(a,b,c,d,e,f,g,h,i){function j(a){var b,c=a.length,e=this,f=0,g=e.i=e.j=0,h=e.S=[];for(c||(a=[c++]);d>f;)h[f]=f++;for(f=0;d>f;f++)h[f]=h[g=s&g+a[f%c]+(b=h[f])],h[g]=b;(e.g=function(a){for(var b,c=0,f=e.i,g=e.j,h=e.S;a--;)b=h[f=s&f+1],c=c*d+h[s&(h[f]=h[g=s&g+b])+(h[g]=b)];return e.i=f,e.j=g,c})(d)}function k(a,b){var c,d=[],e=typeof a;if(b&&"object"==e)for(c in a)try{d.push(k(a[c],b-1))}catch(f){}return d.length?d:"string"==e?a:a+"\0"}function l(a,b){for(var c,d=a+"",e=0;e<d.length;)b[s&e]=s&(c^=19*b[s&e])+d.charCodeAt(e++);return n(b)}function m(c){try{return o?n(o.randomBytes(d)):(a.crypto.getRandomValues(c=new Uint8Array(d)),n(c))}catch(e){return[+new Date,a,(c=a.navigator)&&c.plugins,a.screen,n(b)]}}function n(a){return String.fromCharCode.apply(0,a)}var o,p=c.pow(d,e),q=c.pow(2,f),r=2*q,s=d-1,t=c["seed"+i]=function(a,f,g){var h=[];f=1==f?{entropy:!0}:f||{};var o=l(k(f.entropy?[a,n(b)]:null==a?m():a,3),h),s=new j(h);return l(n(s.S),b),(f.pass||g||function(a,b,d){return d?(c[i]=a,b):a})(function(){for(var a=s.g(e),b=p,c=0;q>a;)a=(a+c)*d,b*=d,c=s.g(1);for(;a>=r;)a/=2,b/=2,c>>>=1;return(a+c)/b},o,"global"in f?f.global:this==c)};if(l(c[i](),b),g&&g.exports){g.exports=t;try{o=require("crypto")}catch(u){}}else h&&h.amd&&h(function(){return t})}(this,[],Math,256,6,52,"object"==typeof module&&module,"function"==typeof define&&define,"random");
Math.seedrandom(seed);

const preLoadedHatches = Array.from({length: 7}, () => Array.from({length: 6}, () => [
    new LineHatching(Math.PI / 8 + Math.random() * Math.PI * .5, .2, .2),
    new LineHatching(Math.PI / 8 + Math.random() * Math.PI * .5, .5, .3)
]));
Math.seedrandom(seed);

const texts = {
    logo: new Text({"name":"EMSSwiss","unitsPerEm":1000,"ascent":800,"descent":-200,"capHeight":500,"xHeight":300,"glyphs":"IADEDgDAIQDyEhoYjN3kDEb2AED2CWv9EAko\/jIK5P4OCyj+AEAAwCIA0gpcCFjasARy4wBAHg862pALcuMAQADAIwASKvoZXOCqFArnIhCQ7XILLvWABw78AEBEJVzg\/h8K52wbkO3kFi711BIO\/ABA3ghc6uQleuoAQOwE+vGYITbyAEAAwCQAfhjkFvjp3hdk5\/wXcOVcFzDkCBZU46oU+uJSEvri3A9s5KYO6OWsDQTo6A0W6gAPkusiECztFhJm73QTlvEKFLLzzhPs9dQS\/vciEBr6hA26+pALuvr8CNT5RAdE+IYGyPZoBqz0pAZY80QHfPLkB5bxAEBEFgjf\/AgXAgBAAMAlAPYniA5Q3TIKeOKAB8TmpAba6SYHzuvkB1DsVglu7MwLdOumDjrpFhLu5M4THuLIFIDfqhTI3QoUktxSEjTbohdM3Egcktw6IHTckCTy2\/4nNNvSKpTaAiaM3egcrOXIFCztTgxG9rwHlvv0AjsBAEDiHULwVBo09D4X2vgIFm78RBZn\/gIXov9gGKL\/ThuG\/sgeUPu8IGL49iIu9bQjuPK0Ix7xFCPA71YiAu96IWzuniAm7gBAAMAmAIQmXh8S8ygeZPYqHFz5PBmO\/BAT4f\/iDn4ANgvh\/54Ha\/3gBtj6YgeC9vYJ1vKsDULwfBCK7jIUUOyiFxbqchqC5wwcSOVIHBjj1hp+4d4XOOHMFTbjyBRw5fAUpOggFyDv4B8k\/24jUgPkJewEPShGBTQqkgRsLPQCwi78AJ8vQ\/8AQBwR+PigD2T23A+s9LwRdvPsE9bybBu48tQhEvMsJDDzzCQS8wBAAMAnAAgHOAle2SgFkOMAQADAKABYEc4iyNbQG8DYqhRM3MQOouAaCYjmbgVu7LEDuvBXAuD31QLo\/QoFvQCAB9gB2An4Aa4LWgEAQADAKQBYIHQT59aAFojWWhmF104bHNoMHCbfuBri5lwXIO80Eqb1rA22+d4IrfzQA2L\/JP\/cAJb7mgEAQADAKgAcIJgSXtkSDMDvAEDgBgjfYBg06gBAMwM06pQb6t4AQADAKwDwFNoRgOlaCvD7AEBMBNzxuhgY8gBAAMAsACAIFAPN\/NADhv6xA+H\/dgIXAp4AEAQAQADALQDcD3IDLvUwDEz1AEAAwC4AugkXAkv9OwHo\/TcCpv4zA+j9AEAAwC8A3hekH4DYQBBW69gBDP1D\/9wAAEAAwDAAhBfMC2rmngd66uwEAu9SA4701QKe+PIDMPwEBij+XAjk\/tIKR\/6EDY784BA++S4TiPXwFLrw6hXO62gVpOgKFMrlmBLu5JoQsuQeD7LkZg1w5cwLauYAQADAMQCgD4YGHOlkD\/Tj7gxM5koGrfxMBEf+ov\/F\/lQL5P4aCcX+hgZu\/ABAAMAyAMgUwAgQ65wJBuYMDU7kdhHW4woUbORoFejlCBZ86GgVVuvUEoruQg4S834JQPfcAOT+dgJH\/gQGKP4UCkf+hA1H\/r4PS\/1YEbT7VhO8+ABAAMAzAFAUgAcQ6+QH1Or8CAbmVAuy5KAPEuQWEpTkbhSs5fAUQOhuFFzqthJu7CIQCO5IDWbvfgn876wNfvBAEDzxvBG48vgRpvWaED75pg4w\/BIMCP5+CeT+LAZD\/1IDBP\/4Aej9WgHs\/FcCvPgAQADANACWGfwXPPEKFM71Wgoo9rUCRvZIDfLq2hFM5sgU7uTuDOz8nAlH\/mgGxf5SEuT+AA\/J\/SoN7PwAQADANQBoFYYVNOpCGNDkkAvQ5KQGJPDYCWbvhA1I7+AQYPCYEprycBJk9rgQ1PkkDm78bAwI\/jgJJP8EBkP\/sQME\/\/gBKP56Aez8VwI++QBAAMA2ACwVpBWk6KgWJOZiFtDkNBJy4wwNSOU4CXzowgb26+wEJPByA3D0tQKe+FIDzfwsBqb+GgnF\/k4M6P2gD1D7OhFc+VISKPZwEvTyOhG68KAPSO8qDcbuugmE7+QHnPBoBpbxbgX08nID7PUAQADANwBWE2gG+OmGBp7p3ggM5cwVSOU+F9DkthJ66moOZu\/YCaz0BAb8+fQCpv4AQADAOAAmFl4QVONODLLk\/AhA6J4HdOtcCETu2AmE76YOlPOgD871Rg+e+KwN0vu0Cij+mAgk\/6oFJP+SA+T+uQEs\/VoB2Pr0AkD3jAXK9D4I1vJkD2zucBKw61AUnunIFObn8BRI5ZITEuRSEpDjXhBU4wBAAMA5ACYWtQL8+fgBa\/2wBAT\/wAim\/ioNMPxAELz4mBIu9QoUPPFKFWzu6hV068wVfOiqFMrlLhNs5DoR9OPEDhLkTgzQ5D4IBOikBrbq5gWo7uAGWvGYCPTyNguy80IOMPPgEL7xmBJ+8DIUJu6kFdTqAEAAwDoAuglcCK7tgAdE7noIAu9WCUTuAEAQBKn9MwNH\/i4EBP8KBUf+AEAAwDsA2AkaCa7tPghE7lYJAu8yCkTuAECSA0f+CgVr\/ZIE3AC1Ai4EfgAEBgBAAMA8ABATvBEc6a4LaO0KBb7x9gmI9e4MgPgGDtT56A26+gBAAMA9AAgWEASK7lAUqO4AQFcCKPYuE0b2AEAAwD4A8hJKBhzpWgpK7U4MIO+CD\/rxegjs9WoEvPi1Ajj6FAO6+gBAAMA\/AAwchgap\/aoFR\/6kBgT\/gAdH\/gBA9gm44xgLgN8SDA7eyg0K3XYRLtyMFLbb5BZM3B4Z5t1UGiDg+hk244QXpuaqFKToWBF66ugNjOz0C0TuFAok8MAIVPJ6CKz0wAgo9hoJBPcAQADAQAC2MBoixOZMHabmfhiC50oVtupSEibuuBD68dwPEPVeEKr2WBFk9lAUiPXAFzbyeBlC8O4baO3IHl7opB8o59wZ1vL2GOz1Hhki9wYdBPeYIQr2QCcw8zQqou\/uK3TrLCy+51ArlOQ9KH7hMiNE34IeaN72GMzezhM+4IQNuON6CP7oBAYC74wF0PPCBvj4fgmO\/AYOBP9WE+H\/uhiC\/3Acpv68IOz8tCO6+oolnvgAQADAQQBoM34AvPii\/zL7wf\/J\/XoBYv+SA8H\/4AaC\/zYLS\/24EHr5Ehuc8NQhnulsJazlPSj64m8rPuCDLuremjDI3dYxbt0QM27dqjRQ3YI2Ltz8N5jb9TlS25431NtnNezcjjPM3towluKiLuLmiyyY6pMqAu++J7Lz5CXa+CYltPsmJej95CXk\/t4mBP97KCT\/NCqG\/mwsjvxkLpL6+TBA9wBAOAno9PwI0PMUCvrxEgwA8UYP3u80EoTvyBRm7\/YYhO+mHd7vmCHe764kwO8CJsDvgCfk7gBArBzi5pQbSOUqHFriRh4C4BoiaN67KIzdyiyq3TgxjN0AQADAQgCMI0wEBuYUA2zk1QIY49UCOOFSAyDgzgQI3wgHLN72CardZg1u3f4QMt3wFIzd3hdQ3cocjN1AH6TevCCi4DQhsuT+H4LnxB3a6bIbdOucGNLsRBZo7bgQbO5YEa7thBfq7RIbqO4oHn7wgh7u88Qd4PdsG\/b62Bgs\/aoUJP\/aEeH\/7gzh\/xQKQ\/+AB2f+SgZr\/YwFFPsuBOz8SgZr\/QBAYhbI3ZITxt8cEdbjsQPw+xcC6P0\/AIL\/Yv8fAGb+PwAAQADAQwBKJDogBub4IBLktiH24ZghXODgH6TeKB6q3U4bCt0IFozd3A8C4LQKTuSkBsLoTATS7LUCAPHYAez1VwL8+WoES\/3CBgT\/MgoAAKAPPwDwFEP\/lhnJ\/YIeuvqeIJ741CFA9wBAAMBEADgnkgPK5XYCcuNXAtjh9AIg4HIDYt\/IBYbePgjm3fQLbt0cEVDdIBcK3dAbUN3+H0rekiJi38wkuuFmJiTmAiba6YolbO6SInD0QB\/a+PQabvzGFmf+dBNi\/6APwf8SDMH\/nAmC\/4AHxf6GBij+RgWS+tADbvwsBij+AEBcFzLdsBPq3pQR\/OBuBZ74zgRW+vQCzfxaAQj+fgDk\/iT\/wf\/J\/QAAAEAAwEUANCGkFVDdcBLq3nYRIOAQBIb3dgJ0+lcCbvxSA6n9KAWG\/lQLYv\/+EEP\/8BSG\/iAXKP5aGc380Bu6+sgeQPcAQG4F6OXyA3jiTAQg4MgFCN8aCQ7ehA2M3bIbCt3aIIzdtiEI33ohwOCAIBjjAEAUCkrtPheu7fYYaO1UGm7sAEAAwEYAzCRKJFDdXh\/M3i4d\/OBgGBbqgg+6+oQNLP3wCgT\/eggAAOYFXgBSA8H\/2AHF\/n4ADP0\/ADL7HwBE+ABAyg3i5hIM7uRUC\/riNgsa4YQNpN5eECze7BPI3VQaMt2XKVDdjyuM3Sktqt3lLWje5S3k3wktWuLKLDbjAEA+F0rtbiOQ7ZAkzO1oJKjujCOc8DIjHvEAQADARwCQJOAfJOYWIfTjtiGc4RYh5N9eH+bdGBrs3LgQYt+WCvTjbgV66vQCfvB6AaT3lQIU+ygF6P3YCZ4AoA+9AIwU5P5yGvD7yhza+EAfLvUaIuTu6BzJ\/dwZhgaMFMQOiA7UEsAIUBQoBQoUegHaER8A3A8fAE4MAEAWEu7zdhF88nYR\/O8uE+TuqCUI7gBAAMBIACwzEgyO5bQKGOO0CsDgigwI3x4PLN7UEqrdCBaq3QoeUN3UIVDdKB7I3RIbGuH8FwbmkhOK7iIQpvWoDLr6Wgpr\/cIGYv+SAyT\/HAGG\/qb+bvxH\/pL6KP4i9wBAwjZw27Iy7Ny6MObdGSlW6\/YihvcaIpL6ViLN\/BQjhv7qJCT\/HSgk\/\/Eqyf2DLlb6ujBA9wBAtApm70gNqO4KHmbvMiNI72YmRO4AQADASQBcF9oRSOVeEJbiXhDG31ISht7IFMjdiiVu3cgeaN6UGzjhuhgG5jIU3u\/+EGr1rA10+pYKzfyAB4b+8gOG\/loByf3B\/w78xf4a+qb+HPgAQADASgCEDSwG4uZyA3LjkgOE4EoGpN6uC4zd5BYy3RATYt\/gEBrh0AyA6WIHpvUuBOz8AAD0AjD8wgZi+D4IiPWYCBjyJgd+8M4EJPDYAQBAAMBLACAmOAkG5oAHGONcCALg2Amk3jAMLN6gD4zd3Blu3eQWyN3yEt7gXhDu5JgImvJ2Amv9Q\/\/B\/\/wAZ\/6xAwj+UgNP\/ABAQC9E32ctyN09KA7eziLA4JQb7uRwEpjqNgvA7yoNbO52EQT3pBVu\/HgZZ\/6OHGf+fB9u\/LYh\/Pm0I0D3AEAAwEwAdCIGHSrlZB6W4qAext8GHQ7e+hns3IYVyN2YEsDgvg9w5agM9uvYCXzyngf+924FMvtyA478ngDo\/Uf+ov9H\/kf+sATo\/aAPKP5+GEf+jhxL\/cocT\/zoHDL7rBx0+pAawvcAQADATQAeN9wPpuZmDTDkSA0a4WoOqN+4EKTezhPI3UQWjN3aIFDdxB0I33gZ+uLGFljpLhMA8b4P5vYwDPb6eghH\/s4E5P4XAkf+BP+W+4b+uvpH\/oD4AEAEH0reph0a4WodpuZqHcDvTB1M9UwdpPesHD75Thsy+\/UxcuMcOOzctTlw2ys8vNrVObbbYzYI3xAzEuQpLYTv9imq9rsoUPvaKKn9kyok\/8os5P5bMG78czIa+qo0QPcAQADATgC+LSIQxOagD8TmhA2444QN3uBGDwjf1BLm3eQWjN22IW7dtiHI3cQdCN+yGz7gtBmW4t4XRudKFWjtthJY83wQpPcMDZb72AnJ\/WgGBP\/0AsX+Q\/+t\/Kb+uvoo\/mL4AECOHITg6Bww5FAjPvlsJWv9ziLB\/+QlevnOKzTqmjDk344zNNvGNeDZoja42Tw44NkYObzaGDlw2wBAAMBPAIolnBiw3FgRyN3wCn7hLAbi5jcC0uxeALjyPwBi+LkBMPywBGf+\/AjB\/+gNwf\/wFMn9Lh3g93QiuvDqJJ7pJiVO5LQjXODgH8jd6Bzs3JwYsNwAQADAUAA4IroYUN1KFYbedBMg4IwFGvoQBE\/81QLo\/TsBBP8E\/+H\/9ALo\/eYFa\/3mBZj5AEBqBKzlFAMY47EDxt+qBWjeFAqq3XASbt2WGTLdgh7I3Zgh3uDUIZTk2iAi6EYedOswG5DtwBfk7sgUZu\/cD4TvAEAAwFEAqCXcGbDcdBMy3XILwOBGBQrnuQEs7aL\/0PMAAPz51QJr\/eAGJP\/wCoL\/BBBD\/6oUqf1aGTL7oB4K9nQiJPDMJBzpkCS4484i5N+AICzeLh0y3dwZsNwAQJoQJP9uFOYF\/BeuC1QaxA7oHGQP\/h8AD\/AjZg0GJ9IKAEAAwFIA8CiSBKzlUgM24\/QCOOGxA+TfBAbM3lYJyN0GDm7dyhyM3RwgpN7UIR7i1CEM5TQh5udAH9TqBh0O7ZYZbO7MFUjvfBBm774P3u\/OE8L3AhdP\/HIaxf5kHgT\/+CCK\/fAj2PqEJkD3AECcGIzdyBSk3rYSGuHIBdT59ALo\/cH\/4f\/QA0v9yAVr\/YwFuvoAQADAUwBwHEgcDOXEHZzhCh4m37Ib7Nx+GEzcCBZM3FISDt4EEOTfxA6442QPBOiSE8ztwBcu9SAXdPrwFGv9FhJi\/8QOngAaCQAAkgQo\/pUCbvw7AXr5WgHO9bUCWPMAQADAVAD0GhQjRN\/KHBrhchq44xoYfOhoFYTvthLo9P4Q+PgqDWv9OAnB\/8gFXgAzA8H\/HAFH\/uH\/0vvB\/yD5AEDYAeLmov+U5OT+luLF\/vzgov\/G30wESt72CYzdoA\/m3W4jqN\/+J8bfVCqo348rLN4AQADAVQCQKb0AKuWC\/zziJP\/e4L0ApN4QBIzdYBgy3ZITLN68EVzgyg3C6JwJ3PFoBj75KAW0+84Exf6AB+H\/8Aok\/\/4QMvtEFqb19hjW8iocRO7eJ0retilM3GAnht7SI6jfGBpS9N4X8Pt+GKb+Thvh\/+IdQ\/+AIOj90iP2+iEnQPcAQADAVgAaIhwBcOWC\/1ri4f+o31cCDt4KBYzdfhiM3QBALhOM3Q4VrOVuFGjtthLg99wPmgEcEZ4Achru8\/YivOnlLUTfMDOY28I2dtq1OZTakjqY25I6UN3VOaLgAEAAwFcAuDigD4zdVhPS4lAUMuwuEy71QBAAAPgRHwBCGKr2\/h+w66ImuOMxKyDg4i6k3lgxLN7RMubdZy3G37Yp0uIGJyjnJiWo7q4kZPbSIxT78iGeAM4i3ADeJ5j5ZC7i8Ak1vOmyOrjjXj9E3yNGNNv3SHba0Ep22itM+NqJTEzcaUxK3sxLhOAAQDsBcOVeAFTjwf+E4L0AaN7yAw7e\/Aiq3fYYjN0AQADAWACuJAQQSt6gD+7kuBAs7XAS6PSkFdL7YBhn\/rIbR\/5kHjD8WCD8+TgiQPcAQI8rPOLKLIDfyiyq3RErsNzeJ3TcFCOk3o4cEuQsFdTqbAzK9PQCLP3Y+noBRPj4AYb3fgCk94b+\/vfo\/Zj5CP4AQL0AKuVi\/5biov\/k354A6t5XAizeRgWM3boYyN0AQADAWQCsHL4PDt5SEmDhsBPm5\/ISzO2UEcr0AECTKqLg0irI3bYpTNyiJkzcViJi38QdlOT2GBDrzhMS814QgPgqDc389gli\/0oGfgA3Ah8AHwCm\/qb+bvyG\/pj5AECSBHDlUgOW4hQDwOAuBCbfhgYs3ngKbt38F8jdAEAAwFoA7hveCAzlngeW4p4HIOB6CAjfNgss3iIQjN28IMjdegj08lcCnvhH\/qn9bvw\/AI78HAGK\/RwBAAAw\/PQCGvogCGL4Egxi+HwQ9vqoFtgBSBw+CFwhbAyQJAYOHSgkDnQqqAxvK\/AK7iuYCG8rwgbxKsgF+SiSAwBAAMBbAMYWyB7a2oQXOtqwE0zcHBHA4AAA7AQwDAoFJA6SBABAAMBcACIfov+42UgcvQAAQADAXQCcGKgMNNswG9raPgj0AsIGagRGBQoFmgFGBc38CgXw+0YFAEAAwF4AdBNyA+Tulgp66kAQ5O4AQADAXwBWCfwAyf1XAqb+cgOG\/nYCiv0AQADAYABqDjsBbOR6CFjpAEAAwGEA+BESDOTuRAdE7hcCHvHJ\/cj2bvuO\/DD8CP6G\/uj9KAVi+MIG5vYaCRLzWgoA8VoKZu+oDOTuwgae+G4FMPwKBSj+Sgam\/jgJa\/0MDRr6gg9A9wBAAMBiAIgOiv1A914A6PT0AjbygAc66VQLNuO4ELDc6hX+2boYmtmQGpTaEhsQ3ABAlQJ88oL\/vPjp\/W78CP5H\/mL\/BP8XAiT\/4Abw+\/YJwvcSDDT07gw88RIMZu+0CkjvpAZ88uwEjvT0Agr2AAB6+QBAAMBjAAYOMgr08rQKYPC6Caju5Acm7sgFqO5XAgDxJP9w9I78+Pi0+yz9a\/0k\/70ABP\/sBEv9\/Aga+rQKYvhyC0D3AEAAwGQAdhFAH\/jajhxe2fwXvNqSE0Tfvg8k5lQLiu6YCLLzSgYi93YC2PpeAI78qf0I\/vD7R\/5u+6n98Pv8+WL\/DPQ3ArrwagRI7wQGRO56CETuOAnG7jIKYPDmBZj57ATs\/G4Fhv68B0f+2AnN\/AwNtvkAD0D3AEAAwGUA4g4E\/yL30AOC9oAH6PS6CbjyFApg8LoJ5O4CCGzukgRm770AuPKp\/eb2MPwU+278S\/2p\/Wf+ngBD\/5IDhv7gBs382Al0+mwMQPcAQADAZgBUC04bENwSG5TaPBm42UQWuNlWExbbuBAy3QYOGuHwCkbnngeo7kYFcPT4Ac38gv\/YAY784Ab4+DIKZPbMCzDz7gwe8QwNiu5sDDLslgp0634J9ussBgBA0APk7i4E5O6oDMbuyg2K7gBAAMBnAIQNTgyo7hoJfPJoBkb2NwK6+n4Arfws\/Sj+0vtn\/hT7LP1P\/Fz5R\/4o9j8AsvNSA2DwjAXG7gIIiu4yCuTuNgvk7oYGuvq5AW4FDP3uDD75HBGO9M4TJPBQFBDrmBI66bgQpOhkD8LobAwAQADAaAC8EbgasNyQGhbbfhhA2YwUfNmaENrakAss3mIH0uIQBKTogv+a8rr6hv5Q+4b+JP+e+LUCUvQEBgDxXAii7\/AKqO7wCiTwuglU8m4FdPpqBE\/88gPo\/c4E5P6kBsX+OAnJ\/ZAL0vtGD0D3AEAAwGkAzAuK\/UD3\/AAS8\/IDQvBKBibu7AQk8OH\/tvmG\/gz9hv5n\/oL\/xf65AUf+bgUw\/IAHmPlWCUD3AED2CdDkOAlM5tIKTOY2C+7kAEAAwGoAXAhWCSbuSgaE7ygFYPA3Avj4S\/3QAz75cgvO9YIPlvGYEmjt7BPC6AoUTObyEjDkHBGQ4+IOuOOoDABAnAmU5N4IBuaWCiTmGAuy5ABAAMBrAD4XlBu225QblNq0GXzZaBU62uAQUN2EDfzgGgki6EYFQvDVAub2ngCW+yT\/hv4\/AEf+UgP4+AII0PNUC5zwQg4C7wQQZu98ECTwAEAmB1jzPgjU+VYJqf2QC6b+iA4I\/pQRMvvIFED3AEAAwGwArA2K\/UD3pAbk7jAMOum4EJbiEBPm3TIUmNsyFFjazhNe2S4TItlYESLZHg8021QLhODIBfLqegGU8wT\/+Pim\/s38HwCG\/rEDR\/5iB5b7NgtA9wBAAMBtAOIdiv1A9\/gB+vGwBEjv4AbG7pIElvGaAbz4Yv\/k\/nYCwvdiBxLzFAri8O4MAu9qDgLvxA7e79AM1vIaCRr6vAcs\/SAIyf0yCvj4Bg4Q9ToRNvJWE0LwpBUg7+QW5O6EFyTwShWy8xwRDP3aEeT+DhWp\/UIY2PpsG0D3AEAAwG4ADhWK\/UD3LgTk7m4Fxu7gBkTu7AQk8Gb+5P4E\/+T+cgNA9z4I+vEYC97vhA3k7uIOxu7iDsDvTgx285gIUPvkBwz95AeG\/jgJQ\/8YC4b+Zg3N\/JgSQPcAQADAbwC+D8gFxu7cAHzyqf3m9vD7MvsO\/Kn9S\/3F\/n4Apv5qBJb7RAcc+BoJyvRaCvrxeAr87\/YJ5O56CAjuYgcI7sgFxu4AQAoFUPuMBVD7wgbw+xQKVvpIDUD3AEAAwHAAhA3SCuLmWgrK5cAIjuXIBSjnVwJ66gAAwO9H\/qb1tPuW+9r42AEu9X4JvvEGDg7tFhKA6ewT0ORuFLrhdBMI3xwRaN6gD0reMAwAQEf+yPbVAlTypAaE73oI5O5+Cbrw5AeO9CgF+PjyAzL7+AFL\/eH\/hv6K\/WL\/tPsk\/zL7iv0AQADAcQDgEIQNqO6WCsbuYgdE7tUCJPAAAJTzyf3I9tL79vpQ+yz98Ptn\/kv9hv7F\/uj9\/AAw\/DMDdPoKBZ74\/AhY83gKYPB4CiDvzAsC7xAEhv5H\/vAKuvrOEw788hJeALQKCgXQA1QLMvtCDkD3AEAAwHIAJA6K\/UD3EATk7iwGqO5iB8ztCgVg8Ib+Q\/8k\/yT\/kgMK9pgIYPDSCkjvEgzG7kgNAu\/EDjzxag428ugNWPMAQADAcwCuC94I1vKcCZzwegiK7uYFCO7yAyDvdgJg8LkBfPIUA2r1qgWA+IYG1PmGBs38TAQk\/3oBPwCm\/oL\/rfyG\/m77zfxu+xT78Pu2+QBAiv1A9\/gBePEAQADAdAAMDYr9QPdaAVjzLgT870oGzO0aCebn9glq5nILSOWSBDbyOwGe+B8Abvzh\/yj+OwGm\/pIELP2ABzL7lgpA9wBAOwEC71cCqO5ODIruAEAAwHUAPBmK\/UD3uQF88nIDuvDIBUjvCAeo7oAHSO9oBtzxWgEU+34A6P2eAKb+NwLk\/kwEKP4IBzD8Vgl6+U4MZPYADxjyQBBI784TRO5YEUjvJA5S9K4L\/PmQC2v9Zg2G\/poQS\/10E1D7xhZA9wBAAMB2AAQQiv1A9\/gB3PGSA\/zvBAaK7vIDHvFXAmr1egEy+7kB6P2SAwT\/SgaK\/ZYK+PgqDaz0Qg6+8aYO3u+EDSbuAEAAwHcASByK\/UD3lQLe77UCZu+MBQLv8gN+8DcCgvY7AQ78HAEo\/hQDBP\/sBAj+3giS+jYLBPcqDbLzHg+i7zoRxu6+D+LwQg7o9GYNmPkqDc38Zg1n\/qYO5P52ESj+yBTw+yAXevmWGab1Thv68e4bYPDQG6jubBvM7bgaSu0AQADAeACSE+AGSO8oBULw7AQe8aoFRPhiB+z8mAhH\/rQK5P6QC4b+hA0M\/R4P9vocEUD3AEB0EwLvlBFI7yoNNvLeCGr19AKS+un9Q\/8AQADAeQDUEmv9aPd2ApbxKAUC7wIIIO8sBjbytQJi+D8Aiv1+AOT+LgRH\/vwIOPowDGT2Bg5Y80YPPPEiEGDwlBEg75ITqO6aEB7x0AyY+Z4HCgVqBPAKPwDcD0\/81BJA924UDPQyFCTwcBKo7r4PbO5sDABAAMB6AIgOsQMk8M4EZu9CDkjvAA\/e73oIyvS1Anr5ov9u\/Ef+6P0s\/cH\/a\/1+AOn9pv47ATD8qgUw\/HoIBP94CnoBSA0uBB4PqgVYESwGthLIBVAU7ASqFFIDbhTYAXQT3AAAQADAewBEFkYeTNwYGrDcYhYs3rATwOD4EU7k3A9A6MoNUOz2CeTuPgj878gFfvByA5zwPgjc8RQKdvP2CYL25gXB\/ygFtQKqBfIDCAeSBDgJKAWWCkYFcgtuBQBAAMB8AIQXWBFw2xQDJP8AQADAfQBUGogOktwQE5LczBVo3qQVeOLOE8rlFhJY6f4QUOxYEQLvkhNg8IQXPPHyEpbxoA8S8yoNiPV4Cvz5wAjk\/koGegFqBJID1QJqBIL\/CgUO\/AoFAEAAwH4A+BHVAtrp7ATg6KQGwujACJ7prgu26mYNeuoAD9rpvg866QBAAMA="}),
    default: new Text({"name":"HersheySans1","unitsPerEm":1000,"ascent":800,"descent":-200,"capHeight":500,"xHeight":300,"glyphs":"IADEDgDAIQBODE4MJOZODGj3AEBODIr9GAvF\/k4MAACEDcX+TgyK\/QBAAMAiALATmAgk5pgIxu4AQHASJOZwEsbuAEAAwCMA3Bk6ETjhmAiYCABAnBg44QQQmAgAQJgIPPHcGTzxAEBiB574nBie+ABAAMAkAJwYhA044YQN7AQAQHASOOFwEuwEAECcGNrpJhZk53ASJOaEDSTm2Alk52IH2uliB1DsmAjG7tgJ\/O9ODDzxsBOy8yYW6PRcFyj2nBie+JwYT\/wmFsX+cBIAAIQNAADYCcX+YgdP\/ABAAMAlAIgdiB0k5mIHAAAAQIQNJOYEEKToBBAQ68QOkO1ODMbu2AnG7mIHUOxiB9rpmAhk5xgLJOaEDSTmBBBk57ATpOhcF6ToEhtk54gdJOYAQJwYaPcmFp748BQU+\/AUiv1cFwAA3BkAAEgcxf6IHU\/8iB3U+RIbaPecGGj3AEAAwCYA\/h\/+Hzzx\/h\/878gexu6IHcbuSBz87xIbfPKcGJ74JhZP\/LATxf46EQAATgwAANgJxf6YCIr9YgcU+2IHnviYCCj22Ano9HAS\/O+wE8bu8BRQ7PAU2umwE2TnOhEk5sQOZOeEDdrphA1Q7MQO\/O86EbLzXBdP\/NwZxf5IHAAAyB4AAP4fxf7+H4r9AEAAwCcATgzYCaTomAhk59gJJOYYC2TnGAva6dgJUOyYCJDtAEAAwCgAOhE6ETjhxA64404MZOfYCVDsmAh88pgIaPfYCYr9Tgx2AsQOLAY6EZgIAEAAwCkAOhFiBzjh2Am4404MZOfEDlDsBBB88gQQaPfEDor9Tgx2AtgJLAZiB5gIAEAAwCoAsBOEDZDthA1P\/ABAYgc88bATnvgAQLATPPFiB574AEAAwCsA\/h+wE9rpsBMAAABAmAjo9Mge6PQAQADALADYCdgJFPuYCE\/8YgcU+5gI1PnYCRT72AmK\/WIHAAAAQADALQD+H5gI6PTIHuj0AEAAwC4A2AmYCNT5YgcU+5gIT\/zYCRT7mAjU+QBAAMAvABIbSBw44SwGmAgAQADAMACcGMQOJOYYC2TnmAgQ62IHPPFiB+j0mAgU+xgLxf7EDgAAOhEAAPAUxf5cFxT7nBjo9JwYPPFcFxDr8BRk5zoRJObEDiTmAEAAwDEAnBgYCxDrhA3a6ToRJOY6EQAAAEAAwDIAnBiYCFDsmAgQ69gJpOgYC2TnhA0k5nASJObwFGTnJhak6FwXEOtcF5DtJhb877ATsvNiBwAAnBgAAABAAMAzAJwY2Akk5lwXJOYEEPzvsBP87yYWPPFcF3zynBgo9pwYnvhcF0\/88BTF\/joRAACEDQAA2AnF\/pgIiv1iBxT7AEAAwDQAnBiwEyTmYgdo99wZaPcAQLATJOawEwAAAEAAwDUAnBgmFiTm2Akk5pgIPPHYCfzvhA3G7joRxu7wFPzvXBd88pwYKPacGJ74XBdP\/PAUxf46EQAAhA0AANgJxf6YCIr9YgcU+wBAAMA2AJwYXBfa6SYWZOdwEiTmBBAk5k4MZOfYCRDrmAg88ZgIaPfYCU\/8TgzF\/gQQAAA6EQAA8BTF\/lwXT\/ycGJ74nBho91wXsvPwFDzxOhH87wQQ\/O9ODDzx2Amy85gIaPcAQADANwCcGJwYJOZODAAAAEBiByTmnBgk5gBAAMA4AJwYhA0k5tgJZOeYCNrpmAhQ7NgJxu5ODPzvOhE88fAUfPJcF+j0nBho95wYFPtcF4r9JhbF\/nASAACEDQAA2AnF\/pgIiv1iBxT7Ygdo95gI6PQYC3zyxA488bAT\/O8mFsbuXBdQ7FwX2ukmFmTncBIk5oQNJOYAQADAOQCcGFwXxu4mFnzysBPo9AQQKPbEDij2GAvo9JgIfPJiB8buYgeQ7ZgI2ukYC2TnxA4k5gQQJOawE2TnJhba6VwXxu5cF+j0JhYU+7ATxf4EEAAAhA0AANgJxf6YCE\/8AEAAwDoA2AmYCDzxYgd88pgIsvPYCXzymAg88QBAmAjU+WIHFPuYCE\/82AkU+5gI1PkAQADAOwDYCZgIPPFiB3zymAiy89gJfPKYCDzxAEDYCRT7mAhP\/GIHFPuYCNT52AkU+9gJiv1iBwAAAEAAwDwAiB1IHNrpmAjo9EgcAAAAQADAPQD+H5gIPPHIHjzxAECYCJ74yB6e+ABAAMA+AIgdmAja6Ugc6PSYCAAAAEAAwD8AJhZiB1DsYgcQ65gIpOjYCWTnTgwk5joRJOawE2Tn8BSk6CYWEOsmFpDt8BT877ATPPHEDrLzxA5o9wBAxA6K\/YQNxf7EDgAABBDF\/sQOiv0AQADAQAA0IdwZ\/O+cGJDtJhZQ7HASUOwEEJDtxA7G7oQNfPKEDSj2xA6e+DoR1PnwFNT5XBee+JwYKPYAQHASUOwEEMbuxA588sQOKPYEEJ74OhHU+QBA3BlQ7JwYKPacGJ74EhvU+Ygd1Pn+H2j3NCGy8zQhPPH+H5DtyB4Q60gcpOjcGWTnJhYk5nASJObEDmTnTgyk6NgJEOuYCJDtYgc88WIH6PSYCJ742AkU+04Miv3EDsX+cBIAACYWAADcGcX+SByK\/YgdT\/wAQBIbUOzcGSj23Bme+BIb1PkAQADAQQAmFsQOJObsBAAAAEDEDiTmnBgAAABAmAho9\/AUaPcAQADAQgDcGZgIJOaYCAAAAECYCCTmsBMk5lwXZOecGKTo3BkQ69wZkO2cGPzvXBc88bATfPIAQJgIfPKwE3zyXBey85wY6PTcGWj33BkU+5wYiv1cF8X+sBMAAJgIAAAAQADAQwDcGdwZUOycGNrpJhZk57ATJObEDiTmTgxk59gJ2umYCFDsYgf872IHKPaYCNT52AlP\/E4Mxf7EDgAAsBMAACYWxf6cGE\/83BnU+QBAAMBEANwZmAgk5pgIAAAAQJgIJOY6ESTm8BRk51wX2umcGFDs3Bn879wZKPacGNT5XBdP\/PAUxf46EQAAmAgAAABAAMBFAFwXmAgk5pgIAAAAQJgIJOacGCTmAECYCHzycBJ88gBAmAgAAJwYAAAAQADARgAmFpgIJOaYCAAAAECYCCTmnBgk5gBAmAh88nASfPIAQADARwDcGdwZUOycGNrpJhZk57ATJObEDiTmTgxk59gJ2umYCFDsYgf872IHKPaYCNT52AlP\/E4Mxf7EDgAAsBMAACYWxf6cGE\/83BnU+dwZKPYAQLATKPbcGSj2AEAAwEgAEhuYCCTmmAgAAABA3Bkk5twZAAAAQJgIfPLcGXzyAEAAwEkA2AmYCCTmmAgAAABAAMBKALATcBIk5nAS1Pk6EYr9BBDF\/oQNAAAYCwAAmAjF\/mIHiv0sBtT5LAZo9wBAAMBLANwZmAgk5pgIAAAAQNwZJOaYCGj3AEDEDjzx3BkAAABAAMBMAPAUmAgk5pgIAAAAQJgIAABcFwAAAEAAwE0AiB2YCCTmmAgAAABAmAgk5nASAAAAQEgcJOZwEgAAAEBIHCTmSBwAAABAAMBOABIbmAgk5pgIAAAAQJgIJObcGQAAAEDcGSTm3BkAAABAAMBPABIbxA4k5k4MZOfYCdrpmAhQ7GIH\/O9iByj2mAjU+dgJT\/xODMX+xA4AALATAAAmFsX+nBhP\/NwZ1PkSGyj2Ehv879wZUOycGNrpJhZk57ATJObEDiTmAEAAwFAA3BmYCCTmmAgAAABAmAgk5rATJOZcF2TnnBik6NwZEOvcGcbunBg88VwXfPKwE7LzmAiy8wBAAMBRABIbxA4k5k4MZOfYCdrpmAhQ7GIH\/O9iByj2mAjU+dgJT\/xODMX+xA4AALATAAAmFsX+nBhP\/NwZ1PkSGyj2Ehv879wZUOycGNrpJhZk57ATJObEDiTmAEBwEhT73Bl2AgBAAMBSANwZmAgk5pgIAAAAQJgIJOawEyTmXBdk55wYpOjcGRDr3BmQ7ZwY\/O9cFzzxsBN88pgIfPIAQDoRfPLcGQAAAEAAwFMAnBicGNrpJhZk53ASJOaEDSTm2Alk52IH2uliB1DsmAjG7tgJ\/O9ODDzxsBOy8yYW6PRcFyj2nBie+JwYT\/wmFsX+cBIAAIQNAADYCcX+YgdP\/ABAAMBUALAThA0k5oQNAAAAQOwEJOYmFiTmAEAAwFUAEhuYCCTmmAie+NgJT\/xODMX+BBAAAHASAAAmFsX+nBhP\/NwZnvjcGSTmAEAAwFYAJhbsBCTmxA4AAABAnBgk5sQOAAAAQADAVwCIHSwGJOZODAAAAEBwEiTmTgwAAABAcBIk5pwYAAAAQMgeJOacGAAAAEAAwFgAnBhiByTmnBgAAABAnBgk5mIHAAAAQADAWQAmFuwEJObEDnzyxA4AAABAnBgk5sQOfPIAQADAWgCcGJwYJOZiBwAAAEBiByTmnBgk5gBAYgcAAJwYAAAAQADAWwA6EZgIOOGYCJgIAEDYCTjh2AmYCABAmAg44ToROOEAQJgImAg6EZgIAEAAwFwAOhGxAyTm8BSxAwBAAMBdADoRxA444cQOmAgAQAQQOOEEEJgIAEBiBzjhBBA44QBAYgeYCAQQmAgAQADAXgCwE4QNuOOxA+j0AECEDbjjXBfo9ABAAMBfACYWsQOYCNwZmAgAQADAYADYCdgJUOxiB8buYgc88ZgIfPLYCTzxmAj872IHPPEAQADAYQBcFyYWxu4mFgAAAEAmFnzysBP87zoRxu6EDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AADoRAACwE8X+JhZP\/ABAAMBiAFwXmAgk5pgIAAAAQJgIfPIYC\/zvhA3G7joRxu6wE\/zvJhZ88lwXKPZcF574JhZP\/LATxf46EQAAhA0AABgLxf6YCE\/8AEAAwGMAJhYmFnzysBP87zoRxu6EDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AADoRAACwE8X+JhZP\/ABAAMBkAFwXJhYk5iYWAAAAQCYWfPKwE\/zvOhHG7oQNxu4YC\/zvmAh88mIHKPZiB574mAhP\/BgLxf6EDQAAOhEAALATxf4mFk\/8AEAAwGUAJhZiByj2JhYo9iYWsvPwFDzxsBP87zoRxu6EDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AADoRAACwE8X+JhZP\/ABAAMBmAMQOBBAk5oQNJOYYC2Tn2AkQ69gJAAAAQCwGxu7EDsbuAEAAwGcAXBcmFsbuJhZ2AvAULAawE2IHOhGYCIQNmAgYC2IHAEAmFnzysBP87zoRxu6EDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AADoRAACwE8X+JhZP\/ABAAMBoAFwXmAgk5pgIAAAAQJgIsvNODPzvxA7G7nASxu7wFPzvJhay8yYWAAAAQADAaQDYCWIHJOaYCGTn2Akk5pgI7uRiByTmAECYCMbumAgAAABAAMBqAE4M2Akk5hgLZOdODCTmGAvu5NgJJOYAQBgLxu4YC7ED2AliB2IHmAjsBJgIAEAAwGsA8BSYCCTmmAgAAABA8BTG7pgIFPsAQIQNKPYmFgAAAEAAwGwA2AmYCCTmmAgAAABAAMBtAOokmAjG7pgIAAAAQJgIsvNODPzvxA7G7nASxu7wFPzvJhay8yYWAAAAQCYWsvPcGfzvSBzG7v4fxu50IvzvtCOy87QjAAAAQADAbgBcF5gIxu6YCAAAAECYCLLzTgz878QOxu5wEsbu8BT87yYWsvMmFgAAAEAAwG8AXBeEDcbuGAv875gIfPJiByj2Ygee+JgIT\/wYC8X+hA0AADoRAACwE8X+JhZP\/FwXnvhcFyj2JhZ88rAT\/O86EcbuhA3G7gBAAMBwAFwXmAjG7pgImAgAQJgIfPIYC\/zvhA3G7joRxu6wE\/zvJhZ88lwXKPZcF574JhZP\/LATxf46EQAAhA0AABgLxf6YCE\/8AEAAwHEAXBcmFsbuJhaYCABAJhZ88rAT\/O86EcbuhA3G7hgL\/O+YCHzyYgco9mIHnviYCE\/8GAvF\/oQNAAA6EQAAsBPF\/iYWT\/wAQADAcgAEEJgIxu6YCAAAAECYCCj22Al88k4M\/O\/EDsbucBLG7gBAAMBzAPAU8BR88rAT\/O8EEMbuTgzG7pgI\/O9iB3zymAjo9BgLKPY6EWj3sBOe+PAUFPvwFE\/8sBPF\/gQQAABODAAAmAjF\/mIHT\/wAQADAdADEDtgJJObYCRT7GAvF\/oQNAAAEEAAAAEAsBsbuxA7G7gBAAMB1AFwXmAjG7pgIFPvYCcX+TgwAAAQQAABwEsX+JhYU+wBAJhbG7iYWAAAAQADAdgCwEywGxu6EDQAAAEDwFMbuhA0AAABAAMB3ABIbYgfG7k4MAAAAQDoRxu5ODAAAAEA6EcbuJhYAAABAEhvG7iYWAAAAQADAeADwFGIHxu7wFAAAAEDwFMbuYgcAAABAAMB5ALATLAbG7oQNAAAAQPAUxu6EDQAAGAvsBJgIYgcsBpgI7ASYCABAAMB6APAU8BTG7mIHAAAAQGIHxu7wFMbuAEBiBwAA8BQAAABAAMB7ADoRxA444U4MeOIYC7jj2Akk5tgJpOgYCxDrTgxQ7IQNxu6EDTzxGAuy8wBATgx44hgL7uQYC2TnTgza6YQNEOvEDpDtxA7874QNfPKYCOj0hA1o98QO1PnEDk\/8hA3F\/k4MAAAYC3YCGAvsBE4MYgcAQBgLKPaEDZ74hA0U+04Miv0YC8X+2Ak7AdgJsQMYCywGTgxiB8QOmAgAQADAfADYCZgIOOGYCJgIAEAAwH0AOhHYCTjhTgx44oQNuOPEDiTmxA6k6IQNEOtODFDsGAvG7hgLPPGEDbLzAEBODHjihA3u5IQNZOdODNrpGAsQ69gJkO3YCfzvGAt88gQQ6PQYC2j32AnU+dgJT\/wYC8X+TgwAAIQNdgKEDewETgxiBwBAhA0o9hgLnvgYCxT7TgyK\/YQNxf7EDjsBxA6xA4QNLAZODGIH2AmYCABAAMB+AIgdYgee+GIHKPaYCHzyGAs88YQNPPEEEHzy8BQo9lwXaPfcGWj3SBwo9ogdsvMAQGIHKPaYCLLzGAt88oQNfPIEELLz8BRo91wXnvjcGZ74SBxo94gdsvOIHTzxAEAAwA=="})
}

turtle.jump(-85, -80);
texts.logo.print(turtle, 'Connect4', 1.5);
for(let i = 1; i < 8; i++) {
    turtle.jump(-105 + i*25, 90);
    texts.default.print(turtle, ''+i, 1.2);
}

const gameState = Connect4(legalMoves);

switch(gameState.status) {
    case gameState.GAMEON:
        turtle.jump(0, -90);
        texts.default.print(turtle, 'Add your moves by adding\ncolumn numbers to the\nmoves text box.', .7);
        break;
    case gameState.PLAYERWON:
        turtle.jump(18, -80);
        texts.default.print(turtle, 'You won!', 1.5);
        break;
    case gameState.COMPUTERWON:
        turtle.jump(-10, -80);
        texts.default.print(turtle, 'You lost! Try again.', 1);
        break;
    case gameState.DRAW:
        turtle.jump(30, -80);
        texts.default.print(turtle, 'It\'s a draw.', 1);
        break;
    case gameState.ERROR:
        turtle.jump(-10, -80);
        texts.default.print(turtle, 'No cheating!', 1.5);
        break;
}

const winningConnections = myownconnect4checker(gameState.board);
if(winningConnections.length > 0) {
    winningConnections.forEach((e, i, a) => {
        for(let r = .6; r < 2; r+=.5) {
            const ts = PT.circlesTangents([e[0][0] * 25 - 75, e[0][1] * 25 - 60], r, [e[1][0] * 25 - 75, e[1][1] * 25 - 60], r);
            (()=>{
                const p = polygons.create();
                p.addPoints(ts[0][0], ts[0][1], ts[1][1], ts[1][0]);
                p.dp.push(ts[0][0], ts[1][0]);
                p.dp.push(ts[1][1], ts[0][1]);
                polygons.draw(turtle, p);
            })()
            
            e.forEach(cr => {
                const p = polygons.create();
                p.addPoints(...PT.circle(r).map(pt => V.add(pt, [cr[0] * 25 - 75, cr[1] * 25 - 60])));
                p.addOutline();
                polygons.draw(turtle, p);
            });
        }
    })
}

gameState.board.forEach((column, col, columns) => column.forEach((cell, row, rows) => {
    const p = polygons.create();
    p.addPoints(...PT.circle(10).map(pt => V.add(pt, [col * 25 - 75, row * 25 - 60])));
    switch(cell) {
        case 1:
            p.addHatching(preLoadedHatches[col][row][0]);
            break;
        case 2:
            p.addHatching(preLoadedHatches[col][row][1]);
        default:
    }
    p.addOutline();
    polygons.draw(turtle, p);
}));

function myownconnect4checker(board) {
    //all [col, row]s
    return gameState.board.flatMap(
        (c, col) => c.map((r, row) => [col, row])
    ).filter(cr => gameState.board[cr[0]][cr[1]] == gameState.rot || gameState.board[cr[0]][cr[1]] == gameState.gelb).map(cr => 
        //4 cells in directions right and down
        Array.from({length: 2})
             .flatMap((e,c) => Array.from({length: 2})
                                    .map((e,r) => [c, r]))
             .filter(c => !(c[0] == 0 && c[1] == 0))
             .map(v => Array.from({length: 4})
                            .map((e,i,a) => V.scale(v, i)))
             //and map them relative to all [col, row]s
             .map(combo => combo.map(p => V.add(cr, p)))
             //filter out combos that are out of bound with the board
             .filter(combo => combo.every(cell =>
                0 <= cell[0] && cell[0] < 7 &&
                0 <= cell[1] && cell[1] < 6
             ))
             //filter out combos of which all cell don't have the same value
             .filter(combo => combo.every(cell => gameState.board[combo[0][0]][combo[0][1]] == gameState.board[cell[0]][cell[1]]))
    ).filter(c => c.length > 0)
    .flatMap(c => c)
    .map(combo => combo.filter((c, i) => i == 0 || i == 3));
}

// Port of connect4 implementation by Sebastian Macke
// https://simulationcorner.net/index.php?page=connectfour
function Connect4(moves) {
    const gameState = {
        GAMEON: 0,
        PLAYERWON: 1,
        COMPUTERWON: 2,
        DRAW: 3,
        ERROR: 9,
        
        status: 0,
        board: [],
        
        rot: 1,
        gelb: 2,
        ausserhalb: 3,
        leer: 0,
    }
    
    const feld = Array.from({length: 7}, () => Array.from({length: 6}, () => 0));
    
    const hoehe =  Array.from({length: 7}, () => 5);
    
    function get(spalte, zeile) {
        return (spalte < 0 || 6 < spalte || zeile < 0 || 5 < zeile)? gameState.ausserhalb: feld[spalte][zeile];
    }       
    
    function gesetzt(spalte) {
        if (hoehe[spalte] == -1) throw new Error('Column full');
        
        feld[spalte][hoehe[spalte]] = gameState.rot;
        hoehe[spalte] = hoehe[spalte] - 1;
        //put(spalte,gameState.rot);
        if (pruefe(spalte,hoehe[spalte]+1,4,gameState.rot,false)) {
            gameState.status = gameState.PLAYERWON;
        }
        if (hoehe.every(h => h === -1)) {
            gameState.status = gameState.DRAW;
        }
        if (gameState.status != gameState.PLAYERWON) computer();
    }
    
    /*
     *      function pruefe
     *      Beschreibung: sucht an Koordianten "x,y", ob "menge" Steine in der "farbe" waagrecht,diagonal oder senkrecht aufzufinden sind.
     *      und ob noch die restlichen Stellen f�r die Erschaffung eines Vierers ausreichen
     *      pruefe_bei_2: pr�ft ob 4. Stein gleich gesetzt werden kann(ansonsten k�nnte der Gegner den werdenden Vierer gleich zerst�ren)
     */
    
    function pruefe(x, y, menge, farbe, pruefe_bei_2) {
        let ja = false;
    
        /* Gegnerfarbe bestimmen */
        const farbe2 = (farbe === gameState.rot)? gameState.gelb: gameState.rot;

        for (let k = 0; k < 4; k++) {
            let summe1 = summe2 = summe3 = summe4 = summe12 = summe22 = summe32 = summe42 = 0;
    
            for(let j = 0; j < 4; j++){
                if(get(x-k+j,y)     == farbe)       summe1++;
                if(get(x,y-k+j)     == farbe)       summe2++;
                if(get(x-k+j,y-k+j) == farbe)       summe3++;
                if(get(x+k-j,y-k+j) == farbe)       summe4++;
                if(get(x-k+j,y)     == farbe2)      summe12++;
                if(get(x,y-k+j)     == farbe2)      summe22++;
                if(get(x-k+j,y-k+j) == farbe2)      summe32++;
                if(get(x+k-j,y-k+j) == farbe2)      summe42++;
                if(get(x-k+j,y)     == gameState.ausserhalb)  summe12++;
                if(get(x,y-k+j)     == gameState.ausserhalb)  summe22++;
                if(get(x-k+j,y-k+j) == gameState.ausserhalb)  summe32++;
                if(get(x+k-j,y-k+j) == gameState.ausserhalb)  summe42++;
            }
            if (
                ((summe1 >= menge) && (summe12 == 0)) ||
                ((summe2 >= menge) && (summe22 == 0)) ||
                ((summe3 >= menge) && (summe32 == 0)) ||
                ((summe4 >= menge) && (summe42 == 0))
            ) { ja = true; }
    
    
            if (ja && pruefe_bei_2) {
                summe12 = 0;
                summe22 = 0;
                summe32 = 0;
                summe42 = 0;
                feld[x][y] = farbe;
                hoehe[x]--;
    
                for(let j = 0; j< 4; j++) {
                    if (summe1 >= menge && get(x-k+j,y) == gameState.leer     && get(x-k+j,hoehe[x-k+j]+1) == gameState.leer) summe12++;
                    if (summe2 >= menge && get(x,y-k+j) == gameState.leer     && get(x,hoehe[x]+1) == gameState.leer)         summe22++;
                    if (summe3 >= menge && get(x-k+j,y-k+j) == gameState.leer && get(x-k+j,hoehe[x-k+j]+1) == gameState.leer) summe32++;
                    if (summe4 >= menge && get(x+k-j,y-k+j) == gameState.leer && get(x+k-j,hoehe[x+k-j]+1) == gameState.leer) summe42++;
                }
                if (summe12 == 1 || summe22 == 1 || summe32 == 1 || summe42 == 1) ja = false;
                hoehe[x]++;
                feld[x][y] = gameState.leer;
            }
        }
        return ja;
    }
    
    function computer() {
        const chance = [13, 13, 16, 16, 16, 13, 13].map(v => v + Math.random() * 4);
    
        for(let i = 0; i < 7; i++) {
            if (hoehe[i] < 0) chance[i] = chance[i]-30000;
        }
        
        for(let i = 0; i < 7; i++) {
            //Gewonnen
            if (pruefe(i,hoehe[i],3,gameState.gelb,false) == true) chance[i] = chance[i] + 20000;
    
            //anderer versucht zu gewinnen
            if (pruefe(i,hoehe[i],3,gameState.rot,false) == true) chance[i] = chance[i] + 10000;
    
            //�ber einem 3 rot
            if (pruefe(i,hoehe[i]-1,3,gameState.rot,false) == true) chance[i] = chance[i] -4000;
    
            //�ber einem 3 gelb
            if (pruefe(i,hoehe[i]-1,3,gameState.gelb,false) == true) chance[i] = chance[i] -200;
    
            //2 auf 3 verhindern
            if (pruefe(i,hoehe[i],2,gameState.rot,false) == true) chance[i] = chance[i] +50+Math.random()*3;
    
            //2 auf 3 erm�glichen, aber nicht wenn anderer die 3 ausschalten kann
            if (pruefe(i,hoehe[i],2,gameState.gelb,true) && hoehe[i] > 0) {
                feld[i][hoehe[i]] = gameState.gelb;
                hoehe[i]--;
                let zaehler = 0;
                for(let j = 0; j < 7; j++) if(pruefe(j,hoehe[j],3,gameState.gelb,false)) zaehler++;
                chance[i] = chance[i] + (zaehler == 0? 60 + Math.random() * 2: -60);
                hoehe[i]++;
                feld[i][hoehe[i]] = gameState.leer;
            }
            
            //nein wenn rot dr�ber
            if (pruefe(i,hoehe[i]-1,2,gameState.rot,false)) chance[i] -= 10;
    
            //nein wenn gameState.gelb dr�ber
            if (pruefe(i,hoehe[i]-1,2,gameState.gelb,false)) chance[i] -= 8;
    
            //1 auf 2 verhindern
            if (pruefe(i,hoehe[i],1,gameState.rot,false)) chance[i] += 5 + Math.random() * 2;
    
            //1 auf 2 erm�glichen
            if (pruefe(i,hoehe[i],1,gameState.gelb,false) == true) chance[i] += 5 + Math.random() * 2;
        
            //nein wenn rot dr�ber
            if (pruefe(i,hoehe[i]-1,1,gameState.rot,false) == true) chance[i] -= 2;
    
            //ja wenn gelb dr�ber
            if (pruefe(i,hoehe[i]-1,1,gameState.gelb,false) == true) chance[i]++;
    
            //m�glichkeit zum austricksen suchen
            if (pruefe(i,hoehe[i],2,gameState.gelb,true) && hoehe[i] > 0) {
                feld[i][hoehe[i]] = gameState.gelb;
                hoehe[i]--;
                for(let k=0; k < 7; k++) {       
                    if (pruefe(k,hoehe[k],3,gameState.gelb,false) && hoehe[k] > 0) {
                        feld[k][hoehe[k]] = gameState.rot;
                        hoehe[k]--;
                        for(let j = 0; j < 7; j++) {
                            if (pruefe(j,hoehe[j],3,gameState.gelb,false) == true) chance[i] += 2000;
                        }
                        hoehe[k]++;
                        feld[k][hoehe[k]] = gameState.leer;
                    }
                }
                hoehe[i]++;
                feld[i][hoehe[i]] = gameState.leer;
            }
    
            //pr�fen ob anderer austricksen kann
            if(pruefe(i,hoehe[i],2,gameState.rot,true) && hoehe[i] > 0) {
                feld[i][hoehe[i]] = gameState.rot;
                hoehe[i]--;
                for(let k = 0; k < 7; k++) {
                    if (pruefe(k,hoehe[k],3,gameState.rot,false) && hoehe[k] > 0) {
                        feld[k][hoehe[k]] = gameState.gelb;
                        hoehe[k]--;
                        for(let j = 0; j < 7; j++) {
                            if (pruefe(j,hoehe[j],3,gameState.rot,false)) chance[i] += 1000;
                        }
                        hoehe[k]++;
                        feld[k][hoehe[k]] = gameState.leer;
                    }
                }
                hoehe[i]++;
                feld[i][hoehe[i]] = gameState.leer;
            }       

            //pr�fen ob anderer austricksen kann wenn ich ins feld reingehe
            if(pruefe(i,hoehe[i]-1,2,gameState.rot,true) && hoehe[i] > 1) {
                feld[i][hoehe[i]] = gameState.rot;
                hoehe[i]--;
                for(let k = 0; k < 7; k++) {
                    if(pruefe(k,hoehe[k]-1,3,gameState.rot,false) && hoehe[k] > 0) {
                        feld[k][hoehe[k]] = gameState.gelb;
                        hoehe[k]--;
                        for(let j = 0; j < 7; j++) {
                            if (pruefe(j,hoehe[j]-1,3,gameState.rot,false)) chance[i] -= 500;
                        }
                        hoehe[k]++;
                        feld[k][hoehe[k]] = gameState.leer;
                    }
                }
                hoehe[i]++;
                feld[i][hoehe[i]] = gameState.leer;
            }
        }
    
        let spalte = 0;
        let x = -10000;
        for(let i = 0; i < 7; i++) {
            if (chance[i] > x) {
                x = chance[i];
                spalte = i;
            }
        }
        
        feld[spalte][hoehe[spalte]] = gameState.gelb;
        hoehe[spalte]--;
        //put(spalte,gameState.gelb);
        if (pruefe(spalte,hoehe[spalte]+1,4,gameState.gelb,false)) {
            gameState.status = gameState.COMPUTERWON;
        }
        if (hoehe.every(v => v == -1)) {
            gameState.status = gameState.DRAW;
        }
    }
    
    
    moves.split('').forEach(v => {
        if(hoehe[v-1] < 0) {
            gameState.status = gameState.ERROR;
        }
        if(gameState.status == gameState.ERROR) {
            return;
        }
        if(gameState.status == gameState.GAMEON) {
            gesetzt(v - 1);
        }
    });
    gameState.board = feld;
    return gameState;
}


function init() {
    ///////////////////////////////////////////////////////
    // Vector functions - Created by Jurgen Westerhof 2024
    // https://turtletoy.net/turtle/d068ad6040
    ///////////////////////////////////////////////////////
    class Vector {
        static add  (a,b) { return a.map((v,i)=>v+b[i]); }
        static sub  (a,b) { return a.map((v,i)=>v-b[i]); }
        static mul  (a,b) { return a.map((v,i)=>v*b[i]); }
        static div  (a,b) { return a.map((v,i)=>v/b[i]); }
        static scale(a,s) { return a.map(v=>v*s); }
    
        static det(m)                { return m.length == 1? m[0][0]: m.length == 2 ? m[0][0]*m[1][1]-m[0][1]*m[1][0]: m[0].reduce((r,e,i) => r+(-1)**(i+2)*e*this.det(m.slice(1).map(c => c.filter((_,j) => i != j))),0); }
        static angle(a)              { return Math.PI - Math.atan2(a[1], -a[0]); } //compatible with turtletoy heading
        static rot2d(angle)          { return [[Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]]; }
        static rot3d(yaw,pitch,roll) { return [[Math.cos(yaw)*Math.cos(pitch), Math.cos(yaw)*Math.sin(pitch)*Math.sin(roll)-Math.sin(yaw)*Math.cos(roll), Math.cos(yaw)*Math.sin(pitch)*Math.cos(roll)+Math.sin(yaw)*Math.sin(roll)],[Math.sin(yaw)*Math.cos(pitch), Math.sin(yaw)*Math.sin(pitch)*Math.sin(roll)+Math.cos(yaw)*Math.cos(roll), Math.sin(yaw)*Math.sin(pitch)*Math.cos(roll)-Math.cos(yaw)*Math.sin(roll)],[-Math.sin(pitch), Math.cos(pitch)*Math.sin(roll), Math.cos(pitch)*Math.cos(roll)]]; }
        static trans(matrix,a)       { return a.map((v,i) => a.reduce((acc, cur, ci) => acc + cur * matrix[ci][i], 0)); }
        //Mirror vector a in a ray through [0,0] with direction mirror
        static mirror2d(a,mirror)    { return [Math.atan2(...mirror)].map(angle => this.trans(this.rot2d(angle), this.mul([-1,1], this.trans(this.rot2d(-angle), a)))).pop(); }

        static approx(a,b,p) { return this.len(this.sub(a,b)) < (p === undefined? .001: p); }
        static norm  (a)     { return this.scale(a,1/this.len(a)); }
        static len   (a)     { return Math.hypot(...a); }
        static lenSq (a)     { return a.reduce((a,c)=>a+c**2,0); }
        static lerp  (a,b,t) { return a.map((v, i) => v*(1-t) + b[i]*t); }
        static dist  (a,b)   { return Math.hypot(...this.sub(a,b)); }
        
        static dot  (a,b)   { return a.reduce((a,c,i) => a+c*b[i], 0); }
        static cross(...ab) { return ab[0].map((e, i) => ab.map(v => v.filter((ee, ii) => ii != i))).map((m,i) => (i%2==0?-1:1)*this.det(m)); }
    }
    this.V = Vector;
    
    class Intersection2D {
        //a-start, a-direction, b-start, b-direction
        //returns false on no intersection or [[intersection:x,y], scalar a-direction, scalar b-direction
        static info(as, ad, bs, bd) { const d = V.sub(bs, as), det = -V.det([bd, ad]); if(det === 0) return false; const res = [V.det([d, bd]) / det, V.det([d, ad]) / det]; return [V.add(as, V.scale(ad, res[0])), ...res]; }
        static ray(a, b, c, d) { return this.info(a, b, c, d); }
        static segment(a,b,c,d, inclusiveStart = true, inclusiveEnd = true) { const i = this.info(a, V.sub(b, a), c, V.sub(d, c)); return i === false? false: ( (inclusiveStart? 0<=i[1] && 0<=i[2]: 0<i[1] && 0<i[2]) && (inclusiveEnd?   i[1]<=1 && i[2]<=1: i[1]<1 && i[2]<1) )?i[0]:false;}
        static tour(tour, segmentStart, segmentDirection) { return tour.map((e, i, a) => [i, this.info(e, V.sub(a[(i+1)%a.length], e), segmentStart, segmentDirection)]).filter(e => e[1] !== false && 0 <= e[1][1] && e[1][1] <= 1).filter(e => 0 <= e[1][2]).map(e => ({position: e[1][0],tourIndex: e[0],tourSegmentPortion: e[1][1],segmentPortion: e[1][2],}));}
        static inside(tour, pt) { return tour.map((e,i,a) => this.segment(e, a[(i+1)%a.length], pt, [Number.MAX_SAFE_INTEGER, 0], true, false)).filter(e => e !== false).length % 2 == 1; }
        static circles(centerA, radiusA, centerB, radiusB) {const result = {intersect_count: 0,intersect_occurs: true,one_is_in_other: false,are_equal: false,point_1: [null, null],point_2: [null, null],};const dx = centerB[0] - centerA[0];const dy = centerB[1] - centerA[1];const dist = Math.hypot(dy, dx);if (dist > radiusA + radiusB) {result.intersect_occurs = false;}if (dist < Math.abs(radiusA - radiusB) && !N.approx(dist, Math.abs(radiusA - radiusB))) {result.intersect_occurs = false;result.one_is_in_other = true;}if (V.approx(centerA, centerB) && radiusA === radiusB) {result.are_equal = true;}if (result.intersect_occurs) {const centroid = (radiusA**2 - radiusB**2 + dist * dist) / (2.0 * dist);const x2 = centerA[0] + (dx * centroid) / dist;const y2 = centerA[1] + (dy * centroid) / dist;const prec = 10000;const h = (Math.round(radiusA**2 * prec)/prec - Math.round(centroid**2 * prec)/prec)**.5;const rx = -dy * (h / dist);const ry = dx * (h / dist);result.point_1 = [x2 + rx, y2 + ry];result.point_2 = [x2 - rx, y2 - ry];if (result.are_equal) {result.intersect_count = null;} else if (result.point_1.x === result.point_2.x && result.point_1.y === result.point_2.y) {result.intersect_count = 1;} else {result.intersect_count = 2;}}return result;}
    }
    this.Intersection = Intersection2D;
    
    class PathTools {
        static bezier(p1, cp1, cp2, p2, steps = null) {steps = (steps === null? (V.len(V.sub(cp1, p1)) + V.len(V.sub(cp2, cp1)) + V.len(V.sub(p2, cp2))) | 0: steps) - 1;return Array.from({length: steps + 1}).map((v, i, a, f = i/steps) => [[V.lerp(p1, cp1, f),V.lerp(cp1, cp2, f),V.lerp(cp2, p2, f)]].map(v => V.lerp(V.lerp(v[0], v[1], f), V.lerp(v[1], v[2], f), f))[0]);}
        // https://stackoverflow.com/questions/18655135/divide-bezier-curve-into-two-equal-halves#18681336
        static splitBezier(p1, cp1, cp2, p2, t=.5) {const e = V.lerp(p1, cp1, t);const f = V.lerp(cp1, cp2, t);const g = V.lerp(cp2, p2, t);const h = V.lerp(e, f, t);const j = V.lerp(f, g, t);const k = V.lerp(h, j, t);return [[p1, e, h, k], [k, j, g, p2]];}
        static circular(radius,verticeCount,rotation=0) {return Array.from({length: verticeCount}).map((e,i,a,f=i*2*Math.PI/verticeCount+rotation) => [radius*Math.cos(f),radius*Math.sin(f)])}
        static circle(r){return this.circular(r,Math.max(12, r*2*Math.PI|0));}
        static arc(radius, extend = 2 * Math.PI, clockWiseStart = 0, steps = null, includeLast = false) { return [steps == null? (radius*extend+1)|0: steps].map(steps => Array.from({length: steps}).map((v, i, a) => [radius * Math.cos(clockWiseStart + extend*i/(a.length-(includeLast?1:0))), radius * Math.sin(clockWiseStart + extend*i/(a.length-(includeLast?1:0)))])).pop(); }
        static draw(turtle, path) {path.forEach((pt, i) => turtle[i==0?'jump':'goto'](pt));}
        static drawTour(turtle, path) {this.draw(turtle, path.concat([path[0]]));}
        static drawPoint(turtle, pt, r = .1) {this.drawTour(turtle, this.circle(r).map(e => V.add(e, pt)));}
        static drawArrow(turtle, s, d, width = 6, length = 3) {turtle.jump(s);const arrowHeadBase = V.add(s,d);turtle.goto(arrowHeadBase);turtle.goto(V.add(arrowHeadBase, V.trans(V.rot2d(-V.angle(d)), [-length, width/2])));turtle.jump(V.add(arrowHeadBase, V.trans(V.rot2d(-V.angle(d)), [-length, -width/2])));turtle.goto(arrowHeadBase);}

        static circlesTangents(c1_center, c1_radius, c2_center, c2_radius, internal = false) {
            let middle_circle = [V.scale(V.sub(c1_center, c2_center), .5)].map(hwp => [V.add(c2_center, hwp), V.len(hwp)]).pop();
            
            if(!internal && c1_radius == c2_radius) {
                let target = V.sub(c2_center, c1_center);
                let scaledTarget = V.scale(target, c1_radius/V.len(target));
                let partResult = [
                    V.add(c1_center, V.trans(V.rot2d(Math.PI/2), scaledTarget)),
                    V.add(c1_center, V.trans(V.rot2d(Math.PI/-2), scaledTarget))
                ];
                return [
                    partResult,
                    partResult.map(pt => V.add(pt, target))
                ]
            }
        
            let swap = !internal && c2_radius > c1_radius;
            
            if(swap) {
                let t = [[...c1_center], c1_radius];
                c1_center = c2_center;
                c1_radius = c2_radius;
                c2_center = t[0];
                c2_radius = t[1];
            }
            
            let internal_waypoints = intersectCircles2(c1_center, c1_radius + (internal?c2_radius:-c2_radius), ...middle_circle);
        
            if(internal_waypoints.length == 0) return [];
            
            const circlePointAtDirection2 = (circle_center, radius, direction) => V.add(circle_center, V.scale(direction, radius/V.len(direction)));
            
            const result = [
                [ // circle 1, point 1 and 2
                    circlePointAtDirection2(c1_center, c1_radius, V.sub(internal_waypoints[0], c1_center)),
                    circlePointAtDirection2(c1_center, c1_radius, V.sub(internal_waypoints[1], c1_center))
                ],
                [ // circle 2, point 1 and 2
                    circlePointAtDirection2(c2_center, c2_radius, internal? V.sub(c1_center, internal_waypoints[0]): V.sub(internal_waypoints[0], c1_center)),
                    circlePointAtDirection2(c2_center, c2_radius, internal? V.sub(c1_center, internal_waypoints[1]): V.sub(internal_waypoints[1], c1_center))
                ]
            ];
            
            return swap? [[result[1][1],result[1][0]],[result[0][1],result[0][0]]]: result;
        }
    }
    
    this.PT = PathTools;
    
    class Complex {
        static add(a,b)     { return V.add(a,b); }
        static sub(a,b)     { return V.sub(a,b); }
        static scale(a,s)   { return V.scale(a,s); }
        static mult(a,b)    { return [a[0]*b[0]-a[1]*b[1],a[0]*b[1]+a[1]*b[0]]; }
        static sqrt(a)      { return [[Math.hypot(...a)**.5, Math.atan2(...a.reverse()) / 2]].map(ra => [ra[0]*Math.cos(ra[1]), ra[0]*Math.sin(ra[1])]).pop(); }
    }
    this.C = Complex;
    
    class Numbers {
        static approx(a,b,p)        { return Math.abs(a-b) < (p === undefined? .001: p); }
        static clamp(a, min, max)   { return Math.min(Math.max(a, min), max); }
    }
    this.N = Numbers;
}

////////////////////////////////////////////////////////////////
// Polygon Clipping utility code - Created by Reinder Nijhoff 2019
// (Polygon binning by Lionel Lemarie 2021) https://turtletoy.net/turtle/95f33bd383
// (Delegated Hatching by Jurgen Westerhof 2024) https://turtletoy.net/turtle/d068ad6040
// (Deferred Polygon Drawing by Jurgen Westerhof 2024) https://turtletoy.net/turtle/6f3d2bc0b5
// https://turtletoy.net/turtle/a5befa1f8d
//
// const polygons = new Polygons();
// const p = polygons.create();
// polygons.draw(turtle, p);
// polygons.list();
// polygons.startDeferSession();
// polygons.stopDeferring();
// polygons.finalizeDeferSession(turtle);
//
// p.addPoints(...[[x,y],]);
// p.addSegments(...[[x,y],]);
// p.addOutline();
// p.addHatching(angle, distance); OR p.addHatching(HatchObject); where HatchObject has a method 'hatch(PolygonClass, thisPolygonInstance)'
// p.inside([x,y]);
// p.boolean(polygon, diff = true);
// p.segment_intersect([x,y], [x,y], [x,y], [x,y]);
////////////////////////////////////////////////////////////////
function Polygons(){const t=[],s=25,e=Array.from({length:s**2},t=>[]),n=class{constructor(){this.cp=[],this.dp=[],this.aabb=[]}addPoints(...t){let s=1e5,e=-1e5,n=1e5,h=-1e5;(this.cp=[...this.cp,...t]).forEach(t=>{s=Math.min(s,t[0]),e=Math.max(e,t[0]),n=Math.min(n,t[1]),h=Math.max(h,t[1])}),this.aabb=[s,n,e,h]}addSegments(...t){t.forEach(t=>this.dp.push(t))}addOutline(){for(let t=0,s=this.cp.length;t<s;t++)this.dp.push(this.cp[t],this.cp[(t+1)%s])}draw(t){for(let s=0,e=this.dp.length;s<e;s+=2)t.jump(this.dp[s]),t.goto(this.dp[s+1])}addHatching(t, s) {if(typeof t == 'object') return t.hatch(n, this);const e=new n;e.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);const h=Math.sin(t)*s,o=Math.cos(t)*s,a=200*Math.sin(t),i=200*Math.cos(t);for(let t=.5;t<150/s;t++) {e.dp.push([h*t+i,o*t-a],[h*t-i,o*t+a]);e.dp.push([-h*t+i,-o*t-a],[-h*t-i,-o*t+a]);}e.boolean(this,!1);this.dp=[...this.dp,...e.dp]}inside(t){let s=0;for(let e=0,n=this.cp.length;e<n;e++)this.segment_intersect(t,[.1,-1e3],this.cp[e],this.cp[(e+1)%n])&&s++;return 1&s}boolean(t,s=!0){const e=[];for(let n=0,h=this.dp.length;n<h;n+=2){const h=this.dp[n],o=this.dp[n+1],a=[];for(let s=0,e=t.cp.length;s<e;s++){const n=this.segment_intersect(h,o,t.cp[s],t.cp[(s+1)%e]);!1!==n&&a.push(n)}if(0===a.length)s===!t.inside(h)&&e.push(h,o);else{a.push(h,o);const n=o[0]-h[0],i=o[1]-h[1];a.sort((t,s)=>(t[0]-h[0])*n+(t[1]-h[1])*i-(s[0]-h[0])*n-(s[1]-h[1])*i);for(let n=0;n<a.length-1;n++)(a[n][0]-a[n+1][0])**2+(a[n][1]-a[n+1][1])**2>=.001&&s===!t.inside([(a[n][0]+a[n+1][0])/2,(a[n][1]+a[n+1][1])/2])&&e.push(a[n],a[n+1])}}return(this.dp=e).length>0}segment_intersect(t,s,e,n){const h=(n[1]-e[1])*(s[0]-t[0])-(n[0]-e[0])*(s[1]-t[1]);if(0===h)return!1;const o=((n[0]-e[0])*(t[1]-e[1])-(n[1]-e[1])*(t[0]-e[0]))/h,a=((s[0]-t[0])*(t[1]-e[1])-(s[1]-t[1])*(t[0]-e[0]))/h;return o>=0&&o<=1&&a>=0&&a<=1&&[t[0]+o*(s[0]-t[0]),t[1]+o*(s[1]-t[1])]}};const y=function(n,j=[]){const h={},o=200/s;for(var a=0;a<s;a++){const c=a*o-100,r=[0,c,200,c+o];if(!(n[3]<r[1]||n[1]>r[3]))for(var i=0;i<s;i++){const c=i*o-100;r[0]=c,r[2]=c+o,n[0]>r[2]||n[2]<r[0]||e[i+a*s].forEach(s=>{const e=t[s];n[3]<e.aabb[1]||n[1]>e.aabb[3]||n[0]>e.aabb[2]||n[2]<e.aabb[0]||j.includes(s)||(h[s]=1)})}}return Array.from(Object.keys(h),s=>t[s])};return{list:()=>t,create:()=>new n,draw:(n,h,o=!0)=>{rpl=y(h.aabb, this.dei === undefined? []: Array.from({length: t.length - this.dei}).map((e, i) => this.dsi + i));for(let t=0;t<rpl.length&&h.boolean(rpl[t]);t++);const td=n.isdown();if(this.dsi!==undefined&&this.dei===undefined)n.pu();h.draw(n),o&&function(n){t.push(n);const h=t.length-1,o=200/s;e.forEach((t,e)=>{const a=e%s*o-100,i=(e/s|0)*o-100,c=[a,i,a+o,i+o];c[3]<n.aabb[1]||c[1]>n.aabb[3]||c[0]>n.aabb[2]||c[2]<n.aabb[0]||t.push(h)})}(h);if(td)n.pd();},startDeferSession:()=>{if(this.dei!==undefined)throw new Error('Finalize deferring before starting new session');this.dsi=t.length;},stopDeferring:()=>{if(this.dsi === undefined)throw new Error('Start deferring before stopping');this.dei=t.length;},finalizeDeferSession:(n)=>{if(this.dei===undefined)throw new Error('Stop deferring before finalizing');for(let i=this.dsi;i<this.dei;i++) {rpl = y(t[i].aabb,Array.from({length:this.dei-this.dsi+1}).map((e,j)=>i+j));for(let j=0;j<rpl.length&&t[i].boolean(rpl[j]);j++);t[i].draw(n);}this.dsi=undefined;this.dei=undefined;}}}

///////////////////////////////////////////////////////////////////
// This is only the part of the Polygon Hatching utility code
// required in this turtle. For the full hatchery code see
// https://turtletoy.net/turtle/d068ad6040
//
///////////////////////////////////////////////////////////////////
// Polygon Hatching utility code - Created by Jurgen Westerhof 2024
// https://turtletoy.net/turtle/d068ad6040
// ////////////////////////////////////////////////////////////////
// // To be used with modified Polygon Clipping utility code
// //            Orginal: https://turtletoy.net/turtle/a5befa1f8d
// //    Polygon binning: https://turtletoy.net/turtle/95f33bd383
// // Delegated Hatching: https://turtletoy.net/turtle/d068ad6040
///////////////////////////////////////////////////////////////////
function loadHatcheryNamespace() {
    //for convenience on Turtletoy you can click the arrow to the right of the line number to collapse a class
    //////////////////////////////////////////////////// root
    class PolygonHatching {
        constructor() {
            if (this.constructor === PolygonHatching) {
                throw new Error("PolygonHatching is an abstract class and can't be instantiated.");
            }
            
            this.minX = -100;
            this.minY = -100;
            this.maxX = 100;
            this.maxY = 100;
            this.width = 200;
            this.height = 200;
            
            this.segments = [];
            
            this.init();
        }
        hatch(polygonsClass, thePolygonToHatch) {
            const e = new polygonsClass;
            e.cp.push(...thePolygonToHatch.aabb);//[-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);
            
            this.addHatchSegments(e.dp);
            
            e.boolean(thePolygonToHatch,!1);
            thePolygonToHatch.dp=[...thePolygonToHatch.dp,...e.dp]
        }
        addHatchSegments(segments) {
            this.getSegments().forEach(e => segments.push(e));
        }
        getSegments() { return this.segments; }
        init() {
            ///////////////////////////////////////////////////////
            // Vector functions - Created by Jurgen Westerhof 2024
            // https://turtletoy.net/turtle/d068ad6040
            ///////////////////////////////////////////////////////
            class Vector {
                static add  (a,b) { return a.map((v,i)=>v+b[i]); }
                static sub  (a,b) { return a.map((v,i)=>v-b[i]); }
                static mul  (a,b) { return a.map((v,i)=>v*b[i]); }
                static div  (a,b) { return a.map((v,i)=>v/b[i]); }
                static scale(a,s) { return a.map(v=>v*s); }
            
                static det(m)                { return m.length == 1? m[0][0]: m.length == 2 ? m[0][0]*m[1][1]-m[0][1]*m[1][0]: m[0].reduce((r,e,i) => r+(-1)**(i+2)*e*this.det(m.slice(1).map(c => c.filter((_,j) => i != j))),0); }
                static angle(a)              { return Math.PI - Math.atan2(a[1], -a[0]); } //compatible with turtletoy heading
                static rot2d(angle)          { return [[Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]]; }
                static rot3d(yaw,pitch,roll) { return [[Math.cos(yaw)*Math.cos(pitch), Math.cos(yaw)*Math.sin(pitch)*Math.sin(roll)-Math.sin(yaw)*Math.cos(roll), Math.cos(yaw)*Math.sin(pitch)*Math.cos(roll)+Math.sin(yaw)*Math.sin(roll)],[Math.sin(yaw)*Math.cos(pitch), Math.sin(yaw)*Math.sin(pitch)*Math.sin(roll)+Math.cos(yaw)*Math.cos(roll), Math.sin(yaw)*Math.sin(pitch)*Math.cos(roll)-Math.cos(yaw)*Math.sin(roll)],[-Math.sin(pitch), Math.cos(pitch)*Math.sin(roll), Math.cos(pitch)*Math.cos(roll)]]; }
                static trans(matrix,a)       { return a.map((v,i) => a.reduce((acc, cur, ci) => acc + cur * matrix[ci][i], 0)); }
                //Mirror vector a in a ray through [0,0] with direction mirror
                static mirror2d(a,mirror)    { return [Math.atan2(...mirror)].map(angle => this.trans(this.rot2d(angle), this.mul([-1,1], this.trans(this.rot2d(-angle), a)))).pop(); }
        
                static approx(a,b,p) { return this.len(this.sub(a,b)) < (p === undefined? .001: p); }
                static norm  (a)     { return this.scale(a,1/this.len(a)); }
                static len   (a)     { return Math.hypot(...a); }
                static lenSq (a)     { return a.reduce((a,c)=>a+c**2,0); }
                static lerp  (a,b,t) { return a.map((v, i) => v*(1-t) + b[i]*t); }
                static dist  (a,b)   { return Math.hypot(...this.sub(a,b)); }
                
                static dot  (a,b)   { return a.reduce((a,c,i) => a+c*b[i], 0); }
                static cross(...ab) { return ab[0].map((e, i) => ab.map(v => v.filter((ee, ii) => ii != i))).map((m,i) => (i%2==0?-1:1)*this.det(m)); }
            }
            this.V = Vector;
            
            class Intersection2D {
                //a-start, a-direction, b-start, b-direction
                //returns false on no intersection or [[intersection:x,y], scalar a-direction, scalar b-direction
                static info(as, ad, bs, bd) {
                    const d = V.sub(bs, as), det = -V.det([bd, ad]);
                    if(det === 0) return false;
                    const res = [V.det([d, bd]) / det, V.det([d, ad]) / det];
                    return [V.add(as, V.scale(ad, res[0])), ...res];
                }
                static ray(a, b, c, d) {
                    return this.info(a, b, c, d);
                }
                static segment(a,b,c,d, inclusiveStart = true, inclusiveEnd = true) {
                    const i = this.info(a, V.sub(b, a), c, V.sub(d, c));
                    return i === false? false: (
                        (inclusiveStart? 0<=i[1] && 0<=i[2]: 0<i[1] && 0<i[2])
                     && (inclusiveEnd?   i[1]<=1 && i[2]<=1: i[1]<1 && i[2]<1)
                    )?i[0]:false;
                }
            }
            this.Intersection = Intersection2D;
        }
    }
    this.PolygonHatching = PolygonHatching;
    class LineHatching extends PolygonHatching {
        constructor(angle, distance, inp = 0) {
            super();
            
            const h=Math.sin(angle)*distance,o=Math.cos(angle)*distance,a=200*Math.sin(angle),i=200*Math.cos(angle);
            this.segments = Array.from({length: 150/distance}).flatMap((x,y,z,t=.5+y) => [
                [h*t+i+inp*(Math.random()-.5),o*t-a+inp*(Math.random()-.5)],[h*t-i+inp*(Math.random()-.5),o*t+a+inp*(Math.random()-.5)], [-h*t+i+inp*(Math.random()-.5),-o*t-a+inp*(Math.random()-.5)],[-h*t-i+inp*(Math.random()-.5),-o*t+a+inp*(Math.random()-.5)]
            ])
        }
    }
    this.LineHatching = LineHatching;
}


////////////////////////////////////////////////////////////////
// Text utility code. Created by Reinder Nijhoff 2024
// https://turtletoy.net/turtle/0f84fd3ae4
// Modifications by Jurgen Westerhof 2024: https://turtletoy.net/turtle/9aef87b45e
// - Does not force turtle to operate in radians
// - Now respects turtle.isdown() and doesn't print if true
// - Text.size(turtle, str, scale = 1) returns properties of text str to be printed
// - Text.bbox(turtle, str, scale = 1) returns bounding box of text str to be printed
// - Text.dimensions(scale = 1) returns properties of fontData for scale
// - Text.bboxPrint(turtle, str, scale = 1) prints the text and returns the absolute bounding box
////////////////////////////////////////////////////////////////
function Text(fontData) {
    const decode = (data) => {
        const b = atob(data), a = new Int16Array(b.length / 2), g = {};
        for (let i = 0; i < b.length; i+=2) {
            a[i / 2 | 0] = b.charCodeAt(i) | (b.charCodeAt(i + 1) << 8);
        }
        for (let i = 0;i < a.length; i++) {
            let u = String.fromCharCode(a[i++]), x = a[i++], d = [], p = [];
            for (;a[i] !== -16384; i++) a[i] === 16384 ? (d.push(p), p = []) : p.push(a[i]);
            g[u] = { x, d };
        }
        return g;
    }
    const rotAdd = (a, b, h) => [Math.cos(h)*a[0] - Math.sin(h)*a[1] + b[0],
                                 Math.cos(h)*a[1] + Math.sin(h)*a[0] + b[1]];

    const data = Object.fromEntries(Object.entries(fontData).map(([key, value]) =>
        [key, key === 'glyphs' ? decode(value) : value]));

    class RangeFinderTurtle extends Turtle {
        constructor(x, y) {super(x, y);this.reset();}
        goto(x, y) {super.goto(x, y);const [a, b] = this.pos();this.rangeX = [Math.min(a, this.rangeX? this.rangeX[0]: a), Math.max(a, this.rangeX? this.rangeX[1]: a)];this.rangeY = [Math.min(b, this.rangeY? this.rangeY[0]: b), Math.max(b, this.rangeY? this.rangeY[1]: b)];}
        reset() {const [a, b] = this.pos();this.rangeX = [a, a];this.rangeY = [b, b];}
        bbox() { return [[this.rangeX[0], this.rangeY[0]], [this.rangeX[1], this.rangeY[1]]]; }
    }
    
    class Text {
        print (t, str, scale = 1) {
            const isDown = t && t.isdown();
            let pos = t ? [t.x(), t.y()] : [0,0], h = t ? t.h() * 2*Math.PI/t.fullCircle() : 0, o = pos, s = scale / data.unitsPerEm;
            str.split('').map(c => {
                if (c == '\n') {
                    pos = o = rotAdd([0, 10 * scale], o, h);
                    return;
                }
                const glyph = (data.glyphs[c] || data.glyphs[' ']), d = glyph.d;
                d.forEach( (p, k) => {
                    t && t.up();
                    for (let i=0; i<p.length; i+=2) {
                        t && t.goto(rotAdd([p[i]*s, p[i+1]*s], pos, h));
                        t && (!isDown || t.down());
                    }
                });
                pos = rotAdd([glyph.x*s, 0], pos, h);
            });
            if(isDown) t.down();
            return rotAdd([0, 10*scale], pos, h);
        }
        bboxPrint(t, str, scale = 1) {
            const rft = new RangeFinderTurtle(t.pos());
            rft.degrees(t.fullCircle());
            rft.seth(t.h());
            rft.up();
            this.print(rft, str, scale);
            this.print(t, str, scale);
            return rft.bbox();
        }
        size(t, str, scale = 1) {
            const rft = new RangeFinderTurtle();
            rft.up();
            this.print(rft, str, scale);
            return {
                width: rft.rangeX[1] - rft.rangeX[0],
                height: rft.rangeY[1] - rft.rangeY[0],
                bbox: t.h() === 0? rft.bbox(): this.bbox(t, str, scale)
            }
        }
        bbox(t, str, scale = 1) {
            const rft = new RangeFinderTurtle();
            rft.degrees(t.fullCircle());
            rft.seth(t.h());

            rft.up();
            this.print(rft, str, scale);
            return rft.bbox();
        }
        dimensions(scale = 1, standardCharacter = 'x') {
            const t = new Turtle();
            const x        = this.size(t, standardCharacter, scale);
            const allLower = this.size(t, 'abcdefghijklmnopqrstuvwxyz', scale);
            const allUpper = this.size(t, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', scale);
            // property names from: https://www.onlineprinters.co.uk/magazine/font-sizes/
            return {
                height: x.height,
                width: x.width,
                ascenders: Math.abs(allLower.bbox[0][1]) - x.height,
                descenders: Math.abs(allLower.bbox[1][1]),
                capitalHeight: Math.abs(allUpper.bbox[0][1]),
                capitalDescenders: Math.abs(allUpper.bbox[1][1])
            };
        }
    }

    return new Text();
}