// Battleship. Created by Jurgen Westerhof 2024

const seed = 'Change this text for a different board setup. Delete this text for a different puzzle every day.'; //type=string
const minePercentage = .15; //min=0 max=1 step=.01
const hitTokenSvg = 'M16.945,5.905L32.967,23.964L16.242,17.722L17.138,42.595L-1.557,18.329L-30.904,32.829L-11.421,10.287L-46.149,10.088L-21.418,-3.75L-39.028,-22.015L-11.939,-21.672L-3.463,-39.986L2.269,-19.485L29.405,-31.168L15.975,-7.986L44.307,1.992L16.945,5.905';
const markTokenSvg = 'M0,0L23,-12L0,-23L0,23H23V24H-23V23H0';
const mines = ''; //type=string
const clears = ''; //type=string

const markArr = mines.toUpperCase().match(/([A-K][1-9]|[1-9][A-K])/g)
const legalMarks = [(markArr? markArr.join(''): '').match(/(..)/g)].map(m => m == null? []: m).pop().map(lm => lm[0].match(/^[1-9]/)? lm[1] + lm[0]: lm).map(m => [m.charCodeAt(0) - 65, m[1]-1]);
const clearArr = clears.toUpperCase().match(/([A-K][1-9]|[1-9][A-K])/g)
const legalClears = [(clearArr? clearArr.join(''): '').match(/(..)/g)].map(m => m == null? []: m).pop().map(lm => lm[0].match(/^[1-9]/)? lm[1] + lm[0]: lm).map(m => [m.charCodeAt(0) - 65, m[1]-1]);

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

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

// 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 == ''? (new Date().toDateString()): seed);

