Due to popular demand I bring you another #game: en.wikipedia.org/wiki/battleship_(game)
Add your targets to the shots variable by adding column and row pairs. For example: f2h4h8d2d7b3e6k7j3i4g4h5h3h2h1
Log in to post a comment.
// Battleship. Created by Jurgen Westerhof 2024 const seed = 'Change this text for a different board setup'; //type=string const ships = '5 4 3 3 2'; //type=string 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'; //type=path bbox=-50,-50,100,100 const misTokenSvg = 'M-23,0L23,0'; //type=path bbox=-50,-50,100,100 const shipsCanTouch = 1; //min=0 max=1 step=1 (Not even diagonally, Yes) const shellsPercentage = 75; //min=0 max=100 step=1 const shots = ''; //type=string const moveArr = shots.toUpperCase().match(/([A-K][1-9]|[1-9][A-K])/g) const legalMoves = [(moveArr? moveArr.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); 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 cellSize = 13; const logoSize = texts.logo.size(turtle, 'BATTLESHIP', 2); turtle.jump(-logoSize.width/2, -83); texts.logo.print(turtle, 'BATTLESHIP', 2); for(let i = 0; i < 11; i++) { turtle.jump(-69 + i*cellSize, 66); texts.default.print(turtle, 'ABCDEFGHIJK'[i], .8); } for(let i = 0; i < 9; i++) { turtle.jump(-81, i*cellSize - 51); texts.default.print(turtle, ''+(i+1), .8); } const bs = Battleship({ ships: [ships.match(/([1-9])/g)].map(s => s == null? []: s).pop(), shells: (99 * shellsPercentage / 100) | 0, shipsCanTouch: shipsCanTouch == 1 }); legalMoves.forEach(move => bs.move(move)); function Battleship(config = null) { class Battleship { GAMEON = 0; WON = 1; LOST = 2; status = 0; moves = 0; board = Array.from({length: 11}, () => Array.from({length: 9}, () => 0)); EMPTY = 0; SHIP = 1; MIS = 2; HIT = 3; shellStartStackSize = 0; fleet = null; constructor(userConfig) { const defaultConfig = {ships: [5, 4, 3, 3, 2], shells: this.board.length * this.board[0].length, shipsCanTouch: false} const config = defaultConfig; for(let k in userConfig) { config[k] = userConfig[k]; } this.setup(config); } setup(config) { this.fleet = config.ships.sort((a,b) => a<b?1:-1); this.shellStartStackSize = config.shells; const fleet = [...this.fleet]; while(fleet.length > 0) { const ship = fleet.pop(); const combos = (Math.random() < .5? [[1, 0], [0, 1]]: [[0, 1], [1, 0]]).map(pt => Array.from({length: ship}).map((l,i) => V.scale(pt, i))); const proximity = config.shipsCanTouch? [[0,0]]: Array.from({length: 3}).flatMap((e,c) => Array.from({length: 3}).map((e,r) => [c-1,r-1])); let candidates = []; while(candidates.length == 0) { const location = [Math.random() * (this.board.length - ship) | 0, Math.random() * (this.board[0].length - ship)| 0]; candidates = combos.map(rls => rls.map(rl => V.add(rl, location))) .filter(combo => combo.every(pt => proximity.every((e,i,a,check = V.add(pt, e)) => (check[0] < 0 || this.board.length <= check[0] || check[1] < 0 || this.board[0].length <= check[1] || this.board[check[0]][check[1]] == 0)))); } candidates[0].forEach(com => this.board[com[0]][com[1]] = 1); } } move(colrow) { if(this.shellStackSize == 0) { this.status = this.LOST; return; } this.board[colrow[0]][colrow[1]] |= 2; if(this.hitCount == this.shipCount) { this.status = this.WON; return; } if(this.shellStackSize == 0) { this.status = this.LOST; return; } } get hitCount() { return this.board.reduce((a, c) => a + c.reduce((a, c) => a + (c == 3? 1: 0), 0), 0); } get shipCount() { return this.board.reduce((a, c) => a + c.reduce((a, c) => a + ((c & 1) == 1? 1: 0), 0), 0); } get progress() { return this.hitCount / this.shipCount; } get shotCount() { return this.board.reduce((a, c) => a + c.reduce((a, c) => a + ((c & 2) == 2? 1: 0), 0), 0); } get shellStartStackSize() { return this.shellStartStackSize; } get shellStackSize() { return this.shellStartStackSize - this.shotCount; } } return new Battleship(config); } const rawHitPath = new Path(hitTokenSvg); const rawMisPath = new Path(misTokenSvg); 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 misPath = Array.from({length: rawMisPath.length() + 1}).map((e,i,a) => rawMisPath.p(i/(a.length - 1))).map(pt => V.scale(pt, (cellSize*.9) / 100)); bs.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))); switch(cell) { case 3: PT.draw(turtle, hitPath.map(pt => V.add(cellPosition, pt))); break; case 2: PT.draw(turtle, misPath.map(pt => V.add(cellPosition, pt))); default: } })); (()=>{ let size, txt, y; let weight = .7; let dim7 = texts.default.dimensions(weight); const lineHeight = 1.6; txt = `You've got ${bs.hitCount} hits. Get ${bs.shipCount} hits to sink the fleet.`; 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 fired ${bs.shotCount} shots and got ${bs.shellStackSize} shells left.`; size = texts.default.size(turtle, txt, weight); turtle.jump(-size.width/2, y); texts.default.print(turtle, txt, weight); y += dim7.capitalHeight * lineHeight; const fleetStr = bs.fleet.length == 0? 's so 0': (bs.fleet.length < 2? ' '+bs.fleet.join(''): 's '+bs.fleet.filter((e,i)=>i!=bs.fleet.length-1).join(', ') + ' and ' + bs.fleet[bs.fleet.length - 1]); txt = `The fleet has ${bs.fleet.length} ship${fleetStr} long.`; 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(bs.status) { case bs.GAMEON: weight = .7; y = -73; txt = 'Add your moves by adding column and row'; size = texts.default.size(turtle, txt, weight); turtle.jump(-size.width/2, y); y += dim7.capitalHeight * lineHeight; texts.default.print(turtle, txt, weight); txt = 'pairs to the shots text box. Example: a1b2c3'; break; case bs.WON: txt = 'You won!'; break; case bs.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); }