const texts = {
    logo: new Text({"name":"EMSOsmotron","unitsPerEm":1000,"ascent":800,"descent":-200,"capHeight":500,"xHeight":300,"glyphs":"IADEDgDAIQDCBoYGZOeGBpj5AEDIBSP\/yAV+AEQHfgAmByP\/yAUj\/wBAAMAiAO4M7ASg5+wEzusAQJYKoOeWCs7rAEAAwCMAshskDqDn5gX8AABAkBqC5zQS3QAAQC4ERO5qHUTuAEAzA5j5KhyY+QBAAMAkAJQbTAQO\/EwEyf1KBsH\/eBnB\/5Qbiv2UGwr2WhnQ82gG0PNqBNzxagSY6mgGpOjcGaTolBt66pQbMuwAQEAQcuNAECgFAEAAwCUA8iHsBDTq7ASi78IGePGKDHjxiA6E74gOXOqoDF7o4AZe6OwENOoAQLwgOungBuT+AEB+GJ74fhgI\/nIa4f8cIOH\/GiLo\/Roinvg6IMj2chrI9n4YnvgAQADAJgBcIQwciPUMHMn9Nhqi\/yYHov9GBcn9RgVS9MAIVPIAQDAbjOwwG5jqHhmk6MAIpOikBrbqpAYe8fIhpv4AQADAJwBEBwoFgucKBc7rAEAAwCgAwAjeCKToRAek6CgFtuooBcn9JgfB\/94Iwf8AQADAKQBWCfIDwuiqBcLo5Afy6uQHqf3mBaL\/EASi\/wBAAMAqAF4QyAVw9BQKAu9SA6rsAEDYCQTo9gkC75oQquwAQPYJAu8kDqz0AEAAwCsA4g66CWv9ugn87wBAFAOq9nwQqvYAQADALACqBRAEI\/8QBOwEqgVSA6oFQ\/8QBCP\/AEAAwC0AvBHQA4L2dhGC9gBAAMAuAIYGEAQj\/xAEfgDmBX4A5gUj\/xAEI\/8AQADALwC8EdgBxf7sE57pAEAAwDAAxB0KBRDrCgWp\/UQHwf+4GsH\/Bh2K\/QYdmOoSG6TogAek6AoFEOsAQEYFiv3KHHrqAEAAwDEADA1qBKjueAqg51QLoOdUC90AAEAAwDIA6BwKBVDsCgXU6oAHXui4Gl7o6By26ugcNvK4GnD0Ygdw9AoFyPYKBaL\/4h2i\/wBAAMAzAOgczgRQ7M4ENOpKBsLoHhnC6E4b8upOGxjytBmy88AIsvO0GbLzDBwK9gwca\/3cGaL\/pAai\/+wEyf3sBM38AEAAwDQAeBnMFd0AzBUE6HYCgPgwG4D4AEAAwDUAph2qBS\/8qgWK\/bwHov9sG6L\/ph1r\/aYdCvaUG+7zjAXu84wFwujIHsLoAEAAwDYAKhw8GaToJgek6CgFmOooBYr9gAfh\/7ga4f\/oHKn96Bzs9bga0PNoBtDzCgWa8gBAAMA3AFwX2AHC6JITwujMFdTqzBX8AABAAMA4AOgcngei\/9Yaov\/oHIr96BzO9bgalPOkBpTz7ATc8ewEtuomB3zokBp86MocturKHNzx9BrQ86QG0PPOBIj1zgSp\/Z4Hov8AQADAOQDoHIwFwf82GsH\/jhyK\/Y4ctuo2Gl7ogAde6AoF8uoKBVTyRAeO9EgcjvQAQADAOgBoBvIDquzyA0Tu5gVE7uYFquzyA6rsAEDQAyP\/0AO9AKoFvQCqBSP\/0AMj\/wBAAMA7AGgGzgSq7M4EbO6GBmzuhgaq7M4EquwAQOwEYv\/sBM4EaAZSA2gGYv\/sBGL\/AEAAwDwAgg\/cDybuVwJG9iIQR\/4AQADAPQDMFbED9PImFvTyAEByA1b6RBZW+gBAAMA+AEAQsQOu7TQSZPbQA4b+AEAAwD8AXBczA6TogBak6NgY8urYGPTygBZM9RgLTPWYCML3mAh6+QBAvAcj\/7wHngA4CZ4AOAlD\/7wHI\/8AQADAQACmHeQWnvjkFnjxqhRI76YOSO9ODHjxTgwi90IOIPmIHSD5iB3y6jAbpOjkB6ToqgXU6qoFyf2eB8H\/ah3B\/wBAAMBBAMoc7AT8AOwE1OpEB3zo3Bl86Awc1OoMHOz1zgTs9Qwc7PUMHN0AAEAAwEIAiB0KBcLo9hjC6JQbVuuUG3jxGBqy86QGsvNGBVTyRgXo9KQGsvMYGrLzSBzO9Ugcqf0YGuH\/7ATh\/woFwugAQADAQwAMHEwdfOhEB3zo7ATU6uwEiv1EB+H\/ah3h\/wBAAMBEAIgd7ASk6OwEwf\/cGcH\/Khxr\/Soc1Or6GaTo7ASk6ABAAMBFANYaMBvB\/woFwf8KBdDzxhbQ8woF0PMKBaToEhuk6ABAAMBGAH4Y7ATdAOwE0PPkFtDz7ATQ8+wEfOgSG3zoAEAAwEcAiB3IFI70DByO9AwcDP1aGcH\/RAfB\/+wEa\/3sBPLqngdA6BgaQOgMHDTqDBwy7ABAAMBIAKYdCgWg5woF3QAKBe7zyhzu88ocoOfKHJ4AAEAAwEkASgbsBKDn7AT8AABAAMBKAGwbkBqC55AaKP7YGOH\/CgXh\/9UCiv3VAlD7AEAAwEsAbBvsBILn7AS9AOwE0PPcD9DzVBqeAL4P0PNyGoLnAEAAwEwAlBvIBb7nyAWi\/ygeov8AQADATQBYIAoFngAKBebnSgYE6HASRvaCHubnpB\/m56Qf3QAAQADATgCmHc4EngDOBCLohgYi6DAbfgAqHH4ADBzm5wBAAMBPAMoc7ATU6uwEqf0IB6L\/Nhqi\/yocqf0qHHrqNhrC6OAGpOjsBNTqAEAAwFAAlBvOBN0AzgTC6FQawugqHJjqKhw09DYaKPakBij2zgRS9ABAAMBRAAQf2iCi\/wgHov\/sBIr97ASY6ggHfOj6GXzoSBzU6kgcyf1UGqL\/AEAAwFIAyhzOBN0AzgSk6BgapOgqHLbqKhzQ8\/oZ7PWGBuz1sAQM9IYG7PU0Euz1MBvdAABAAMBTAOgc7AQv\/OwECP6GBqL\/GBqi\/+4byf3uG871GBru88IG7vPsBPrx7ASY6iYHXuhUGl7oKhw06iocMuwAQADAVAA2GmQP3QBkD6To1QKk6NAbpOgAQADAVQAGHewEvufsBKn94AbB\/zYawf8qHKn9KhyC5wBAAMBWANIjzgS+53QTvQB0IqDnAEAAwFcArimSBL7nyg2eAAIXguf4IPwANCpk5wBAAMBYACocjAWeANYaoOdAEDT0jAW+5\/Qa3QAAQADAWQBIHBAEvueCD871gg\/dAIIPzvX0Gr7nAEAAwFoAyhyxA6ToDByk6AwcmOpGBan9RgWi\/yocov8AQADAWwCYCN4Iwf8KBcH\/CgXC6N4IwugAQADAXAC8EZoBpOiwE6b+AEAAwF0AGgmxA6ToJgek6CYHgv+xA4L\/AEAAwF4ACBbAF6z0DA0i6HYCrPQAQADAXwBIHJIDtQKmHbUCAEAAwGAAaAYQBOzcKAVg4QBAAMBhAN4X8gNo7UoVaO1cF2bvXBfB\/8IGwf\/OBMn9zgTI9iAXyPYAQADAYgAgF84EjuXOBKL\/ShWi\/z4Xiv0+F4TvLBVo7WgGaO0KBcbuAEAAwGMAGhh+GMH\/CAfB\/+wEiv3sBGbvCAdK7X4YSu0AQADAZAA+F6gWrOWoFsH\/yAXB\/5IDa\/2SA8DvqgWu7aoUru2AFmbvAEAAwGUAGhhgGOH\/4Abh\/+wEyf3sBITvCAdK7UoVSu1cF2bvXBeC9s4EgvYAQADAZgBmDc4EvQDOBJDtHg+Q7c4EkO3OBHzo4AZq5kYPauYAQADAZwD8F0QHPggOFT4IPhcEBj4XSO8OFQ7taAYO7S4ESO8uBAj+BAbh\/\/AU4f8gF6n9AEAAwGgAhBfsBL0A7ASO5ewEaO1KFWjthBeE74QX3QAAQADAaQCqBewE3QDOBJ4AzgSq7ABALgRG5y4ErOXIBazlyAVG5y4ERucAQADAagDkBxT7XAhqBFwIhgYsBoYGbuwAQKoFgueqBazlYges5WIHgueqBYLnAEAAwGsAYhbsBKzl7AT8AOwEqvYMDar2JhZu7AwNqvZEFt0AAEAAwGwAGgmwBI7lsASK\/eAGwf\/SCsH\/AEAAwG0AkiLOBJ4AzgSQ7fgRkO3OE0jvzhP8AM4TSO+GFZDtniCQ7ZIihO+SIt0AAEAAwG4A3hfOBN0AzgTM7YYVzO1cF6LvXBf8AABAAMBvALoYqgXe76oFiv28B8H\/CBbB\/\/wXyf38FwLvRBYs7SAILO2qBd7vAEAAwHAA5BYKBX4JCgWu7cwVru2EF0jvhBdn\/uoVAAAIBwAARgVH\/gBAAMBxAFwXYhY4CWIWkO1uBZDtcgNm73IDqf2MBcH\/ChTB\/yYWyf0AQADAcgAcEewE\/ADsBGbvCAcs7XQTLO0AQADAcwAaGOwEzfzsBAj+pAbB\/2gVwf8+F8n9PheA+IYVyPYmB8j2KAXK9CgFSO9EB0rtLBVK7SAXSO8gF2DwAEAAwHQAhA3sBHDl7ASQ7R4PkO3sBJDt7AQI\/koGgv8eD4L\/AEAAwHUAnBiMBYzsjAXo\/UQHov\/qFaL\/\/Bdr\/fwXUOwAQADAdgCUG0wEjOyCD90AThtQ7ABAAMB3AAImsATS7KgM4f8sFarsZB6i\/+QlbuwAQADAeAD8F0YF3QBEFozsyg2q9kYFbuxiFr0AAEAAwHkAPhdEBz4IDhU+CD4XBAY+F27sPhdn\/swVwf8sBsH\/EASp\/RAEjOwAQADAegC6GBAEaO3AF2jtwBfk7kwEZ\/5MBKL\/ohei\/wBAAMB7AFYJnAmi\/54Hov\/mBej95gUE9\/QCDPTmBR7x5gW26uQHpOicCaToAEAAwHwASgbOBLTizgQKBQBAAMB9AFYJLgSi\/wQGov+8B+j9vAcE99IKDPTkBx7x5Ae26sgFpOguBKToAEAAwH4ASA21As71LAZM9XILpPdCDub2AEAAwA=="}),
    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=="})
}

const ms = new MineSweeper({minePercentage: minePercentage})

const cellSize = 13;
const textColumnStart = -69;
const textRowStart = -81;
const cellTextSize = .8;

const logoSize = texts.logo.size(turtle, 'MINESWEEPER', 2);
turtle.jump(-logoSize.width/2, -83);
texts.logo.print(turtle, 'MINESWEEPER', 2);
for(let i = 0; i < 11; i++) {
    turtle.jump(textColumnStart + i*cellSize, 66);
    texts.default.print(turtle, 'ABCDEFGHIJK'[i], cellTextSize);
}
for(let i = 0; i < 9; i++) {
    turtle.jump(textRowStart, i*cellSize - 51);
    texts.default.print(turtle, ''+(i+1), cellTextSize);
}

function MineSweeper(config = null) {
    class MineSweeper {
        GAMEON = 0;
        WON = 1;
        LOST = 2;
        status = 0;
        board = Array.from({length: 11}, () => Array.from({length: 9}, () => 0));
        config = {};
        
        OPEN = 1;
        MINE = 2;
        MARK = 4;

        constructor(userConfig) {
            const defaultConfig = {mineCount: (this.board.length * this.board[0].length * .3) | 0}
            this.config = defaultConfig;
            for(let k in userConfig) {
                if(k == 'minePercentage') {
                    this.config.mineCount = (this.board.length * this.board[0].length * userConfig.minePercentage) | 0
                    continue;
                }
                if(k == 'mineCount') {
                    continue;
                }
                this.config[k] = userConfig[k];
            }
            this.setup();
        }
        
        setup() {
            shuffle(Array.from({length: this.board.length * this.board[0].length}, (e,i) => i)).some((e, i) => {
                this.board[e % this.board.length][e / this.board.length | 0] = this.MINE;
                return i == this.config.mineCount;
            });
        }
        
        mark(colrow) {
            this.board[colrow[0]][colrow[1]] |= this.MARK;
        }
        
        clear(colrow) {
            if(this.status != this.GAMEON) return;
            
            this.board[colrow[0]][colrow[1]] |= this.OPEN;

            if((this.board[colrow[0]][colrow[1]] & this.MINE) == this.MINE) {
                this.status = this.LOST;
                return;
            }
            
            this.open(colrow);
            
            if(this.clearCount + this.markCount == this.fieldCount) this.status = this.WON;
        }
        
        getValidNeighbors(colrow) {
            return Array.from({length: 3}).flatMap((e,c) => Array.from({length: 3}, (e,r) => [c-1,r-1]))
                .filter(c => !(c[0] == 0 && c[1] == 0))
                .map(c => V.add(c, colrow))
                .filter(c => 0 <= c[0] && c[0] < this.board.length && 0 <= c[1] && c[1] < this.board[0].length);
        }
        
        getNeighborMineCount(colrow) {
            return this.getValidNeighbors(colrow).reduce((a, c) => a + ((this.board[c[0]][c[1]] & this.MINE) == this.MINE? 1: 0), 0);
        }
        
        open(colrow, recursion = []) {
            this.board[colrow[0]][colrow[1]] |= this.OPEN;
            
            recursion.push(colrow.join('-'));

            const currentTests = this.getValidNeighbors(colrow);
            
            if(this.getNeighborMineCount(colrow) == 0) {
                currentTests.forEach(e => {
                    if(recursion.includes(e.join('-'))) return;
                    if(this.getNeighborMineCount(e) == 0) {
                        this.board[e[0]][e[1]] = this.OPEN;
                        this.open(e, recursion);
                    }
                    this.board[e[0]][e[1]] |= this.OPEN;
                });
            }
        }
        
        get markCount() {
            return this.board.reduce((a, c) => a + c.reduce((a, c) => a + ((c & this.MARK) == this.MARK? 1: 0), 0), 0);
        }
        get mineCount() {
            return this.board.reduce((a, c) => a + c.reduce((a, c) => a + ((c & this.MINE) == this.MINE? 1: 0), 0), 0);
        }
        get progress() {
            return Math.floor((this.clearCount + this.markCount) / (this.fieldCount) * 100);
        }
        get clearCount() {
            return this.board.reduce((a, c) => a + c.reduce((a, c) => a + ((c & this.OPEN) == this.OPEN? 1: 0), 0), 0);
        }
        get fieldCount() {
            return this.board.length * this.board[0].length;
        }
    }
    
    return new MineSweeper(config);
}

const rawHitPath = new Path(hitTokenSvg);
const rawMarkPath = new Path(markTokenSvg);
const hitPath = Array.from({length: rawHitPath.length() + 1}).map((e,i,a) => rawHitPath.p(i/(a.length - 1))).map(pt => V.scale(pt, (cellSize*.9) / 100));
const markPath = Array.from({length: rawMarkPath.length() + 1}).map((e,i,a) => rawMarkPath.p(i/(a.length - 1))).map(pt => V.scale(pt, (cellSize*.9) / 100));

legalMarks.forEach((e) => ms.mark(e));
legalClears.forEach((e) => ms.clear(e));

ms.board.forEach((column, col, columns) => column.forEach((cell, row, rows) => {
    const cellPosition = [col*cellSize - 66, row*cellSize - 54];
    PT.drawTour(turtle, [[-.5,-.5],[.5,-.5],[.5,.5],[-.5,.5]].map(pt => V.add(V.scale(pt, cellSize), cellPosition)));
    
    if((cell & ms.MARK) == ms.MARK) {
        PT.draw(turtle, markPath.map(pt => V.add(cellPosition, pt)));
    }

    if(ms.status == ms.LOST && (cell & ms.MINE) == ms.MINE) {
        PT.draw(turtle, hitPath.map(pt => V.add(cellPosition, pt)));
        return;
    }
    
    if(cell == ms.OPEN) {
        turtle.jump(textRowStart - 1 + (col + 1) * cellSize, 5 + textColumnStart + (row + 1) * cellSize);
        texts.default.print(turtle, ''+(ms.getNeighborMineCount([col,row])), cellTextSize);
    }
}));

(()=>{
    let size, txt, y;
    let weight = .7;
    let dim7 = texts.default.dimensions(weight);
    const lineHeight = 1.6;
    
    txt = `You marked ${ms.markCount} fields as mines. Find ${ms.mineCount - ms.markCount} more.`;
    size = texts.default.size(turtle, txt, weight);
    y = 75;
    turtle.jump(-size.width/2, y);
    texts.default.print(turtle, txt, weight);
    y += dim7.capitalHeight * lineHeight;
    txt = `You cleared ${ms.clearCount} fields. Clear ${ms.fieldCount - ms.mineCount - ms.clearCount} more.`;
    size = texts.default.size(turtle, txt, weight);
    turtle.jump(-size.width/2, y);
    texts.default.print(turtle, txt, weight);
    y += dim7.capitalHeight * lineHeight;
    
    txt = `${ms.progress}% done. Clear or mark all fields.`;
    size = texts.default.size(turtle, txt, weight);
    turtle.jump(-size.width/2, y);
    texts.default.print(turtle, txt, weight);

    weight = 1.7;
    y = -66;
    let dim2 = texts.default.dimensions(weight);
    switch(ms.status) {
        case ms.GAMEON:
            weight = .7;
            y = -73;
            txt = 'Add your moves by adding column and row pairs';
            size = texts.default.size(turtle, txt, weight);
            turtle.jump(-size.width/2, y);
            y += dim7.capitalHeight * lineHeight;
            texts.default.print(turtle, txt, weight);
            txt = 'to the mines and clears text boxes. Example: a1b2c3';
            break;
        case ms.WON:
            txt = 'You won!';
            break;
        case ms.LOST:
            txt = 'You lost! Try again.';
            break;
    }
    size = texts.default.size(turtle, txt, weight);
    turtle.jump(-size.width/2, y);
    texts.default.print(turtle, txt, weight);
})();

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 = [[circlePointAtDirection2(c1_center, c1_radius, V.sub(internal_waypoints[0], c1_center)),circlePointAtDirection2(c1_center, c1_radius, V.sub(internal_waypoints[1], c1_center))],[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;
}

////////////////////////////////////////////////////////////////
// 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();
}

////////////////////////////////////////////////////////////////
// Path utility code. Created by Reinder Nijhoff 2023
// Parses a single SVG path (only M, C and L statements are
// supported). The p-method will return
// [...position, ...derivative] for a normalized point t.
//
// https://turtletoy.net/turtle/46adb0ad70
//
// Modifications by Jurgen Westerhof 2024 (support for InkScape absolute export)
// Added support for H and V statements
// Added support for multiple sets of parameter(s) for one statement
////////////////////////////////////////////////////////////////
function Path(svg) {
    class MoveTo {
        constructor(p) { this.p0 = p; }
        p(t, s) { return [...this.p0, 1, 0]; }
        length() { return 0; }
    }
    class LineTo {
        constructor(p0, p1) { this.p0 = p0, this.p1 = p1; }
        p(t, s = 1) {
            const nt = 1 - t, p0 = this.p0, p1 = this.p1;
            return [ 
                nt*p0[0] + t*p1[0],
                nt*p0[1] + t*p1[1],
                (p1[0] - p0[0]) * s,
                (p1[1] - p0[1]) * s,
            ];
        }
        length() { 
            const p0 = this.p0, p1 = this.p1;
            return Math.hypot(p0[0]-p1[0], p0[1]-p1[1]);
        }
    }
    class BezierTo {
        constructor(p0, c0, c1, p1) { this.p0 = p0, this.c0 = c0, this.c1 = c1, this.p1 = p1; }
        p(t, s = 1) {
            const nt = 1 - t, p0 = this.p0, c0 = this.c0, c1 = this.c1, p1 = this.p1;
            return [ 
                nt*nt*nt*p0[0] + 3*t*nt*nt*c0[0] + 3*t*t*nt*c1[0] + t*t*t*p1[0],
                nt*nt*nt*p0[1] + 3*t*nt*nt*c0[1] + 3*t*t*nt*c1[1] + t*t*t*p1[1],
                (3*nt*nt*(c0[0]-p0[0]) + 6*t*nt*(c1[0]-c0[0]) + 3*t*t*(p1[0]-c1[0])) * s,
                (3*nt*nt*(c0[1]-p0[1]) + 6*t*nt*(c1[1]-c0[1]) + 3*t*t*(p1[1]-c1[1])) * s,
            ];
        }
        length() {
            return this._length || (
                this._length = Array.from({length:25}, (x, i) => this.p(i/25)).reduce( 
                    (a,c,i,v) => i > 0 ? a + Math.hypot(c[0]-v[i-1][0], c[1]-v[i-1][1]) : a, 0));
        }
    }
    class Path {
        constructor(svg) {
            this.segments = [];
            this.parsePath(svg);
        }
        parsePath(svg) {
            const knownStatements = 'MLCHV';
            const t = svg.match(/([0-9.-]+|[MLCHV])/g); // Turtletoy does not support 'new Regex()'. If it did, MLCHV would not have to be repeated here
            let st = t[0];
            for (let s, i=0; i<t.length;) {
                const a = t[i++];
                if(knownStatements.indexOf(a) > -1) st = a; else i--;
                switch (st) {
                    case 'M': this.add(new MoveTo(s=[t[i++],t[i++]]));
                              break;
                    case 'L': this.add(new LineTo(s, s=[t[i++],t[i++]]));
                              break;
                    case 'C': this.add(new BezierTo(s, [t[i++],t[i++]], [t[i++],t[i++]], s=[t[i++],t[i++]]));
                              break;
                    case 'H': this.add(new LineTo(s, s=[t[i++],s[1]]));
                              break;
                    case 'V': this.add(new LineTo(s, s=[s[0],t[i++]]));
                              break;
                    default:  i++;
                }
            }
        }
        add(segment) {
            this.segments.push(segment);
            this._length = 0;
        }
        length() {
            return this._length || (this._length = this.segments.reduce((a,c) => a + c.length(), 0));
        }
        p(t) {
            t = Math.max(Math.min(t, 1), 0) * this.length();
            for (let l=0, i=0, sl=0; i<this.segments.length; i++, l+=sl) {
                sl = this.segments[i].length();
                if (t > l && t <= l + sl) {
                    return this.segments[i].p((t-l)/sl, sl/this.length());
                }
            }
            return this.segments[Math.min(1, this.segments.length-1)].p(0);
        }
    }
    return new Path(svg);
}

// Fisher-Yates (aka Knuth) Shuffle
// https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array#2450976
function shuffle(array) {
  let currentIndex = array.length,  randomIndex;

  // While there remain elements to shuffle.
  while (currentIndex > 0) {

    // Pick a remaining element.
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex--;

    // And swap it with the current element.
    [array[currentIndex], array[randomIndex]] = [
      array[randomIndex], array[currentIndex]];
  }

  return array;
}