<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="description" content="__GAMETITLE__"> <title>__GAMETITLE__</title> <style>body{ background-color:black;font-family:"Courier New", Courier, monospace }#gameCanvas{ position:absolute;top:0px;left:0px;width:100%;height:100%;bottom:0px;right:0px;border:0px;background-color:black;-webkit-tap-highlight-color:rgba(0,0,0,0);image-rendering:-moz-crisp-edges;image-rendering:-webkit-crisp-edges;image-rendering:pixelated;image-rendering:crisp-edges;}h1{ color:lightblue;font-weight:normal;}a{ color:lightblue;}.title{ background-color:none;text-align:center;font-size:100%;float:center;color:gray;position:absolute;left:10%;right:10%;top:0%;height:10%;}.footer{ background-color:none;text-align:center;float:center;color:white;position:absolute;margin-top:10px;left:10%;right:10%;top:90%;bottom:10%;}.gameContainer{ background-color:none;position:absolute;left:10%;right:10%;top:70px;bottom:70px;touch-action:none;}.mobile-menu{ position:relative;top:4em;margin-left:auto;margin-right:auto;font-weight:bold;border-radius:0.25em;}.mobile-menu.item-count-3{ width:30em;}.mobile-menu.item-count-3 .button{ width:28.3333%; padding:7.5% 0%;}.mobile-menu.item-count-2{ width:20em;}.mobile-menu.item-count-2 .button{ width:46%; padding:12.1765% 0%;}.mobile-menu.item-count-1{ width:10em;}.mobile-menu.item-count-1 .button{ width:98%; padding:26.5% 0%;}.mobile-menu, .tab-icon, .mobile-menu .close{ background:rgba(0,0,0,0.4);border:2px solid rgba(255, 255, 255, 0.4);color:rgba(255, 255, 255, 1);}.mobile-menu .button{ margin:2%;border-radius:0.25em;text-align:center;float:left;}.mobile-menu .clear{ clear:both;}.tab-affordance, .close-affordance{ width:6em;height:6em;position:absolute;z-index:1000;}.tab-affordance{ left:-2em;top:55px;}.close-affordance{ left:-4em;top:-1em;}.tab-icon, .mobile-menu .close{ height:48px;position:absolute;border-radius:6px;}.tab-icon{ left:-0.5em;top:70px;width:18px;border-radius:0 6px 6px 0;border-left:0;}.mobile-menu .close{ left:-18px;width:18px;top:0px;border-radius:6px 0 0 6px;border-right:0;}.tab-icon .slice, .mobile-menu .close .slice{ margin:4.5px 1px;width:2px;height:80%;background:rgba(255, 255, 255, 0.4);}.tab-icon .slice{ float:right;}.tab-icon .slice:first-child{ margin-right:4.5px;}.mobile-menu .close .slice{ float:left;}.mobile-menu .close .slice:first-child{ margin-left:4.5px;}@media screen and (max-width:32em){ .mobile-menu{ font-size:0.8em;width:90%;}}@media screen and (max-width:24em){ .mobile-menu{ font-size:0.65em;width:90%;}}.disable-select{ -webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}</style> <style> .gameContainer{ left:0;right:0;top:0;bottom:0;}</style> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> </head> <body> <div class="gameContainer"> <canvas id="gameCanvas"></canvas> </div> <script>function storage_has(e){return null!==localStorage.getItem(e)}function storage_get(e){return localStorage.getItem(e)}function storage_set(e,t){return localStorage.setItem(e,t)}function storage_remove(e){localStorage.removeItem(e)}</script> <script>function doSetupTitleScreenLevelContinue(){try{if(storage_has(document.URL)){if(storage_has(document.URL+"_checkpoint")){var e=storage_get(document.URL+"_checkpoint");curlevelTarget=JSON.parse(e);var t=[];for(var n in Object.keys(curlevelTarget.dat))t[n]=curlevelTarget.dat[n];curlevelTarget.dat=new Int32Array(t)}curlevel=storage_get(document.URL),void 0!==localStorage[document.URL+"_sections"]&&(solvedSections=JSON.parse(localStorage.getItem(document.URL+"_sections")))}}catch(e){}}var unitTesting=!1,curlevel=0,solvedSections=[],curlevelTarget=null,hasUsedCheckpoint=!1,levelEditorOpened=!1,muted=0,runrulesonlevelstart_phase=!1,ignoreNotJustPressedAction=!0;doSetupTitleScreenLevelContinue();var verbose_logging=!1,throttle_movement=!1,cache_console_messages=!1,quittingTitleScreen=!1,quittingMessageScreen=!1,deltatime=17,timer=0,repeatinterval=150,autotick=0,autotickinterval=0,winning=!1,againing=!1,againinterval=150,norepeat_action=!1,oldflickscreendat=[],keybuffer=[],restarting=!1,messageselected=!1,textImages={},initLevel={width:5,height:5,layerCount:2,dat:[1,3,3,1,1,2,2,3,3,1,2,1,2,2,3,3,1,1,2,2,3,2,1,3,2,1,3,2,1,3,1,3,3,1,1,2,2,3,3,1,2,1,2,2,3,3,1,1,2,2],movementMask:[1,3,3,1,1,2,2,3,3,1,2,1,2,2,3,3,1,1,2,2,3,2,1,3,2,1,3,2,1,3,1,3,3,1,1,2,2,3,3,1,2,1,2,2,3,3,1,1,2,2],rigidGroupIndexMask:[],rigidMovementAppliedMask:[],bannedGroup:[],colCellContents:[],rowCellContents:[],colCellContents_Movements:[],rowCellContents_Movements:[]},level=initLevel;</script> <script>function stripTags(n){var e=document.createElement("div");return e.innerHTML=n,e.textContent||e.innerText||""}function consolePrint(n,e){}function consolePrintFromRule(n,e,r){}function consoleCacheDump(n){}function consoleError(n,e){var r=document.getElementById("errormessage");n=stripTags(n),r.innerHTML+=n+"<br>"}function logErrorNoLine(n){var e=document.getElementById("errormessage");n=stripTags(n),e.innerHTML+=n+"<br>"}function logBetaMessage(n){var e=document.getElementById("errormessage");n=stripTags(n),e.innerHTML+=n+"<br>"}function clearInputHistory(){}function pushInput(n){}solving=!1;var canSetHTMLColors=!0,canDump=!1,canOpenEditor=!1,canYoutube=!0,IDE=!1;const diffToVisualize=null;</script> <script>var font = { 
    '0':` 
00000 
00000 
00000 
01110 
10001 
10011 
10101 
11001 
10001 
01110 
00000 
00000`, 
 '1':` 
00000 
00000 
00000 
11100 
00100 
00100 
00100 
00100 
00100 
11111 
00000 
00000`, 
 '2':` 
00000 
00000 
00000 
11110 
00001 
00001 
01110 
10000 
10000 
11111 
00000 
00000`, 
 '3':` 
00000 
00000 
00000 
11110 
00001 
00110 
00001 
00001 
00001 
11110 
00000 
00000`, 
 '4':` 
00000 
00000 
00000 
10000 
10000 
10000 
10010 
11111 
00010 
00010 
00000 
00000`, 
 '5':` 
00000 
00000 
00000 
11111 
10000 
11110 
00001 
00001 
00001 
11110 
00000 
00000`, 
 '6':` 
00000 
00000 
00000 
01110 
10000 
11110 
10001 
10001 
10001 
01110 
00000 
00000`, 
 '7':` 
00000 
00000 
00000 
11111 
00001 
00010 
00100 
00100 
00100 
00100 
00000 
00000`, 
 '8':` 
00000 
00000 
00000 
01110 
10001 
01110 
10001 
10001 
10001 
01110 
00000 
00000`, 
 '9':` 
00000 
00000 
00000 
01110 
10001 
10001 
10001 
01111 
00001 
01110 
00000 
00000`, 
 'a':` 
00000 
00000 
00000 
00000 
00000 
01111 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'b':` 
00000 
00000 
00000 
10000 
10000 
11110 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'c':` 
00000 
00000 
00000 
00000 
00000 
01111 
10000 
10000 
10000 
01111 
00000 
00000`, 
 'd':` 
00000 
00000 
00000 
00001 
00001 
01111 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'e':` 
00000 
00000 
00000 
00000 
00000 
01110 
10001 
11111 
10000 
01110 
00000 
00000`, 
 'f':` 
00000 
00000 
00000 
00011 
00100 
11111 
00100 
00100 
00100 
00100 
00000 
00000`, 
 'g':` 
00000 
00000 
00000 
00000 
00000 
01111 
10001 
10001 
10001 
01111 
00001 
01110`, 
 'h':` 
00000 
00000 
00000 
10000 
10000 
11110 
10001 
10001 
10001 
10001 
00000 
00000`, 
 'i':` 
00000 
00000 
00000 
00100 
00000 
01100 
00100 
00100 
00100 
01110 
00000 
00000`, 
 'j':` 
00000 
00000 
00000 
00100 
00000 
01100 
00100 
00100 
00100 
00100 
10100 
01000`, 
 'k':` 
00000 
00000 
00000 
10000 
10000 
10001 
10010 
11100 
10010 
10001 
00000 
00000`, 
 'l':` 
00000 
00000 
00000 
01100 
00100 
00100 
00100 
00100 
00100 
01110 
00000 
00000`, 
 'm':` 
00000 
00000 
00000 
00000 
00000 
01010 
10101 
10101 
10101 
10101 
00000 
00000`, 
 'n':` 
00000 
00000 
00000 
00000 
00000 
01110 
10001 
10001 
10001 
10001 
00000 
00000`, 
 'o':` 
00000 
00000 
00000 
00000 
00000 
01110 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'p':` 
00000 
00000 
00000 
00000 
00000 
11110 
10001 
10001 
10001 
11110 
10000 
10000`, 
 'q':` 
00000 
00000 
00000 
00000 
00000 
01111 
10001 
10001 
10001 
01111 
00001 
00001`, 
 'r':` 
00000 
00000 
00000 
00000 
00000 
01111 
10000 
10000 
10000 
10000 
00000 
00000`, 
 's':` 
00000 
00000 
00000 
00000 
00000 
01111 
10000 
01110 
00001 
11110 
00000 
00000`, 
 't':` 
00000 
00000 
00000 
00100 
00100 
11111 
00100 
00100 
00100 
00011 
00000 
00000`, 
 'u':` 
00000 
00000 
00000 
00000 
00000 
10001 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'v':` 
00000 
00000 
00000 
00000 
00000 
10001 
10010 
10100 
11000 
10000 
00000 
00000`, 
 'w':` 
00000 
00000 
00000 
00000 
00000 
10101 
10101 
10101 
10101 
01010 
00000 
00000`, 
 'x':` 
00000 
00000 
00000 
00000 
00000 
10001 
01010 
00100 
01010 
10001 
00000 
00000`, 
 '×':` 
00000 
00000 
00000 
00000 
00000 
10001 
01010 
00100 
01010 
10001 
00000 
00000`, 
 'y':` 
00000 
00000 
00000 
00000 
00000 
10001 
10001 
10001 
10001 
01111 
00001 
11110`, 
 'z':` 
00000 
00000 
00000 
00000 
00000 
11111 
00010 
00100 
01000 
11111 
00000 
00000`, 
 'A':` 
00000 
00000 
00000 
01110 
10001 
10001 
10001 
11111 
10001 
10001 
00000 
00000`, 
 'B':` 
00000 
00000 
00000 
11110 
10001 
11110 
10001 
10001 
10001 
11110 
00000 
00000`, 
 'C':` 
00000 
00000 
00000 
01111 
10000 
10000 
10000 
10000 
10000 
01111 
00000 
00000`, 
 'D':` 
00000 
00000 
00000 
11110 
10001 
10001 
10001 
10001 
10001 
11110 
00000 
00000`, 
 'E':` 
00000 
00000 
00000 
11111 
10000 
11111 
10000 
10000 
10000 
11111 
00000 
00000`, 
 'F':` 
00000 
00000 
00000 
11111 
10000 
11111 
10000 
10000 
10000 
10000 
00000 
00000`, 
 'G':` 
00000 
00000 
00000 
01111 
10000 
10000 
10000 
10011 
10001 
01111 
00000 
00000`, 
 'H':` 
00000 
00000 
00000 
10001 
10001 
11111 
10001 
10001 
10001 
10001 
00000 
00000`, 
 'I':` 
00000 
00000 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
11111 
00000 
00000`, 
 'J':` 
00000 
00000 
00000 
01111 
00001 
00001 
00001 
00001 
00001 
01110 
00000 
00000`, 
 'K':` 
00000 
00000 
00000 
10001 
10010 
10100 
11000 
10100 
10010 
10001 
00000 
00000`, 
 'L':` 
00000 
00000 
00000 
10000 
10000 
10000 
10000 
10000 
10000 
11111 
00000 
00000`, 
 'M':` 
00000 
00000 
00000 
11111 
10101 
10101 
10101 
10101 
10101 
10101 
00000 
00000`, 
 'N':` 
00000 
00000 
00000 
10001 
11001 
10101 
10011 
10001 
10001 
10001 
00000 
00000`, 
 'O':` 
00000 
00000 
00000 
01110 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'P':` 
00000 
00000 
00000 
11110 
10001 
10001 
10001 
11110 
10000 
10000 
00000 
00000`, 
 'Q':` 
00000 
00000 
00000 
01110 
10001 
10001 
10001 
10001 
10101 
01110 
00100 
00000`, 
 'R':` 
00000 
00000 
00000 
11110 
10001 
10001 
11110 
10001 
10001 
10001 
00000 
00000`, 
 'S':` 
00000 
00000 
00000 
01111 
10000 
01110 
00001 
00001 
00001 
11110 
00000 
00000`, 
 'T':` 
00000 
00000 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
00100 
00000 
00000`, 
 'U':` 
00000 
00000 
00000 
10001 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'V':` 
00000 
00000 
00000 
10001 
10001 
10001 
10001 
10001 
01010 
00100 
00000 
00000`, 
 'W':` 
00000 
00000 
00000 
10101 
10101 
10101 
10101 
10101 
10101 
01010 
00000 
00000`, 
 'X':` 
00000 
00000 
00000 
10001 
10001 
01010 
00100 
01010 
10001 
10001 
00000 
00000`, 
 'Y':` 
00000 
00000 
00000 
10001 
10001 
01010 
00100 
00100 
00100 
00100 
00000 
00000`, 
 'Z':` 
00000 
00000 
00000 
11111 
00001 
00010 
00100 
01000 
10000 
11111 
00000 
00000`, 
 '.':` 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00100 
00000 
00000`, 
 '·':` 
00000 
00000 
00000 
00000 
00000 
00000 
00100 
00000 
00000 
00000 
00000 
00000`, 
 '•':` 
00000 
00000 
00000 
00000 
00000 
01110 
01110 
01110 
00000 
00000 
00000 
00000`, 
 '…':` 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
10101 
00000 
00000`, 
 '†':` 
00000 
00100 
00100 
01110 
00100 
00100 
00100 
00100 
00100 
00100 
00000 
00000`, 
 '‡':` 
00000 
00100 
00100 
01110 
00100 
00100 
00100 
00100 
01110 
00100 
00000 
00000`, 
 'ƒ':` 
00000 
00000 
00000 
00011 
00100 
11111 
00100 
00100 
00100 
00100 
01000 
00000`, 
 '‚':` 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00100 
01100 
00000 
00000`, 
 '„':` 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
01001 
11011 
00000 
00000`, 
 ',':` 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00100 
01100 
00000 
00000`, 
 ';':` 
00000 
00000 
00000 
00000 
00000 
00100 
00000 
00000 
00100 
01100 
00000 
00000`, 
 ':':` 
00000 
00000 
00000 
00000 
00000 
00100 
00000 
00000 
00000 
00100 
00000 
00000`, 
 '?':` 
00000 
00000 
00000 
01110 
10001 
00001 
00001 
00110 
00000 
00100 
00000 
00000`, 
 '¿':` 
00000 
00000 
00000 
00100 
00000 
01100 
10000 
10000 
10001 
01110 
00000 
00000`, 
 '!':` 
00000 
00000 
00000 
00100 
00100 
00100 
00100 
00100 
00000 
00100 
00000 
00000`, 
 '¡':` 
00000 
00000 
00000 
00100 
00000 
00100 
00100 
00100 
00100 
00100 
00000 
00000`, 
 '@':` 
00000 
00000 
00000 
00000 
00000 
01110 
10001 
10111 
10000 
01110 
00000 
00000`, 
 '£':` 
00000 
00000 
00000 
00000 
00000 
01110 
01001 
11100 
01000 
11111 
00000 
00000`, 
 '$':` 
00000 
00000 
00000 
00000 
00100 
01111 
10100 
01110 
00101 
11110 
00100 
00000`, 
 '%':` 
00000 
00000 
00000 
00000 
00000 
11001 
11010 
00100 
01011 
10011 
00000 
00000`, 
 '‰':` 
00000 
00000 
00000 
00000 
11001 
11010 
00100 
01011 
10011 
00000 
00011 
00011`, 
 '^':` 
00000 
00000 
00000 
00100 
01010 
00000 
00000 
00000 
00000 
00000 
00000 
00000`, 
 '&':` 
00000 
00000 
00000 
00000 
00000 
01100 
10000 
01011 
10010 
01100 
00000 
00000`, 
 '*':` 
00000 
00000 
00000 
00000 
00000 
01010 
00100 
01010 
00000 
00000 
00000 
00000`, 
 '(':` 
00000 
00000 
00000 
00010 
00100 
00100 
00100 
00100 
00100 
00010 
00000 
00000`, 
 ')':` 
00000 
00000 
00000 
01000 
00100 
00100 
00100 
00100 
00100 
01000 
00000 
00000`, 
 '+':` 
00000 
00000 
00000 
00000 
00000 
00100 
00100 
11111 
00100 
00100 
00000 
00000`, 
 '÷':` 
00000 
00000 
00000 
00000 
00000 
00100 
00000 
11111 
00000 
00100 
00000 
00000`, 
 '±':` 
00000 
00000 
00000 
00000 
00000 
00100 
00100 
11111 
00100 
11111 
00000 
00000`, 
 '-':` 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
01110 
00000 
00000 
00000 
00000`, 
 '–':` 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
11110 
00000 
00000 
00000 
00000`, 
 '—':` 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
11111 
00000 
00000 
00000 
00000`, 
 '_':` 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
11111 
00000 
00000`, 
 '=':` 
00000 
00000 
00000 
00000 
00000 
00000 
11111 
00000 
11111 
00000 
00000 
00000`, 
 ' ':` 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000`, 
 '{':` 
00000 
00000 
00000 
00110 
00100 
00100 
01100 
00100 
00100 
00110 
00000 
00000`, 
 '}':` 
00000 
00000 
00000 
01100 
00100 
00100 
00110 
00100 
00100 
01100 
00000 
00000`, 
 '[':` 
00000 
00000 
00000 
00110 
00100 
00100 
00100 
00100 
00100 
00110 
00000 
00000`, 
 ']':` 
00000 
00000 
00000 
01100 
00100 
00100 
00100 
00100 
00100 
01100 
00000 
00000`, 
 '\'':` 
00000 
00000 
00000 
00100 
00100 
00100 
00000 
00000 
00000 
00000 
00000 
00000`, 
 '‘':` 
00000 
00000 
00000 
00110 
00100 
00000 
00000 
00000 
00000 
00000 
00000 
00000`, 
 '’':` 
00000 
00000 
00000 
00100 
01100 
00000 
00000 
00000 
00000 
00000 
00000 
00000`, 
 '“':` 
00000 
00000 
00000 
11011 
10010 
00000 
00000 
00000 
00000 
00000 
00000 
00000`, 
 '”':` 
00000 
00000 
00000 
01001 
11011 
00000 
00000 
00000 
00000 
00000 
00000 
00000`, 
 '"':` 
00000 
00000 
00000 
01010 
01010 
01010 
00000 
00000 
00000 
00000 
00000 
00000`, 
 '/':` 
00000 
00000 
00000 
00000 
00000 
00001 
00010 
00100 
01000 
10000 
00000 
00000`, 
 '\\':` 
00000 
00000 
00000 
00000 
00000 
10000 
01000 
00100 
00010 
00001 
00000 
00000`, 
 '|':` 
00000 
00000 
00000 
00000 
00000 
00100 
00100 
00100 
00100 
00100 
00000 
00000`, 
 '¦':` 
00000 
00000 
00000 
00000 
00100 
00100 
00000 
00100 
00100 
00100 
00000 
00000`, 
 '<':` 
00000 
00000 
00000 
00000 
00000 
00010 
00100 
01000 
00100 
00010 
00000 
00000`, 
 '‹':` 
00000 
00000 
00000 
00000 
00000 
00000 
00100 
01000 
00100 
00000 
00000 
00000`, 
 '«':` 
00000 
00000 
00000 
00000 
00000 
00000 
01001 
10010 
01001 
00000 
00000 
00000`, 
 '>':` 
00000 
00000 
00000 
00000 
00000 
01000 
00100 
00010 
00100 
01000 
00000 
00000`, 
 '›':` 
00000 
00000 
00000 
00000 
00000 
00000 
00100 
00010 
00100 
00000 
00000 
00000`, 
 '»':` 
00000 
00000 
00000 
00000 
00000 
00000 
10010 
01001 
10010 
00000 
00000 
00000`, 
 '~':` 
00000 
00000 
00000 
00000 
00000 
00000 
01000 
10101 
00010 
00000 
00000 
00000`, 
 '˜':` 
00000 
00000 
00000 
00000 
00000 
01010 
10100 
00000 
00000 
00000 
00000 
00000`, 
 '`':` 
00000 
00000 
00000 
00000 
00000 
01000 
00100 
00000 
00000 
00000 
00000 
00000`, 
 '#':` 
00000 
00000 
00000 
00000 
00000 
01010 
11111 
01010 
11111 
01010 
00000 
00000`, 
 'À':` 
01000 
00100 
00000 
01110 
10001 
10001 
10001 
11111 
10001 
10001 
00000 
00000`, 
 'Á':` 
00010 
00100 
00000 
01110 
10001 
10001 
10001 
11111 
10001 
10001 
00000 
00000`, 
 'Â':` 
00100 
01010 
00000 
01110 
10001 
10001 
10001 
11111 
10001 
10001 
00000 
00000`, 
 'Ã':` 
01000 
10101 
00010 
01110 
10001 
10001 
10001 
11111 
10001 
10001 
00000 
00000`, 
 'Ä':` 
00000 
01010 
00000 
01110 
10001 
10001 
10001 
11111 
10001 
10001 
00000 
00000`, 
 'Å':` 
00100 
01010 
00100 
01110 
10001 
10001 
10001 
11111 
10001 
10001 
00000 
00000`, 
 'Æ':` 
00000 
00000 
00000 
01111 
10100 
10100 
10100 
11111 
10100 
10111 
00000 
00000`, 
 'Ç':` 
00000 
00000 
00000 
01111 
10000 
10000 
10000 
10000 
10000 
01111 
00100 
01000`, 
 'È':` 
01000 
00100 
00000 
11111 
10000 
11111 
10000 
10000 
10000 
11111 
00000 
00000`, 
 'É':` 
00010 
00100 
00000 
11111 
10000 
11111 
10000 
10000 
10000 
11111 
00000 
00000`, 
 'Ê':` 
00100 
01010 
00000 
11111 
10000 
11111 
10000 
10000 
10000 
11111 
00000 
00000`, 
 'Ë':` 
00000 
01010 
00000 
11111 
10000 
11111 
10000 
10000 
10000 
11111 
00000 
00000`, 
 'Ì':` 
01000 
00100 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
11111 
00000 
00000`, 
 'Í':` 
00010 
00100 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
11111 
00000 
00000`, 
 'Î':` 
00100 
01010 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
11111 
00000 
00000`, 
 'Ï':` 
00000 
01010 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
11111 
00000 
00000`, 
 'Ð':` 
00000 
00000 
00000 
01110 
01001 
01001 
11101 
01001 
01001 
01110 
00000 
00000`, 
 'Ñ':` 
01001 
10110 
00000 
10001 
11001 
10101 
10011 
10001 
10001 
10001 
00000 
00000`, 
 'Ò':` 
01000 
00100 
00000 
01110 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'Ó':` 
00010 
00100 
00000 
01110 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'Ô':` 
00100 
01010 
00000 
01110 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'Õ':` 
01001 
10110 
00000 
01110 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'Ö':` 
00000 
01010 
00000 
01110 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'Ø':` 
00000 
00010 
00100 
01110 
10101 
10101 
10101 
10101 
10101 
01110 
00100 
01000`, 
 'Ù':` 
00000 
01000 
00100 
10001 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'Ú':` 
00000 
00010 
00100 
10001 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'Û':` 
00100 
01010 
00000 
10001 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'Ü':` 
00000 
01010 
00000 
10001 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'Ý':` 
00000 
00000 
00100 
10001 
10001 
01010 
00100 
00100 
00100 
00100 
00000 
00000`, 
 'Þ':` 
00000 
00000 
10000 
11110 
10001 
10001 
10001 
10001 
10001 
11110 
10000 
00000`, 
 'ß':` 
00000 
00000 
00000 
01110 
10001 
10110 
10001 
10001 
10001 
10110 
10000 
00000`, 
 'ẞ':` 
00000 
00000 
00000 
01110 
10001 
10110 
10001 
10001 
10001 
10110 
00000 
00000`, 
 'à':` 
00000 
00000 
01000 
00100 
00000 
01111 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'á':` 
00000 
00000 
00010 
00100 
00000 
01111 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'â':` 
00000 
00000 
00100 
01010 
00000 
01111 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'ã':` 
00000 
00000 
01001 
10110 
00000 
01111 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'ä':` 
00000 
00000 
00000 
01010 
00000 
01111 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'å':` 
00000 
00100 
01010 
00100 
00000 
01111 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'æ':` 
00000 
00000 
00000 
00000 
00000 
01110 
10101 
10110 
10100 
01111 
00000 
00000`, 
 'ç':` 
00000 
00000 
00000 
00000 
00000 
01111 
10000 
10000 
10000 
01111 
00100 
01000`, 
 'è':` 
00000 
00000 
01000 
00100 
00000 
01110 
10001 
11111 
10000 
01110 
00000 
00000`, 
 'é':` 
00000 
00000 
00010 
00100 
00000 
01110 
10001 
11111 
10000 
01110 
00000 
00000`, 
 'ê':` 
00000 
00000 
00100 
01010 
00000 
01110 
10001 
11111 
10000 
01110 
00000 
00000`, 
 'ë':` 
00000 
00000 
00000 
01010 
00000 
01110 
10001 
11111 
10000 
01110 
00000 
00000`, 
 'ì':` 
00000 
00000 
01000 
00100 
00000 
01100 
00100 
00100 
00100 
01110 
00000 
00000`, 
 'í':` 
00000 
00000 
00010 
00100 
00000 
01100 
00100 
00100 
00100 
01110 
00000 
00000`, 
 'î':` 
00000 
00000 
00100 
01010 
00000 
01100 
00100 
00100 
00100 
01110 
00000 
00000`, 
 'ï':` 
00000 
00000 
00000 
01010 
00000 
01100 
00100 
00100 
00100 
01110 
00000 
00000`, 
 'ð':` 
00000 
00000 
00010 
00111 
00010 
01110 
10010 
10010 
10010 
01110 
00000 
00000`, 
 'ñ':` 
00000 
00000 
01001 
10110 
00000 
01110 
10001 
10001 
10001 
10001 
00000 
00000`, 
 'ò':` 
00000 
00000 
01000 
00100 
00000 
01110 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'ó':` 
00000 
00000 
00010 
00100 
00000 
01110 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'ô':` 
00000 
00000 
00100 
01010 
00000 
01110 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'õ':` 
00000 
00000 
01001 
10110 
00000 
01110 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'ö':` 
00000 
00000 
00000 
01010 
00000 
01110 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'ø':` 
00000 
00000 
00000 
00010 
00100 
01110 
10101 
10101 
10101 
01110 
00100 
01000`, 
 'ù':` 
00000 
00000 
00000 
01000 
00100 
10001 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'ú':` 
00000 
00000 
00000 
00010 
00100 
10001 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'û':` 
00000 
00000 
00100 
01010 
00000 
10001 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'ü':` 
00000 
00000 
00000 
01010 
00000 
10001 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'ý':` 
00000 
00000 
00000 
00010 
00100 
10001 
10001 
10001 
10001 
01111 
00001 
11110`, 
 'þ':` 
00000 
00000 
00000 
10000 
10000 
11110 
10001 
10001 
10001 
11110 
10000 
10000`, 
 'ÿ':` 
00000 
00000 
00000 
01010 
00000 
10001 
10001 
10001 
10001 
01111 
00001 
11110`, 
 'Ā':` 
00000 
01110 
00000 
01110 
10001 
10001 
10001 
11111 
10001 
10001 
00000 
00000`, 
 'ā':` 
00000 
00000 
00000 
01110 
00000 
01111 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'Ă':` 
01010 
00100 
00000 
01110 
10001 
10001 
10001 
11111 
10001 
10001 
00000 
00000`, 
 'ă':` 
00000 
00000 
01010 
00100 
00000 
01111 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'Ą':` 
00000 
00000 
00000 
01110 
10001 
10001 
10001 
11111 
10001 
10001 
00010 
00001`, 
 'ą':` 
00000 
00000 
00000 
00000 
00000 
01111 
10001 
10001 
10001 
01111 
00010 
00001`, 
 'Ć':` 
00010 
00100 
00000 
01111 
10000 
10000 
10000 
10000 
10000 
01111 
00000 
00000`, 
 'ć':` 
00000 
00000 
00010 
00100 
00000 
01111 
10000 
10000 
10000 
01111 
00000 
00000`, 
 'Ĉ':` 
00100 
01010 
00000 
01111 
10000 
10000 
10000 
10000 
10000 
01111 
00000 
00000`, 
 'ĉ':` 
00000 
00000 
00100 
01010 
00000 
01111 
10000 
10000 
10000 
01111 
00000 
00000`, 
 'Ċ':` 
00000 
00100 
00000 
01111 
10000 
10000 
10000 
10000 
10000 
01111 
00000 
00000`, 
 'ċ':` 
00000 
00000 
00000 
00100 
00000 
01111 
10000 
10000 
10000 
01111 
00000 
00000`, 
 'Č':` 
01010 
00100 
00000 
01111 
10000 
10000 
10000 
10000 
10000 
01111 
00000 
00000`, 
 'č':` 
00000 
00000 
01010 
00100 
00000 
01111 
10000 
10000 
10000 
01111 
00000 
00000`, 
 'Ď':` 
01010 
00100 
00000 
11110 
10001 
10001 
10001 
10001 
10001 
11110 
00000 
00000`, 
 'ď':` 
00000 
00000 
00000 
00101 
00101 
01100 
10100 
10100 
10100 
01100 
00000 
00000`, 
 'Đ':` 
00000 
00000 
00000 
01110 
01001 
01001 
11101 
01001 
01001 
01110 
00000 
00000`, 
 'đ':` 
00000 
00000 
00010 
00111 
00010 
01110 
10010 
10010 
10010 
01110 
00000 
00000`, 
 'Ē':` 
00000 
01110 
00000 
11111 
10000 
11111 
10000 
10000 
10000 
11111 
00000 
00000`, 
 'ē':` 
00000 
00000 
00000 
01110 
00000 
01110 
10001 
11111 
10000 
01110 
00000 
00000`, 
 'Ĕ':` 
01010 
00100 
00000 
11111 
10000 
11111 
10000 
10000 
10000 
11111 
00000 
00000`, 
 'ĕ':` 
00000 
00000 
01010 
00100 
00000 
01110 
10001 
11111 
10000 
01110 
00000 
00000`, 
 'Ė':` 
00000 
00100 
00000 
11111 
10000 
11111 
10000 
10000 
10000 
11111 
00000 
00000`, 
 'ė':` 
00000 
00000 
00000 
00100 
00000 
01110 
10001 
11111 
10000 
01110 
00000 
00000`, 
 'Ę':` 
00000 
00000 
00000 
11111 
10000 
11111 
10000 
10000 
10000 
11111 
00010 
00001`, 
 'ę':` 
00000 
00000 
00000 
00000 
00000 
01110 
10001 
11111 
10000 
01110 
00010 
00001`, 
 'Ě':` 
01010 
00100 
00000 
11111 
10000 
11111 
10000 
10000 
10000 
11110 
00000 
00000`, 
 'ě':` 
00000 
00000 
01010 
00100 
00000 
01110 
10001 
11111 
10000 
01110 
00000 
00000`, 
 'Ĝ':` 
00100 
01010 
00000 
01111 
10000 
10000 
10000 
10011 
10001 
01111 
00000 
00000`, 
 'ĝ':` 
00000 
00000 
00100 
01010 
00000 
01111 
10001 
10001 
10001 
01111 
00001 
01110`, 
 'Ğ':` 
01010 
00100 
00000 
01111 
10000 
10000 
10000 
10011 
10001 
01111 
00000 
00000`, 
 'ğ':` 
00000 
00000 
01010 
00100 
00000 
01111 
10001 
10001 
10001 
01111 
00001 
01110`, 
 'Ġ':` 
00000 
00100 
00000 
01111 
10000 
10000 
10000 
10011 
10001 
01111 
00000 
00000`, 
 'ġ':` 
00000 
00000 
00000 
00100 
00000 
01111 
10001 
10001 
10001 
01111 
00001 
01110`, 
 'Ģ':` 
00000 
00000 
00000 
01111 
10000 
10000 
10000 
10011 
10001 
01111 
00000 
01100`, 
 'ģ':` 
00010 
00100 
00000 
01111 
10000 
10000 
10000 
10011 
10001 
01111 
00000 
00000`, 
 'Ĥ':` 
00100 
01010 
00000 
10001 
10001 
11111 
10001 
10001 
10001 
10001 
00000 
00000`, 
 'ĥ':` 
00100 
01010 
00000 
10000 
10000 
11110 
10001 
10001 
10001 
10001 
00000 
00000`, 
 'Ħ':` 
00000 
00000 
01010 
11111 
01010 
01110 
01010 
01010 
01010 
01010 
00000 
00000`, 
 'ħ':` 
00000 
00000 
01000 
11100 
01000 
01110 
01001 
01001 
01001 
01001 
00000 
00000`, 
 'Ĩ':` 
01001 
10110 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
11111 
00000 
00000`, 
 'ĩ':` 
01010 
10100 
00000 
00100 
00000 
01100 
00100 
00100 
00100 
01110 
00000 
00000`, 
 'Ī':` 
00000 
01110 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
11111 
00000 
00000`, 
 'ī':` 
00000 
00000 
00000 
01110 
00000 
01100 
00100 
00100 
00100 
01110 
00000 
00000`, 
 'Ĭ':` 
01010 
00100 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
11111 
00000 
00000`, 
 'ĭ':` 
00000 
00000 
01010 
00100 
00000 
01100 
00100 
00100 
00100 
01110 
00000 
00000`, 
 'Į':` 
00000 
00000 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
11111 
00010 
00001`, 
 'į':` 
00000 
00000 
00000 
00100 
00000 
01100 
00100 
00100 
00100 
01110 
00010 
00001`, 
 'İ':` 
00000 
00100 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
11111 
00000 
00000`, 
 'ı':` 
00000 
00000 
00000 
00000 
00000 
01100 
00100 
00100 
00100 
01110 
00000 
00000`, 
 'Ĳ':` 
00000 
00000 
00000 
10010 
10010 
10010 
10010 
10010 
10010 
10110 
00000 
00000`, 
 'ĳ':` 
00000 
00000 
00000 
01001 
00000 
11001 
01001 
01001 
01001 
11101 
00001 
00010`, 
 'Ĵ':` 
00010 
00101 
00000 
01111 
00001 
00001 
00001 
00001 
00001 
01110 
00000 
00000`, 
 'ĵ':` 
00000 
00000 
00100 
01010 
00000 
01100 
00100 
00100 
00100 
00100 
10100 
01000`, 
 'Ķ':` 
00000 
00000 
00000 
10001 
10010 
10100 
11000 
10100 
10010 
10001 
00100 
01000`, 
 'ķ':` 
00000 
00000 
00000 
10000 
10000 
10001 
10010 
11100 
10010 
10001 
00100 
01000`, 
 'ĸ':` 
00000 
00000 
00000 
00000 
00000 
10001 
10010 
11100 
10010 
10001 
00000 
00000`, 
 'Ĺ':` 
00000 
00010 
00100 
10000 
10000 
10000 
10000 
10000 
10000 
11111 
00000 
00000`, 
 'ĺ':` 
00010 
00100 
00000 
01100 
00100 
00100 
00100 
00100 
00100 
01110 
00000 
00000`, 
 'Ļ':` 
00000 
00000 
00000 
10000 
10000 
10000 
10000 
10000 
10000 
11111 
00000 
00100`, 
 'ļ':` 
00000 
00000 
00000 
01100 
00100 
00100 
00100 
00100 
00100 
01110 
00000 
00100`, 
 'Ľ':` 
00000 
00000 
00000 
10010 
10010 
10000 
10000 
10000 
10000 
11111 
00000 
00000`, 
 'ľ':` 
00000 
00000 
00000 
01101 
00101 
00100 
00100 
00100 
00100 
01110 
00000 
00000`, 
 'Ŀ':` 
00000 
00000 
00000 
10000 
10000 
10100 
10000 
10000 
10000 
11111 
00000 
00000`, 
 'ŀ':` 
00000 
00000 
00000 
01100 
00100 
00100 
00101 
00100 
00100 
01110 
00000 
00000`, 
 'Ł':` 
00000 
00000 
00000 
01000 
01010 
01100 
11000 
01000 
01000 
01111 
00000 
00000`, 
 'ł':` 
00000 
00000 
00000 
01100 
00100 
00100 
00110 
01100 
00100 
01110 
00000 
00000`, 
 'Ń':` 
00000 
00010 
00100 
10001 
11001 
10101 
10011 
10001 
10001 
10001 
00000 
00000`, 
 'ń':` 
00000 
00000 
00010 
00100 
00000 
01110 
10001 
10001 
10001 
10001 
00000 
00000`, 
 'Ņ':` 
00000 
00000 
00000 
10001 
11001 
10101 
10011 
10001 
10001 
10001 
00100 
01000`, 
 'ņ':` 
00000 
00000 
00000 
00000 
00000 
01110 
10001 
10001 
10001 
10001 
00100 
01000`, 
 'Ň':` 
00000 
01010 
00100 
10001 
11001 
10101 
10011 
10001 
10001 
10001 
00000 
00000`, 
 'ň':` 
00000 
00000 
01010 
00100 
00000 
01110 
10001 
10001 
10001 
10001 
00000 
00000`, 
 'ŉ':` 
00000 
00000 
00000 
10000 
10000 
00110 
01001 
01001 
01001 
01001 
00000 
00000`, 
 'Ŋ':` 
00000 
00000 
00000 
10001 
11001 
10101 
10011 
10001 
10001 
10001 
00001 
00010`, 
 'ŋ':` 
00000 
00000 
00000 
00000 
00000 
01110 
10001 
10001 
10001 
10001 
00001 
00010`, 
 'Ō':` 
00000 
01110 
00000 
01110 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'ō':` 
00000 
00000 
00000 
01110 
00000 
01110 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'Ŏ':` 
01010 
00100 
00000 
01110 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'ŏ':` 
00000 
00000 
01010 
00100 
00000 
01110 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'Ő':` 
01001 
10010 
00000 
01110 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'ő':` 
00000 
00000 
01001 
10010 
00000 
01110 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'Œ':` 
00000 
00000 
00000 
01111 
10100 
10100 
10111 
10100 
10100 
01111 
00000 
00000`, 
 'œ':` 
00000 
00000 
00000 
00000 
00000 
01110 
10101 
10110 
10100 
01111 
00000 
00000`, 
 'Ŕ':` 
00010 
00100 
00000 
11110 
10001 
10001 
11110 
10001 
10001 
10001 
00000 
00000`, 
 'ŕ':` 
00000 
00000 
00010 
00100 
00000 
01111 
10000 
10000 
10000 
10000 
00000 
00000`, 
 'Ŗ':` 
00000 
00000 
00000 
11110 
10001 
10001 
11110 
10001 
10001 
10001 
00100 
01000`, 
 'ŗ':` 
00000 
00000 
00000 
00000 
00000 
01111 
10000 
10000 
10000 
10000 
00100 
01000`, 
 'Ř':` 
01010 
00100 
00000 
11110 
10001 
10001 
11110 
10001 
10001 
10001 
00000 
00000`, 
 'ř':` 
00000 
00000 
01010 
00100 
00000 
01111 
10000 
10000 
10000 
10000 
00000 
00000`, 
 'Ś':` 
00010 
00100 
00000 
01111 
10000 
01110 
00001 
00001 
00001 
11110 
00000 
00000`, 
 'ś':` 
00000 
00000 
00010 
00100 
00000 
01111 
10000 
01110 
00001 
11110 
00000 
00000`, 
 'Ŝ':` 
00100 
01010 
00000 
01111 
10000 
01110 
00001 
00001 
00001 
11110 
00000 
00000`, 
 'ŝ':` 
00000 
00000 
00100 
01010 
00000 
01111 
10000 
01110 
00001 
11110 
00000 
00000`, 
 'Ş':` 
00000 
00000 
00000 
01111 
10000 
01110 
00001 
00001 
00001 
11110 
00100 
00000`, 
 'ş':` 
00000 
00000 
00000 
00000 
00000 
01111 
10000 
01110 
00001 
11110 
00100 
01000`, 
 'Š':` 
01010 
00100 
00000 
01111 
10000 
01110 
00001 
00001 
00001 
11110 
00000 
00000`, 
 'š':` 
00000 
00000 
01010 
00100 
00000 
01111 
10000 
01110 
00001 
11110 
00000 
00000`, 
 'Ţ':` 
00000 
00000 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
00100 
00010 
00100`, 
 'ţ':` 
00000 
00000 
00000 
00100 
00100 
11111 
00100 
00100 
00100 
00011 
00000 
01100`, 
 'Ť':` 
01010 
00100 
00000 
11111 
00100 
00100 
00100 
00100 
00100 
00100 
00000 
00000`, 
 'ť':` 
00000 
00000 
00001 
00101 
00100 
11111 
00100 
00100 
00100 
00011 
00000 
00000`, 
 'Ŧ':` 
00000 
00000 
00000 
11111 
00100 
00100 
01110 
00100 
00100 
00100 
00000 
00000`, 
 'ŧ':` 
00000 
00000 
00000 
00100 
00100 
11111 
00100 
01110 
00100 
00011 
00000 
00000`, 
 'Ũ':` 
01001 
10110 
00000 
10001 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'ũ':` 
00000 
00000 
01001 
10110 
00000 
10001 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'Ū':` 
00000 
01110 
00000 
10001 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'ū':` 
00000 
00000 
00000 
01110 
00000 
10001 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'Ŭ':` 
01010 
00100 
00000 
10001 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'ŭ':` 
00000 
00000 
01010 
00100 
00000 
10001 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'Ů':` 
00100 
01010 
00100 
10001 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'ů':` 
00000 
00000 
00100 
01010 
00100 
10001 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'Ű':` 
01001 
10010 
00000 
10001 
10001 
10001 
10001 
10001 
10001 
01110 
00000 
00000`, 
 'ű':` 
00000 
00000 
01001 
10010 
00000 
10001 
10001 
10001 
10001 
01111 
00000 
00000`, 
 'Ų':` 
00000 
00000 
00000 
10001 
10001 
10001 
10001 
10001 
10001 
01110 
00100 
00010`, 
 'ų':` 
00000 
00000 
00000 
00000 
00000 
10001 
10001 
10001 
10001 
01111 
00010 
00001`, 
 'Ŵ':` 
00100 
01010 
00000 
10101 
10101 
10101 
10101 
10101 
10101 
01010 
00000 
00000`, 
 'ŵ':` 
00000 
00000 
00100 
01010 
00000 
10101 
10101 
10101 
10101 
01010 
00000 
00000`, 
 'Ŷ':` 
00100 
01010 
00000 
10001 
10001 
01010 
00100 
00100 
00100 
00100 
00000 
00000`, 
 'ŷ':` 
00000 
00000 
00100 
01010 
00000 
10001 
10001 
10001 
10001 
01111 
00001 
11110`, 
 'Ÿ':` 
00000 
01010 
00000 
10001 
10001 
01010 
00100 
00100 
00100 
00100 
00000 
00000`, 
 'Ź':` 
00010 
00100 
00000 
11111 
00001 
00010 
00100 
01000 
10000 
11111 
00000 
00000`, 
 'ź':` 
00000 
00000 
00010 
00100 
00000 
11111 
00010 
00100 
01000 
11111 
00000 
00000`, 
 'Ż':` 
00000 
00100 
00000 
11111 
00001 
00010 
00100 
01000 
10000 
11111 
00000 
00000`, 
 'ż':` 
00000 
00000 
00000 
00100 
00000 
11111 
00010 
00100 
01000 
11111 
00000 
00000`, 
 'Ž':` 
01010 
00100 
00000 
11111 
00001 
00010 
00100 
01000 
10000 
11111 
00000 
00000`, 
 'ž':` 
00000 
00000 
01010 
00100 
00000 
11111 
00010 
00100 
01000 
11111 
00000 
00000`, 
  
 '€':` 
00000 
00000 
00000 
00111 
01000 
11110 
01000 
11110 
01000 
00111 
00000 
00000`, 
  
 '™':` 
00000 
11111 
00100 
00100 
00100 
00000 
01010 
10101 
10101 
10101 
00000 
00000`, 
 '¢':` 
00000 
00000 
00000 
00010 
00100 
01111 
10100 
10100 
10100 
01111 
00100 
01000`, 
  
 '¤':` 
00000 
00000 
00000 
00000 
10001 
01110 
10001 
10001 
01110 
10001 
00000 
00000`, 
 '¥':` 
00000 
00000 
10001 
01010 
00100 
01110 
00100 
01110 
00100 
00000 
00000`, 
  
 '§':` 
00000 
00000 
00000 
01110 
10000 
01110 
10001 
01110 
00001 
01110 
00000 
00000`, 
 '¨':` 
00000 
00000 
00000 
01010 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000`, 
 '©':` 
00000 
00000 
00000 
01110 
10001 
10111 
10101 
10111 
10001 
01110 
00000 
00000`, 
 '®':` 
00000 
00000 
00000 
01110 
10001 
10111 
10101 
10101 
10001 
01110 
00000 
00000`, 
 'ª':` 
00000 
01110 
00010 
01110 
01010 
01110 
00000 
00000 
00000 
00000 
00000 
00000`, 
 'º':` 
00000 
00100 
01010 
01010 
01010 
00100 
00000 
00000 
00000 
00000 
00000 
00000`, 
  
 '¬':` 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
01110 
00010 
00000 
00000 
00000`, 
 '¯':` 
00000 
00000 
00000 
01110 
00000 
00000 
00000 
00000 
00000 
00000 
00000 
00000`, 
 '°':` 
00000 
00000 
00100 
01010 
00100 
00000 
00000 
00000 
00000 
00000 
00000 
00000`, 
 '✓':` 
00000 
00000 
00000 
00000 
00000 
00001 
00010 
10100 
01000 
00000 
00000 
00000`, 
}</script> <script>function RC4(t){this.s=new Array(256),this.i=0,this.j=0;for(var n=0;n<256;n++)this.s[n]=n;t&&this.mix(t)}function print_call_stack(){var t=new Error,n=t.stack;console.log(n)}function RNG(t){this.seed=t,null==t?t=(Math.random()+Date.now()).toString():"function"==typeof t?(this.uniform=t,this.nextByte=function(){return~~(256*this.uniform())},t=null):"[object String]"!==Object.prototype.toString.call(t)&&(t=JSON.stringify(t)),this._normal=null,this._state=t?new RC4(t):null}String.prototype.getBytes=function(){for(var t=[],n=0;n<this.length;n++){var i=this.charCodeAt(n),o=[];do{o.push(255&i),i>>=8}while(i>0);t=t.concat(o.reverse())}return t},RC4.prototype._swap=function(t,n){var i=this.s[t];this.s[t]=this.s[n],this.s[n]=i},RC4.prototype.mix=function(t){for(var n=t.getBytes(),i=0,o=0;o<this.s.length;o++)i+=this.s[o]+n[o%n.length],i%=256,this._swap(o,i)},RC4.prototype.next=function(){return this.i=(this.i+1)%256,this.j=(this.j+this.s[this.i])%256,this._swap(this.i,this.j),this.s[(this.s[this.i]+this.s[this.j])%256]},RNG.prototype.nextByte=function(){return this._state.next()},RNG.prototype.uniform=function(){for(var t=0,n=0;n<7;n++)t*=256,t+=this.nextByte();return t/(Math.pow(2,56)-1)},RNG.prototype.random=function(t,n){return null==t?this.uniform():(null==n&&(n=t,t=0),t+Math.floor(this.uniform()*(n-t)))},RNG.prototype.normal=function(){if(null!==this._normal){var t=this._normal;return this._normal=null,t}var n=this.uniform()||Math.pow(2,-53),i=this.uniform();return this._normal=Math.sqrt(-2*Math.log(n))*Math.sin(2*Math.PI*i),Math.sqrt(-2*Math.log(n))*Math.cos(2*Math.PI*i)},RNG.prototype.exponential=function(){return-Math.log(this.uniform()||Math.pow(2,-53))},RNG.prototype.poisson=function(t){var n=Math.exp(-(t||1)),i=0,o=1;do{i++,o*=this.uniform()}while(o>n);return i-1},RNG.prototype.gamma=function(t){var n=(t<1?1+t:t)-1/3,i=1/Math.sqrt(9*n);do{do{var o=this.normal(),r=Math.pow(i*o+1,3)}while(r<=0);var s=this.uniform(),h=Math.pow(o,2)}while(s>=1-.0331*h*h&&Math.log(s)>=.5*h+n*(1-r+Math.log(r)));return t<1?n*r*Math.exp(this.exponential()/-t):n*r},RNG.roller=function(t,n){var i=t.split(/(\d+)?d(\d+)([+-]\d+)?/).slice(1),o=parseFloat(i[0])||1,r=parseFloat(i[1]),s=parseFloat(i[2])||0;return n=n||new RNG,function(){for(var t=o+s,i=0;i<o;i++)t+=n.random(r);return t}};</script> <script>function FastBase64_Init(){for(var a=0;a<4096;a++)FastBase64_encLookup[a]=FastBase64_chars[a>>6]+FastBase64_chars[63&a]}function FastBase64_Encode(a){for(var e=a.length,s="",u=0;e>2;)n=a[u]<<16|a[u+1]<<8|a[u+2],s+=FastBase64_encLookup[n>>12]+FastBase64_encLookup[4095&n],e-=3,u+=3;if(e>0){var r=(252&a[u])>>2,t=(3&a[u])<<4;if(e>1&&(t|=(240&a[++u])>>4),s+=FastBase64_chars[r],s+=FastBase64_chars[t],2==e){var o=(15&a[u++])<<2;o|=(192&a[u])>>6,s+=FastBase64_chars[o]}1==e&&(s+="="),s+="="}return s}function u32ToArray(a){return[255&a,a>>8&255,a>>16&255,a>>24&255]}function u16ToArray(a){return[255&a,a>>8&255]}function MakeRiff(a,e,n){var s=[],u=[],r=[],t={chunkId:[82,73,70,70],chunkSize:0,format:[87,65,86,69],subChunk1Id:[102,109,116,32],subChunk1Size:16,audioFormat:1,numChannels:1,sampleRate:a,byteRate:0,blockAlign:0,bitsPerSample:e,subChunk2Id:[100,97,116,97],subChunk2Size:0};return t.byteRate=t.sampleRate*t.numChannels*t.bitsPerSample>>3,t.blockAlign=t.numChannels*t.bitsPerSample>>3,t.subChunk2Size=n.length,t.chunkSize=36+t.subChunk2Size,u=t.chunkId.concat(u32ToArray(t.chunkSize),t.format,t.subChunk1Id,u32ToArray(t.subChunk1Size),u16ToArray(t.audioFormat),u16ToArray(t.numChannels),u32ToArray(t.sampleRate),u32ToArray(t.byteRate),u16ToArray(t.blockAlign),u16ToArray(t.bitsPerSample),t.subChunk2Id,u32ToArray(t.subChunk2Size),n),r="data:audio/wav;base64,"+FastBase64_Encode(u),{dat:s,wav:u,header:t,dataURI:r}}var FastBase64_chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",FastBase64_encLookup=[];FastBase64_Init(),"undefined"!=typeof exports&&(exports.RIFFWAVE=RIFFWAVE);</script> <script>function checkAudioContextExists(){try{null==AUDIO_CONTEXT&&("undefined"!=typeof AudioContext?AUDIO_CONTEXT=new AudioContext:"undefined"!=typeof webkitAudioContext&&(AUDIO_CONTEXT=new webkitAudioContext))}catch(e){window.console.log(e)}}function Params(){var e={};return e.wave_type=SQUARE,e.p_env_attack=0,e.p_env_sustain=.3,e.p_env_punch=0,e.p_env_decay=.4,e.p_base_freq=.3,e.p_freq_limit=0,e.p_freq_ramp=0,e.p_freq_dramp=0,e.p_vib_strength=0,e.p_vib_speed=0,e.p_arp_mod=0,e.p_arp_speed=0,e.p_duty=0,e.p_duty_ramp=0,e.p_repeat_speed=0,e.p_pha_offset=0,e.p_pha_ramp=0,e.p_lpf_freq=1,e.p_lpf_ramp=0,e.p_lpf_resonance=0,e.p_hpf_freq=0,e.p_hpf_ramp=0,e.sound_vol=.5,e.sample_rate=44100,e.bit_depth=8,e}function frnd(e){return seeded?rng.uniform()*e:Math.random()*e}function rnd(e){return seeded?Math.floor(rng.uniform()*(e+1)):Math.floor(Math.random()*(e+1))}function SoundEffect(e,r){this._buffer=AUDIO_CONTEXT.createBuffer(1,e,r)}function ULBS(){if("suspended"===AUDIO_CONTEXT.state){var e=function(){AUDIO_CONTEXT.resume().then(function(){document.body.removeEventListener("touchstart",e),document.body.removeEventListener("touchend",e),document.body.removeEventListener("mousedown",e),document.body.removeEventListener("mouseup",e),document.body.removeEventListener("keydown",e),document.body.removeEventListener("keyup",e)})};document.body.addEventListener("touchstart",e,!1),document.body.addEventListener("touchend",e,!1),document.body.addEventListener("mousedown",e,!1),document.body.addEventListener("mouseup",e,!1),document.body.addEventListener("keydown",e,!1),document.body.addEventListener("keyup",e,!1)}}function cacheSeed(e){if(e in sfxCache)return sfxCache[e];var r=generateFromSeed(e);r.sound_vol=SOUND_VOL,r.sample_rate=SAMPLE_RATE,r.bit_depth=BIT_DEPTH;var _=SoundEffect.generate(r);for(sfxCache[e]=_,cachedSeeds.push(e);cachedSeeds.length>CACHE_MAX;){var p=cachedSeeds[0];cachedSeeds=cachedSeeds.slice(1),delete sfxCache[p]}return _}function playSound(e){if(!muted&&(checkAudioContextExists(),!unitTesting)){cacheSeed(e).play()}}function killAudioButton(){var e=document.getElementById("muteButton"),r=document.getElementById("unMuteButton");e&&(e.remove(),r.remove())}function showAudioButton(){var e=document.getElementById("muteButton"),r=document.getElementById("unMuteButton");e&&(e.style.display="block",r.style.display="none")}function toggleMute(){0===muted?muteAudio():unMuteAudio()}function muteAudio(){muted=1,tryDeactivateYoutube();var e=document.getElementById("muteButton"),r=document.getElementById("unMuteButton");e&&(e.style.display="none",r.style.display="block")}function unMuteAudio(){muted=0,tryActivateYoutube();var e=document.getElementById("muteButton"),r=document.getElementById("unMuteButton");e&&(e.style.display="block",r.style.display="none")}var SOUND_VOL=.25,SAMPLE_RATE=5512,BIT_DEPTH=8,SQUARE=0,SAWTOOTH=1,SINE=2,NOISE=3,TRIANGLE=4,BREAKER=5,SHAPES=["square","sawtooth","sine","noise","triangle","breaker"],AUDIO_CONTEXT;checkAudioContextExists();var masterVolume=1,rng,seeded=!1;pickupCoin=function(){var e=Params();if(e.wave_type=Math.floor(frnd(SHAPES.length)),3===e.wave_type&&(e.wave_type=0),e.p_base_freq=.4+frnd(.5),e.p_env_attack=0,e.p_env_sustain=frnd(.1),e.p_env_decay=.1+frnd(.4),e.p_env_punch=.3+frnd(.3),rnd(1)){e.p_arp_speed=.5+frnd(.2);var r=1+(1|frnd(7)),_=r+(1|frnd(7))+2;e.p_arp_mod=+r/+_}return e},laserShoot=function(){var e=Params();return e.wave_type=rnd(2),e.wave_type===SINE&&rnd(1)&&(e.wave_type=rnd(1)),e.wave_type=Math.floor(frnd(SHAPES.length)),3===e.wave_type&&(e.wave_type=SQUARE),e.p_base_freq=.5+frnd(.5),e.p_freq_limit=e.p_base_freq-.2-frnd(.6),e.p_freq_limit<.2&&(e.p_freq_limit=.2),e.p_freq_ramp=-.15-frnd(.2),0===rnd(2)&&(e.p_base_freq=.3+frnd(.6),e.p_freq_limit=frnd(.1),e.p_freq_ramp=-.35-frnd(.3)),rnd(1)?(e.p_duty=frnd(.5),e.p_duty_ramp=frnd(.2)):(e.p_duty=.4+frnd(.5),e.p_duty_ramp=-frnd(.7)),e.p_env_attack=0,e.p_env_sustain=.1+frnd(.2),e.p_env_decay=frnd(.4),rnd(1)&&(e.p_env_punch=frnd(.3)),0===rnd(2)&&(e.p_pha_offset=frnd(.2),e.p_pha_ramp=-frnd(.2)),rnd(1)&&(e.p_hpf_freq=frnd(.3)),e},explosion=function(){var e=Params();return rnd(1)?(e.p_base_freq=.1+frnd(.4),e.p_freq_ramp=-.1+frnd(.4)):(e.p_base_freq=.2+frnd(.7),e.p_freq_ramp=-.2-frnd(.2)),e.p_base_freq*=e.p_base_freq,0===rnd(4)&&(e.p_freq_ramp=0),0===rnd(2)&&(e.p_repeat_speed=.3+frnd(.5)),e.p_env_attack=0,e.p_env_sustain=.1+frnd(.3),e.p_env_decay=frnd(.5),0===rnd(1)&&(e.p_pha_offset=-.3+frnd(.9),e.p_pha_ramp=-frnd(.3)),e.p_env_punch=.2+frnd(.6),rnd(1)&&(e.p_vib_strength=frnd(.7),e.p_vib_speed=frnd(.6)),0===rnd(2)&&(e.p_arp_speed=.6+frnd(.3),e.p_arp_mod=.8-frnd(1.6)),e},birdSound=function(){var e=Params();return frnd(10)<1?(e.wave_type=Math.floor(frnd(SHAPES.length)),3===e.wave_type&&(e.wave_type=SQUARE),e.p_env_attack=.4304400932967592+frnd(.2)-.1,e.p_env_sustain=.15739346034252394+frnd(.2)-.1,e.p_env_punch=.004488201744871758+frnd(.2)-.1,e.p_env_decay=.07478075528212291+frnd(.2)-.1,e.p_base_freq=.9865265720147687+frnd(.2)-.1,e.p_freq_limit=0+frnd(.2)-.1,e.p_freq_ramp=-.2995018224359539+frnd(.2)-.1,frnd(1)<.5&&(e.p_freq_ramp=.1+frnd(.15)),e.p_freq_dramp=.004598608156964473+frnd(.1)-.05,e.p_vib_strength=-.2202799497929496+frnd(.2)-.1,e.p_vib_speed=.8084998703158364+frnd(.2)-.1,e.p_arp_mod=0,e.p_arp_speed=0,e.p_duty=-.9031808754347107+frnd(.2)-.1,e.p_duty_ramp=-.8128699999808343+frnd(.2)-.1,e.p_repeat_speed=.601486018931999+frnd(.2)-.1,e.p_pha_offset=-.9424902314367765+frnd(.2)-.1,e.p_pha_ramp=-.1055482222272056+frnd(.2)-.1,e.p_lpf_freq=.9989765717851521+frnd(.2)-.1,e.p_lpf_ramp=-.25051720626043017+frnd(.2)-.1,e.p_lpf_resonance=.32777871505494693+frnd(.2)-.1,e.p_hpf_freq=.0023548750981756753+frnd(.2)-.1,e.p_hpf_ramp=-.002375673204842568+frnd(.2)-.1,e):frnd(10)<1?(e.wave_type=Math.floor(frnd(SHAPES.length)),3===e.wave_type&&(e.wave_type=SQUARE),e.p_env_attack=.5277795946672003+frnd(.2)-.1,e.p_env_sustain=.18243733568468432+frnd(.2)-.1,e.p_env_punch=-.020159754546840117+frnd(.2)-.1,e.p_env_decay=.1561353422051903+frnd(.2)-.1,e.p_base_freq=.9028855606533718+frnd(.2)-.1,e.p_freq_limit=-.008842787837148716,e.p_freq_ramp=-.1,e.p_freq_dramp=-.012891241489551925,e.p_vib_strength=-.17923136138403065+frnd(.2)-.1,e.p_vib_speed=.908263385610142+frnd(.2)-.1,e.p_arp_mod=.41690153355414894+frnd(.2)-.1,e.p_arp_speed=.0010766233195860704+frnd(.2)-.1,e.p_duty=-.8735363011184684+frnd(.2)-.1,e.p_duty_ramp=-.7397985366747507+frnd(.2)-.1,e.p_repeat_speed=.0591789344172107+frnd(.2)-.1,e.p_pha_offset=-.9961184222777699+frnd(.2)-.1,e.p_pha_ramp=-.08234769395850523+frnd(.2)-.1,e.p_lpf_freq=.9412475115697335+frnd(.2)-.1,e.p_lpf_ramp=-.18261358925834958+frnd(.2)-.1,e.p_lpf_resonance=.24541438107389477+frnd(.2)-.1,e.p_hpf_freq=-.01831940280978611+frnd(.2)-.1,e.p_hpf_ramp=-.03857383633171346+frnd(.2)-.1,e):frnd(10)<1?(e.wave_type=Math.floor(frnd(SHAPES.length)),3===e.wave_type&&(e.wave_type=SQUARE),e.p_env_attack=.4304400932967592+frnd(.2)-.1,e.p_env_sustain=.15739346034252394+frnd(.2)-.1,e.p_env_punch=.004488201744871758+frnd(.2)-.1,e.p_env_decay=.07478075528212291+frnd(.2)-.1,e.p_base_freq=.9865265720147687+frnd(.2)-.1,e.p_freq_limit=0+frnd(.2)-.1,e.p_freq_ramp=-.2995018224359539+frnd(.2)-.1,e.p_freq_dramp=.004598608156964473+frnd(.2)-.1,e.p_vib_strength=-.2202799497929496+frnd(.2)-.1,e.p_vib_speed=.8084998703158364+frnd(.2)-.1,e.p_arp_mod=-.46410459213693644+frnd(.2)-.1,e.p_arp_speed=-.10955361249587248+frnd(.2)-.1,e.p_duty=-.9031808754347107+frnd(.2)-.1,e.p_duty_ramp=-.8128699999808343+frnd(.2)-.1,e.p_repeat_speed=.7014860189319991+frnd(.2)-.1,e.p_pha_offset=-.9424902314367765+frnd(.2)-.1,e.p_pha_ramp=-.1055482222272056+frnd(.2)-.1,e.p_lpf_freq=.9989765717851521+frnd(.2)-.1,e.p_lpf_ramp=-.25051720626043017+frnd(.2)-.1,e.p_lpf_resonance=.32777871505494693+frnd(.2)-.1,e.p_hpf_freq=.0023548750981756753+frnd(.2)-.1,e.p_hpf_ramp=-.002375673204842568+frnd(.2)-.1,e):frnd(5)>1?(e.wave_type=Math.floor(frnd(SHAPES.length)),3===e.wave_type&&(e.wave_type=SQUARE),rnd(1)?(e.p_arp_mod=.2697849293151393+frnd(.2)-.1,e.p_arp_speed=-.3131172257760948+frnd(.2)-.1,e.p_base_freq=.8090588299313949+frnd(.2)-.1,e.p_duty=-.6210022920964955+frnd(.2)-.1,e.p_duty_ramp=-.00043441813553182567+frnd(.2)-.1,e.p_env_attack=.004321877246874195+frnd(.2)-.1,e.p_env_decay=.1+frnd(.2)-.1,e.p_env_punch=.061737781504416146+frnd(.2)-.1,e.p_env_sustain=.4987252564798832+frnd(.2)-.1,e.p_freq_dramp=.31700340314222614+frnd(.2)-.1,e.p_freq_limit=0+frnd(.2)-.1,e.p_freq_ramp=-.163380391341416+frnd(.2)-.1,e.p_hpf_freq=.4709005021145149+frnd(.2)-.1,e.p_hpf_ramp=.6924667290539194+frnd(.2)-.1,e.p_lpf_freq=.8351398631384511+frnd(.2)-.1,e.p_lpf_ramp=.36616557192873134+frnd(.2)-.1,e.p_lpf_resonance=-.08685777111664439+frnd(.2)-.1,e.p_pha_offset=-.036084571580025544+frnd(.2)-.1,e.p_pha_ramp=-.014806445085568108+frnd(.2)-.1,e.p_repeat_speed=-.8094368475518489+frnd(.2)-.1,e.p_vib_speed=.4496665457171294+frnd(.2)-.1,e.p_vib_strength=.23413762515532424+frnd(.2)-.1):(e.p_arp_mod=-.35697118026766184+frnd(.2)-.1,e.p_arp_speed=.3581140690559588+frnd(.2)-.1,e.p_base_freq=1.3260897696157528+frnd(.2)-.1,e.p_duty=-.30984900436710694+frnd(.2)-.1,e.p_duty_ramp=-.0014374759133411626+frnd(.2)-.1,e.p_env_attack=.3160357835682254+frnd(.2)-.1,e.p_env_decay=.1+frnd(.2)-.1,e.p_env_punch=.24323114016870148+frnd(.2)-.1,e.p_env_sustain=.4+frnd(.2)-.1,e.p_freq_dramp=.2866475886237244+frnd(.2)-.1,e.p_freq_limit=0+frnd(.2)-.1,e.p_freq_ramp=-.10956352368742976+frnd(.2)-.1,e.p_hpf_freq=.20772718017889846+frnd(.2)-.1,e.p_hpf_ramp=.1564090637378835+frnd(.2)-.1,e.p_lpf_freq=.6021372770637031+frnd(.2)-.1,e.p_lpf_ramp=.24016227139979027+frnd(.2)-.1,e.p_lpf_resonance=-.08787383821160144+frnd(.2)-.1,e.p_pha_offset=-.381597686151701+frnd(.2)-.1,e.p_pha_ramp=-.0002481687661373495+frnd(.2)-.1,e.p_repeat_speed=.07812112809425686+frnd(.2)-.1,e.p_vib_speed=-.13648848579133943+frnd(.2)-.1,e.p_vib_strength=.0018874158972302657+frnd(.2)-.1),e):(e.wave_type=Math.floor(frnd(SHAPES.length)),1!==e.wave_type&&3!==e.wave_type||(e.wave_type=2),e.p_base_freq=.85+frnd(.15),e.p_freq_ramp=.3+frnd(.15),e.p_env_attack=0+frnd(.09),e.p_env_sustain=.2+frnd(.3),e.p_env_decay=0+frnd(.1),e.p_duty=frnd(2)-1,e.p_duty_ramp=Math.pow(frnd(2)-1,3),e.p_repeat_speed=.5+frnd(.1),e.p_pha_offset=-.3+frnd(.9),e.p_pha_ramp=-frnd(.3),e.p_arp_speed=.4+frnd(.6),e.p_arp_mod=.8+frnd(.1),e.p_lpf_resonance=frnd(2)-1,e.p_lpf_freq=1-Math.pow(frnd(1),3),e.p_lpf_ramp=Math.pow(frnd(2)-1,3),e.p_lpf_freq<.1&&e.p_lpf_ramp<-.05&&(e.p_lpf_ramp=-e.p_lpf_ramp),e.p_hpf_freq=Math.pow(frnd(1),5),e.p_hpf_ramp=Math.pow(frnd(2)-1,5),e)},pushSound=function(){var e=Params();return e.wave_type=Math.floor(frnd(SHAPES.length)),2===e.wave_type&&e.wave_type++,0===e.wave_type&&(e.wave_type=NOISE),e.p_base_freq=.1+frnd(.4),e.p_freq_ramp=.05+frnd(.2),e.p_env_attack=.01+frnd(.09),e.p_env_sustain=.01+frnd(.09),e.p_env_decay=.01+frnd(.09),e.p_repeat_speed=.3+frnd(.5),e.p_pha_offset=-.3+frnd(.9),e.p_pha_ramp=-frnd(.3),e.p_arp_speed=.6+frnd(.3),e.p_arp_mod=.8-frnd(1.6),e},powerUp=function(){var e=Params();return rnd(1)?e.wave_type=SAWTOOTH:e.p_duty=frnd(.6),e.wave_type=Math.floor(frnd(SHAPES.length)),3===e.wave_type&&(e.wave_type=SQUARE),rnd(1)?(e.p_base_freq=.2+frnd(.3),e.p_freq_ramp=.1+frnd(.4),e.p_repeat_speed=.4+frnd(.4)):(e.p_base_freq=.2+frnd(.3),e.p_freq_ramp=.05+frnd(.2),rnd(1)&&(e.p_vib_strength=frnd(.7),e.p_vib_speed=frnd(.6))),e.p_env_attack=0,e.p_env_sustain=frnd(.4),e.p_env_decay=.1+frnd(.4),e},hitHurt=function(){return result=Params(),result.wave_type=rnd(2),result.wave_type===SINE&&(result.wave_type=NOISE),result.wave_type===SQUARE&&(result.p_duty=frnd(.6)),result.wave_type=Math.floor(frnd(SHAPES.length)),result.p_base_freq=.2+frnd(.6),result.p_freq_ramp=-.3-frnd(.4),result.p_env_attack=0,result.p_env_sustain=frnd(.1),result.p_env_decay=.1+frnd(.2),rnd(1)&&(result.p_hpf_freq=frnd(.3)),result},jump=function(){return result=Params(),result.wave_type=SQUARE,result.wave_type=Math.floor(frnd(SHAPES.length)),3===result.wave_type&&(result.wave_type=SQUARE),result.p_duty=frnd(.6),result.p_base_freq=.3+frnd(.3),result.p_freq_ramp=.1+frnd(.2),result.p_env_attack=0,result.p_env_sustain=.1+frnd(.3),result.p_env_decay=.1+frnd(.2),rnd(1)&&(result.p_hpf_freq=frnd(.3)),rnd(1)&&(result.p_lpf_freq=1-frnd(.6)),result},blipSelect=function(){return result=Params(),result.wave_type=rnd(1),result.wave_type=Math.floor(frnd(SHAPES.length)),3===result.wave_type&&(result.wave_type=rnd(1)),result.wave_type===SQUARE&&(result.p_duty=frnd(.6)),result.p_base_freq=.2+frnd(.4),result.p_env_attack=0,result.p_env_sustain=.1+frnd(.1),result.p_env_decay=frnd(.2),result.p_hpf_freq=.1,result},random=function(){return result=Params(),result.wave_type=Math.floor(frnd(SHAPES.length)),result.p_base_freq=Math.pow(frnd(2)-1,2),rnd(1)&&(result.p_base_freq=Math.pow(frnd(2)-1,3)+.5),result.p_freq_limit=0,result.p_freq_ramp=Math.pow(frnd(2)-1,5),result.p_base_freq>.7&&result.p_freq_ramp>.2&&(result.p_freq_ramp=-result.p_freq_ramp),result.p_base_freq<.2&&result.p_freq_ramp<-.05&&(result.p_freq_ramp=-result.p_freq_ramp),result.p_freq_dramp=Math.pow(frnd(2)-1,3),result.p_duty=frnd(2)-1,result.p_duty_ramp=Math.pow(frnd(2)-1,3),result.p_vib_strength=Math.pow(frnd(2)-1,3),result.p_vib_speed=frnd(2)-1,result.p_env_attack=Math.pow(frnd(2)-1,3),result.p_env_sustain=Math.pow(frnd(2)-1,2),result.p_env_decay=frnd(2)-1,result.p_env_punch=Math.pow(frnd(.8),2),result.p_env_attack+result.p_env_sustain+result.p_env_decay<.2&&(result.p_env_sustain+=.2+frnd(.3),result.p_env_decay+=.2+frnd(.3)),result.p_lpf_resonance=frnd(2)-1,result.p_lpf_freq=1-Math.pow(frnd(1),3),result.p_lpf_ramp=Math.pow(frnd(2)-1,3),result.p_lpf_freq<.1&&result.p_lpf_ramp<-.05&&(result.p_lpf_ramp=-result.p_lpf_ramp),result.p_hpf_freq=Math.pow(frnd(1),5),result.p_hpf_ramp=Math.pow(frnd(2)-1,5),result.p_pha_offset=Math.pow(frnd(2)-1,3),result.p_pha_ramp=Math.pow(frnd(2)-1,3),result.p_repeat_speed=frnd(2)-1,result.p_arp_speed=frnd(2)-1,result.p_arp_mod=frnd(2)-1,result};var generators=[pickupCoin,laserShoot,explosion,powerUp,hitHurt,jump,blipSelect,pushSound,random,birdSound],generatorNames=["pickupCoin","laserShoot","explosion","powerUp","hitHurt","jump","blipSelect","pushSound","random","birdSound"];if(generateFromSeed=function(e){rng=new RNG(e/100|0);var r=e%100,_=generators[r%generators.length];seeded=!0;var p=_();return p.seed=e,seeded=!1,p},SoundEffect.prototype.getBuffer=function(){return this._buffer.getChannelData(0)},SoundEffect.prototype.play=function(){ULBS();var e=AUDIO_CONTEXT.createBufferSource(),r=AUDIO_CONTEXT.createBiquadFilter(),_=AUDIO_CONTEXT.createBiquadFilter(),p=AUDIO_CONTEXT.createBiquadFilter();e.buffer=this._buffer,e.connect(r),r.frequency.value=1600,_.frequency.value=1600,p.frequency.value=1600,r.connect(_),_.connect(p),p.connect(AUDIO_CONTEXT.destination);var n=AUDIO_CONTEXT.currentTime;void 0!==e.start?e.start(n):e.noteOn(n),e.onended=function(){p.disconnect()}},SoundEffect.MIN_SAMPLE_RATE=22050,void 0===AUDIO_CONTEXT&&(SoundEffect=function(e,r){this._sample_rate=r,this._buffer=new Array(e),this._audioElement=null},SoundEffect.prototype.getBuffer=function(){return this._audioElement=null,this._buffer},SoundEffect.prototype.play=function(){if(this._audioElement)this._audioElement.cloneNode(!1).play();else{for(var e=0;e<this._buffer.length;e++)this._buffer[e]=255&Math.floor(128*Math.max(0,Math.min(this._buffer[e]+1,2)));var r=MakeRiff(this._sample_rate,BIT_DEPTH,this._buffer);this._audioElement=new Audio,this._audioElement.src=r.dataURI,this._audioElement.play()}},SoundEffect.MIN_SAMPLE_RATE=1),SoundEffect.generate=function(e){function r(){_=0,p=100/(e.p_base_freq*e.p_base_freq+.001),n=Math.floor(p),t=100/(e.p_freq_limit*e.p_freq_limit+.001),f=1-.01*Math.pow(e.p_freq_ramp,3),a=1e-6*-Math.pow(e.p_freq_dramp,3),d=.5-.5*e.p_duty,o=5e-5*-e.p_duty_ramp,u=e.p_arp_mod>=0?1-.9*Math.pow(e.p_arp_mod,2):1+10*Math.pow(e.p_arp_mod,2),s=0,l=Math.floor(2e4*Math.pow(1-e.p_arp_speed,2)+32),1==e.p_arp_speed&&(l=0)}var _,p,n,t,f,a,d,o,u,s,l;r();var i=0,v=0,h=.1*Math.pow(e.p_lpf_freq,3),m=1+1e-4*e.p_lpf_ramp,c=5/(1+20*Math.pow(e.p_lpf_resonance,2))*(.01+h);c>.8&&(c=.8);var y=0,E=.1*Math.pow(e.p_hpf_freq,2),w=1+3e-4*e.p_hpf_ramp,q=0,S=.01*Math.pow(e.p_vib_speed,2),M=.5*e.p_vib_strength,b=0,A=0,g=0,T=[Math.floor(e.p_env_attack*e.p_env_attack*1e5),Math.floor(e.p_env_sustain*e.p_env_sustain*1e5),Math.floor(e.p_env_decay*e.p_env_decay*1e5)],I=T[0]+T[1]+T[2],O=0,P=1020*Math.pow(e.p_pha_offset,2);e.p_pha_offset<0&&(P=-P);var k=1*Math.pow(e.p_pha_ramp,2);e.p_pha_ramp<0&&(k=-k);for(var N=Math.abs(Math.floor(P)),U=0,C=[],B=0;B<1024;++B)C[B]=0;for(var R=[],B=0;B<32;++B)R[B]=2*Math.random()-1;var L=Math.floor(2e4*Math.pow(1-e.p_repeat_speed,2)+32);0==e.p_repeat_speed&&(L=0);var H,x=2*e.sound_vol,x=Math.exp(e.sound_vol)-1,D=0,X=0,Q=Math.floor(44100/e.sample_rate),F=0,V=Math.ceil(I/Q);H=e.sample_rate<SoundEffect.MIN_SAMPLE_RATE?new SoundEffect(4*V,SoundEffect.MIN_SAMPLE_RATE):new SoundEffect(V,e.sample_rate);for(var W=H.getBuffer(),j=0;;++j){0!=L&&++_>=L&&r(),0!=l&&j>=l&&(l=0,p*=u),f+=a,p*=f,p>t&&(p=t,e.p_freq_limit>0&&!0);var G=p;if(M>0&&(q+=S,G=p*(1+Math.sin(q)*M)),n=Math.floor(G),n<8&&(n=8),d+=o,d<0&&(d=0),d>.5&&(d=.5),++g>T[A]){for(g=1,A++;A<3&&0===T[A];)A++;if(3===A)break}b=0===A?g/T[0]:1===A?1+2*Math.pow(1-g/T[1],1)*e.p_env_punch:1-g/T[2],P+=k,N=Math.abs(Math.floor(P)),N>1023&&(N=1023),0!=w&&(E*=w,E<1e-5&&(E=1e-5),E>.1&&(E=.1));for(var K=0,Y=0;Y<8;++Y){var z=0;if(++O>=n&&(O%=n,e.wave_type===NOISE))for(var B=0;B<32;++B)R[B]=2*Math.random()-1;var J=O/n;if(e.wave_type===SQUARE)z=J<d?.5:-.5;else if(e.wave_type===SAWTOOTH)z=1-2*J;else if(e.wave_type===SINE)z=Math.sin(2*J*Math.PI);else if(e.wave_type===NOISE)z=R[Math.floor(32*O/n)];else if(e.wave_type===TRIANGLE)z=Math.abs(1-2*J)-1;else{if(e.wave_type!==BREAKER)throw new Exception("bad wave type! "+e.wave_type);z=Math.abs(1-J*J*2)-1}var Z=i;h*=m,h<0&&(h=0),h>.1&&(h=.1),1!=e.p_lpf_freq?(v+=(z-i)*h,v-=v*c):(i=z,v=0),i+=v,y+=i-Z,y-=y*E,z=y,C[1023&U]=z,z+=C[U-N+1024&1023],U=U+1&1023,K+=z*b}D+=K,++X>=Q&&(X=0,K=D/Q,D=0,K=K/8*masterVolume,K*=x,W[F++]=K,e.sample_rate<SoundEffect.MIN_SAMPLE_RATE&&(W[F++]=K,W[F++]=K,W[F++]=K))}return Q>0&&(K=D/Q,K=K/8*masterVolume,K*=x,W[F++]=K,e.sample_rate<SoundEffect.MIN_SAMPLE_RATE&&(W[F++]=K,W[F++]=K,W[F++]=K)),H},"undefined"!=typeof exports){var RIFFWAVE=require("./riffwave").RIFFWAVE;exports.Params=Params,exports.generate=generate}var sfxCache={},cachedSeeds=[],CACHE_MAX=50;</script> <script>function CodeMirror(t,i){}CodeMirror.defineMode=function(t,i){};var StringStream=CodeMirror.StringStream=function(t,i){this.pos=this.start=0,this.string=t,this.tabSize=i||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0};StringStream.prototype={eol:function(){return this.pos>=this.string.length},sol:function(){return this.pos==this.lineStart},peek:function(){return this.string.charAt(this.pos)||void 0},next:function(){if(this.pos<this.string.length)return this.string.charAt(this.pos++)},eat:function(t){var i=this.string.charAt(this.pos);if("string"==typeof t)var s=i==t;else var s=i&&(t.test?t.test(i):t(i));if(s)return++this.pos,i},eatWhile:function(t){for(var i=this.pos;this.eat(t););return this.pos>i},eatSpace:function(){for(var t=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>t},skipToEnd:function(){this.pos=this.string.length},skipTo:function(t){var i=this.string.indexOf(t,this.pos);if(i>-1)return this.pos=i,!0},backUp:function(t){this.pos-=t},column:function(){return this.lastColumnPos<this.start&&(this.lastColumnValue=countColumn(this.string,this.start,this.tabSize,this.lastColumnPos,this.lastColumnValue),this.lastColumnPos=this.start),this.lastColumnValue-(this.lineStart?countColumn(this.string,this.lineStart,this.tabSize):0)},indentation:function(){return countColumn(this.string,null,this.tabSize)-(this.lineStart?countColumn(this.string,this.lineStart,this.tabSize):0)},match:function(t,i,s){if("string"!=typeof t){var n=this.string.slice(this.pos).match(t);return n&&n.index>0?null:(n&&!1!==i&&(this.pos+=n[0].length),n)}var r=function(t){return s?t.toLowerCase():t};if(r(this.string.substr(this.pos,t.length))==r(t))return!1!==i&&(this.pos+=t.length),!0},current:function(){return this.string.slice(this.start,this.pos)},hideFirstChars:function(t,i){this.lineStart+=t;try{return i()}finally{this.lineStart-=t}}};</script> <script>colorPalettesAliases={1:"mastersystem",2:"gameboycolour",3:"amiga",4:"arnecolors",5:"famicom",6:"atari",7:"pastel",8:"ega",9:"amstrad",10:"proteus_mellow",11:"proteus_rich",12:"proteus_night",13:"c64",14:"whitingjp"},colorPalettes={mastersystem:{black:"#000000",white:"#FFFFFF",grey:"#555555",darkgrey:"#555500",lightgrey:"#AAAAAA",gray:"#555555",darkgray:"#555500",lightgray:"#AAAAAA",red:"#FF0000",darkred:"#AA0000",lightred:"#FF5555",brown:"#AA5500",darkbrown:"#550000",lightbrown:"#FFAA00",orange:"#FF5500",yellow:"#FFFF55",green:"#55AA00",darkgreen:"#005500",lightgreen:"#AAFF00",blue:"#5555AA",lightblue:"#AAFFFF",darkblue:"#000055",purple:"#550055",pink:"#FFAAFF"},gameboycolour:{black:"#000000",white:"#FFFFFF",grey:"#7F7F7C",darkgrey:"#3E3E44",lightgrey:"#BAA7A7",gray:"#7F7F7C",darkgray:"#3E3E44",lightgray:"#BAA7A7",red:"#A7120C",darkred:"#880606",lightred:"#BA381F",brown:"#57381F",darkbrown:"#3E2519",lightbrown:"#8E634B",orange:"#BA4B32",yellow:"#C0BA6F",green:"#517525",darkgreen:"#385D12",lightgreen:"#6F8E44",blue:"#5D6FA7",lightblue:"#8EA7A7",darkblue:"#4B575D",purple:"#3E3E44",pink:"#BA381F"},amiga:{black:"#000000",white:"#FFFFFF",grey:"#BBBBBB",darkgrey:"#333333",lightgrey:"#FFEEDD",gray:"#BBBBBB",darkgray:"#333333",lightgray:"#FFEEDD",red:"#DD1111",darkred:"#990000",lightred:"#FF4422",brown:"#663311",darkbrown:"#331100",lightbrown:"#AA6644",orange:"#FF6644",yellow:"#FFDD66",green:"#448811",darkgreen:"#335500",lightgreen:"#88BB77",blue:"#8899DD",lightblue:"#BBDDEE",darkblue:"#666688",purple:"#665555",pink:"#997788"},arnecolors:{black:"#000000",white:"#FFFFFF",grey:"#9d9d9d",darkgrey:"#697175",lightgrey:"#cccccc",gray:"#9d9d9d",darkgray:"#697175",lightgray:"#cccccc",red:"#be2633",darkred:"#732930",lightred:"#e06f8b",brown:"#a46422",darkbrown:"#493c2b",lightbrown:"#eeb62f",orange:"#eb8931",yellow:"#f7e26b",green:"#44891a",darkgreen:"#2f484e",lightgreen:"#a3ce27",blue:"#1d57f7",lightblue:"#B2DCEF",darkblue:"#1B2632",purple:"#342a97",pink:"#de65e2"},famicom:{black:"#000000",white:"#ffffff",grey:"#7c7c7c",darkgrey:"#080808",lightgrey:"#bcbcbc",gray:"#7c7c7c",darkgray:"#080808",lightgray:"#bcbcbc",red:"#f83800",darkred:"#881400",lightred:"#f87858",brown:"#AC7C00",darkbrown:"#503000",lightbrown:"#FCE0A8",orange:"#FCA044",yellow:"#F8B800",green:"#00B800",darkgreen:"#005800",lightgreen:"#B8F8B8",blue:"#0058F8",lightblue:"#3CBCFC",darkblue:"#0000BC",purple:"#6644FC",pink:"#F878F8"},atari:{black:"#000000",white:"#FFFFFF",grey:"#909090",darkgrey:"#404040",lightgrey:"#b0b0b0",gray:"#909090",darkgray:"#404040",lightgray:"#b0b0b0",red:"#A03C50",darkred:"#700014",lightred:"#DC849C",brown:"#805020",darkbrown:"#703400",lightbrown:"#CB9870",orange:"#CCAC70",yellow:"#ECD09C",green:"#58B06C",darkgreen:"#006414",lightgreen:"#70C484",blue:"#1C3C88",lightblue:"#6888C8",darkblue:"#000088",purple:"#3C0080",pink:"#B484DC"},pastel:{black:"#000000",white:"#FFFFFF",grey:"#3e3e3e",darkgrey:"#313131",lightgrey:"#9cbcbc",gray:"#3e3e3e",darkgray:"#313131",lightgray:"#9cbcbc",red:"#f56ca2",darkred:"#a63577",lightred:"#ffa9cf",brown:"#b58c53",darkbrown:"#787562",lightbrown:"#B58C53",orange:"#EB792D",yellow:"#FFe15F",green:"#00FF4F",darkgreen:"#2b732c",lightgreen:"#97c04f",blue:"#0f88d3",lightblue:"#00fffe",darkblue:"#293a7b",purple:"#ff6554",pink:"#eb792d"},ega:{black:"#000000",white:"#ffffff",grey:"#555555",darkgrey:"#555555",lightgrey:"#aaaaaa",gray:"#555555",darkgray:"#555555",lightgray:"#aaaaaa",red:"#ff5555",darkred:"#aa0000",lightred:"#ff55ff",brown:"#aa5500",darkbrown:"#aa5500",lightbrown:"#ffff55",orange:"#ff5555",yellow:"#ffff55",green:"#00aa00",darkgreen:"#00aaaa",lightgreen:"#55ff55",blue:"#5555ff",lightblue:"#55ffff",darkblue:"#0000aa",purple:"#aa00aa",pink:"#ff55ff"},proteus_mellow:{black:"#3d2d2e",white:"#ddf1fc",grey:"#9fb2d4",darkgrey:"#7b8272",lightgrey:"#a4bfda",gray:"#9fb2d4",darkgray:"#7b8272",lightgray:"#a4bfda",red:"#9d5443",darkred:"#8c5b4a",lightred:"#94614c",brown:"#89a78d",darkbrown:"#829e88",lightbrown:"#aaae97",orange:"#d1ba86",yellow:"#d6cda2",green:"#75ac8d",darkgreen:"#8fa67f",lightgreen:"#8eb682",blue:"#88a3ce",lightblue:"#a5adb0",darkblue:"#5c6b8c",purple:"#d39fac",pink:"#c8ac9e"},proteus_night:{black:"#010912",white:"#fdeeec",grey:"#051d40",darkgrey:"#091842",lightgrey:"#062151",gray:"#051d40",darkgray:"#091842",lightgray:"#062151",red:"#ad4576",darkred:"#934765",lightred:"#ab6290",brown:"#61646b",darkbrown:"#3d2d2d",lightbrown:"#8393a0",orange:"#0a2227",yellow:"#0a2541",green:"#75ac8d",darkgreen:"#0a2434",lightgreen:"#061f2e",blue:"#0b2c79",lightblue:"#809ccb",darkblue:"#08153b",purple:"#666a87",pink:"#754b4d"},proteus_rich:{black:"#6f686f",white:"#d1b1e2",grey:"#b9aac1",darkgrey:"#8e8b84",lightgrey:"#c7b5cd",gray:"#b9aac1",darkgray:"#8e8b84",lightgray:"#c7b5cd",red:"#a11f4f",darkred:"#934765",lightred:"#c998ad",brown:"#89867d",darkbrown:"#797f75",lightbrown:"#ab9997",orange:"#ce8c5c",yellow:"#f0d959",green:"#75bc54",darkgreen:"#599d79",lightgreen:"#90cf5c",blue:"#8fd0ec",lightblue:"#bcdce7",darkblue:"#0b2c70",purple:"#9b377f",pink:"#cd88e5"},amstrad:{black:"#000000",white:"#ffffff",grey:"#7f7f7f",darkgrey:"#636363",lightgrey:"#afafaf",gray:"#7f7f7f",darkgray:"#636363",lightgray:"#afafaf",red:"#ff0000",darkred:"#7f0000",lightred:"#ff7f7f",brown:"#ff7f00",darkbrown:"#7f7f00",lightbrown:"#ffff00",orange:"#ff007f",yellow:"#ffff7f",green:"#01ff00",darkgreen:"#007f00",lightgreen:"#7fff7f",blue:"#0000ff",lightblue:"#7f7fff",darkblue:"#00007f",purple:"#7f007f",pink:"#ff7fff"},c64:{black:"#000000",white:"#ffffff",grey:"#6C6C6C",darkgrey:"#444444",lightgrey:"#959595",gray:"#6C6C6C",darkgray:"#444444",lightgray:"#959595",red:"#68372B",darkred:"#3f1e17",lightred:"#9A6759",brown:"#433900",darkbrown:"#221c02",lightbrown:"#6d5c0d",orange:"#6F4F25",yellow:"#B8C76F",green:"#588D43",darkgreen:"#345129",lightgreen:"#9AD284",blue:"#6C5EB5",lightblue:"#70A4B2",darkblue:"#352879",purple:"#6F3D86",pink:"#b044ac"},whitingjp:{black:"#202527",white:"#eff8fd",grey:"#7b7680",darkgrey:"#3c3b44",lightgrey:"#bed0d7",gray:"#7b7680",darkgray:"#3c3b44",lightgray:"#bed0d7",red:"#bd194b",darkred:"#6b1334",lightred:"#ef2358",brown:"#b52e1c",darkbrown:"#681c12",lightbrown:"#e87b45",orange:"#ff8c10",yellow:"#fbd524",green:"#36bc3c",darkgreen:"#317610",lightgreen:"#8ce062",blue:"#3f62c6",lightblue:"#57bbe0",darkblue:"#2c2fa0",purple:"#7037d9",pink:"#ec2b8f"}};var reg_color_names=/(black|white|darkgray|lightgray|gray|grey|darkgrey|lightgrey|red|darkred|lightred|brown|darkbrown|lightbrown|orange|yellow|green|darkgreen|lightgreen|blue|lightblue|darkblue|purple|pink|transparent)\s*/,reg_color=/(black|white|gray|darkgray|lightgray|grey|darkgrey|lightgrey|red|darkred|lightred|brown|darkbrown|lightbrown|orange|yellow|green|darkgreen|lightgreen|blue|lightblue|darkblue|purple|pink|transparent|#(?:[0-9a-f]{2}){3,4}|#(?:[0-9a-f]{3}))\s*/;</script> <script>function createSprite(name,spritegrid, colors, padding) { 
 if (colors === undefined) { 
  colors = [state.bgcolor, state.fgcolor]; 
 } 
 
 var sprite = makeSpriteCanvas(name); 
 var spritectx = sprite.getContext('2d'); 
 
    spritectx.clearRect(0, 0, cellwidth, cellheight); 
 
 var w = spritegrid[0].length; 
 var h = spritegrid.length; 
 var cw = ~~(cellwidth / (w + (padding|0))); 
    var ch = ~~(cellheight / (h + (padding|0))); 
    var pixh=ch; 
    if ("scanline" in state.metadata) { 
        pixh=Math.ceil(ch/2); 
    } 
    spritectx.fillStyle = state.fgcolor; 
    for (var j = 0; j < h; j++) { 
        for (var k = 0; k < w; k++) { 
            var val = spritegrid[j][k]; 
            if (val >= 0) { 
                var cy = (j * ch)|0; 
                var cx = (k * cw)|0; 
                spritectx.fillStyle = colors[val]; 
                spritectx.fillRect(cx, cy, cw, pixh); 
            } 
        } 
    } 
 
    return sprite; 
} 
 
function drawTextWithCustomFont(txt, ctx, x, y) { 
    ctx.fillStyle = state.fgcolor; 
    ctx.textBaseline = "middle"; 
    ctx.textAlign = "center"; 
    var fontSize = 1; 
    if (state.metadata.font_size !== undefined) { 
        fontSize = parseFloat(state.metadata.font_size) 
    } 
    ctx.font = (cellheight * fontSize) + "px PuzzleCustomFont"; 
    ctx.fillText(txt, x, y); 
} 
 
function regenText(spritecanvas,spritectx) { 
 textImages={}; 
 
 for (var rowidx in titleImage) { 
        var row=titleImage[rowidx]; 
        for (var nidx in row) { 
            var n = row[nidx]; 
            if (font.hasOwnProperty(n) && !textImages.hasOwnProperty(n)) { 
                fontstr = font[n].split('\n').map(a=>a.trim().split('').map(t=>parseInt(t))); 
                fontstr.shift(); 
                textImages[n] = createSprite('char'+n,fontstr, undefined, 1); 
            } 
        } 
    } 
} 
 
var editor_s_grille=[[0,1,1,1,0],[1,0,0,0,0],[0,1,1,1,0],[0,0,0,0,1],[0,1,1,1,0]]; 
 
var spriteimages; 
function regenSpriteImages() { 
 if (textMode) { 
        spriteimages = []; 
  regenText(); 
  return; 
 }  
     
    if (IDE===true) { 
        textImages['editor_s'] = createSprite('chars',editor_s_grille,undefined); 
    } 
     
    if (state.levels.length===0) { 
        return; 
    } 
    spriteimages = []; 
 
    for (var i = 0; i < sprites.length; i++) { 
        if (sprites[i] == undefined) { 
            continue; 
        } 
        spriteimages[i] = createSprite(i.toString(),sprites[i].dat, sprites[i].colors); 
    } 
 
    if (canOpenEditor) { 
     generateGlyphImages(); 
    } 
} 
 
var glyphImagesCorrespondance; 
var glyphImages; 
var glyphHighlight; 
var glyphHighlightDiff; 
var glyphHighlightResize; 
var glyphPrintButton; 
var glyphMouseOver; 
var glyphSelectedIndex=0; 
var editorRowCount=1; 
var editorGlyphMovements=[]; 
 
var canvasdict={}; 
function makeSpriteCanvas(name) { 
    var canvas; 
    if (name in canvasdict) { 
        canvas = canvasdict[name]; 
    } else { 
        canvas = document.createElement('canvas'); 
        canvasdict[name]=canvas; 
    } 
 canvas.width = cellwidth; 
 canvas.height = cellheight; 
 return canvas; 
} 
 
 
function generateGlyphImages() { 
    if (cellwidth===0||cellheight===0) { 
        return; 
    } 
 glyphImagesCorrespondance=[]; 
 glyphImages=[]; 
  
    seenobjects = {}; 
 for (var n in state.glyphDict) { 
  if (n.length==1 && state.glyphDict.hasOwnProperty(n)) {             
   var g=state.glyphDict[n]; 
 
            /* hide duplicate entries from editor palette*/ 
            var trace = g.join(","); 
            if (seenobjects.hasOwnProperty(trace)){ 
                continue; 
            } 
             
   var sprite = makeSpriteCanvas("C"+n) 
   var spritectx = sprite.getContext('2d'); 
   glyphImagesCorrespondance.push(n); 
            seenobjects[trace]=true; 
 
   for (var i=0;i<g.length;i++){ 
    var id = g[i]; 
    if (id===-1) { 
     continue; 
    } 
    spritectx.drawImage(spriteimages[id], 0, 0); 
   } 
   glyphImages.push(sprite); 
  } 
 } 
 
 if (IDE) { 
  //make highlight thingy 
  glyphHighlight = makeSpriteCanvas("highlight"); 
  var spritectx = glyphHighlight.getContext('2d'); 
  spritectx.fillStyle = '#FFFFFF'; 
 
  spritectx.fillRect(0,0,cellwidth,1); 
  spritectx.fillRect(0,0,1,cellheight); 
   
  spritectx.fillRect(0,cellheight-1,cellwidth,1); 
  spritectx.fillRect(cellwidth-1,0,1,cellheight); 
 
  glyphPrintButton = textImages['editor_s']; 
 
  //make diff highlighter thingy 
  glyphHighlightDiff = makeSpriteCanvas("glyphHighlightDiff"); 
  var spritectx = glyphHighlightDiff.getContext('2d'); 
         
  spritectx.fillStyle =  state.bgcolor; 
 
  spritectx.fillRect(0,0,cellwidth,2); 
  spritectx.fillRect(0,0,2,cellheight); 
   
  spritectx.fillRect(0,cellheight-2,cellwidth,2); 
  spritectx.fillRect(cellwidth-2,0,2,cellheight); 
 
  spritectx.fillStyle = state.fgcolor; 
 
  spritectx.fillRect(0,0,cellwidth,1); 
  spritectx.fillRect(0,0,1,cellheight); 
   
  spritectx.fillRect(0,cellheight-1,cellwidth,1); 
  spritectx.fillRect(cellwidth-1,0,1,cellheight); 
 
         
 
  glyphPrintButton = textImages['editor_s']; 
 
 
  //make highlight thingy 
  glyphHighlightResize = makeSpriteCanvas("highlightresize"); 
  var spritectx = glyphHighlightResize.getContext('2d'); 
  spritectx.fillStyle = '#FFFFFF'; 
   
  var minx=((cellwidth/2)-1)|0; 
  var xsize=cellwidth-minx-1-minx; 
  var miny=((cellheight/2)-1)|0; 
  var ysize=cellheight-miny-1-minx; 
 
  spritectx.fillRect(minx,0,xsize,cellheight); 
  spritectx.fillRect(0,miny,cellwidth,ysize); 
 
  //make highlight thingy 
  glyphMouseOver = makeSpriteCanvas("glyphMouseOver"); 
  var spritectx = glyphMouseOver.getContext('2d'); 
  spritectx.fillStyle = 'yellow'; 
   
  spritectx.fillRect(0,0,cellwidth,2); 
  spritectx.fillRect(0,0,2,cellheight); 
   
  spritectx.fillRect(0,cellheight-2,cellwidth,2); 
  spritectx.fillRect(cellwidth-2,0,2,cellheight); 
 
        //make movement glyphs 
 
        /*  
        up:1 
        down:2 
        left:4 
        right:8 
        action:16 
 
        */ 
        const coords = [ 
            //up 
            [ [3,2],[5,0],[7,2]], 
            //down 
            [ [3,8],[5,10],[7,8]], 
            //left 
            [ [2,3],[0,5],[2,7]], 
            //right 
            [ [7,3],[10,5],[7,7]], 
            //action 
            [ [3,5],[5,7],[7,5],[5,3]], 
        ]; 
 
        for (var i=0;i<coords.length;i++){ 
            editorGlyphMovements[i]=makeSpriteCanvas("editorGlyphMovements"+i); 
            var path = coords[i]; 
 
            var spritectx = editorGlyphMovements[i].getContext('2d'); 
            spritectx.lineWidth=1; 
             
             
      spritectx.fillStyle =  state.bgcolor; 
      spritectx.strokeStyle = state.fgcolor; 
 
 
            spritectx.beginPath();       // Start a new path 
            spritectx.moveTo(path[0][0]*cellwidth/10.0, path[0][1]*cellheight/10.0);     
            for (var j=1;j<path.length;j++){ 
                spritectx.lineTo(path[j][0]*cellwidth/10.0, path[j][1]*cellheight/10.0);    
            } 
            spritectx.closePath(); 
            spritectx.fill(); 
            spritectx.stroke();          // Render the path 
 
                 
 
        } 
 } 
} 
 
var canvas; 
var ctx; 
 
 
var x; 
var y; 
var cellwidth; 
var cellheight; 
var xoffset; 
var yoffset; 
 
window.addEventListener('resize', canvasResize, false); 
canvas = document.getElementById('gameCanvas'); 
ctx = canvas.getContext('2d'); 
x = 0; 
y = 0; 
 
function glyphCount(){ 
    var count=0; 
    for (var n in state.glyphDict) { 
        if (n.length==1 && state.glyphDict.hasOwnProperty(n)) { 
            count++; 
        } 
    }     
    return count; 
} 
 
function redraw() { 
    if (cellwidth===0||cellheight===0) { 
        return; 
    } 
    if (spriteimages===undefined) { 
        regenSpriteImages(); 
    } 
 
    if (textMode) { 
        ctx.fillStyle = state.bgcolor; 
        ctx.fillRect(0, 0, canvas.width, canvas.height); 
 
        if(state.metadata.custom_font === undefined || !loadedCustomFont) {  
            for (var i = 0; i < titleWidth; i++) { 
                for (var j = 0; j < titleHeight; j++) { 
                    var ch = titleImage[j].charAt(i); 
                    if (ch in textImages) { 
                        var sprite = textImages[ch]; 
                        ctx.drawImage(sprite, xoffset + i * cellwidth, yoffset + j * cellheight);                    
                    } 
                } 
            } 
        } else { 
            for(var i = 0; i < titleHeight; i++) { 
                var row = titleImage[i]; 
                drawTextWithCustomFont(row, ctx, xoffset + titleWidth * cellwidth / 2, yoffset + i * cellheight + cellheight/2);            
            } 
        } 
        return; 
    } else { 
        var curlevel = level; 
        if (diffToVisualize!==null){ 
            curlevel = new Level(-1,diffToVisualize.width,diffToVisualize.height,diffToVisualize.layerCount,diffToVisualize.objects); 
            curlevel.movements = diffToVisualize.movements; 
        } 
        ctx.fillStyle = state.bgcolor; 
        ctx.fillRect(0, 0, canvas.width, canvas.height); 
 
        var mini=0; 
        var maxi=screenwidth; 
        var minj=0; 
        var maxj=screenheight; 
 
        var cameraOffset = { 
            x: 0, 
            y: 0 
        }; 
 
        if (levelEditorOpened) { 
            var glyphcount = glyphCount(); 
            editorRowCount = Math.ceil(glyphcount/(screenwidth-1)); 
            maxi-=2; 
            maxj-=2+editorRowCount; 
        } else if (flickscreen) { 
            var playerPositions = getPlayerPositions(); 
            if (playerPositions.length>0) { 
                var playerPosition=playerPositions[0]; 
                var px = (playerPosition/(curlevel.height))|0; 
                var py = (playerPosition%curlevel.height)|0; 
 
                var screenx = (px/screenwidth)|0; 
                var screeny = (py/screenheight)|0; 
                mini=screenx*screenwidth; 
                minj=screeny*screenheight; 
                maxi=Math.min(mini+screenwidth,curlevel.width); 
                maxj=Math.min(minj+screenheight,curlevel.height); 
 
                oldflickscreendat=[mini,minj,maxi,maxj]; 
            } else if (oldflickscreendat.length>0){ 
                mini=oldflickscreendat[0]; 
                minj=oldflickscreendat[1]; 
                maxi=oldflickscreendat[2]; 
                maxj=oldflickscreendat[3]; 
            } 
        } else if (zoomscreen) { 
            var playerPositions = getPlayerPositions(); 
            if (playerPositions.length>0) { 
                var playerPosition=playerPositions[0]; 
                var px = (playerPosition/(curlevel.height))|0; 
                var py = (playerPosition%curlevel.height)|0; 
                mini=Math.max(Math.min(px-((screenwidth/2)|0),curlevel.width-screenwidth),0); 
                minj=Math.max(Math.min(py-((screenheight/2)|0),curlevel.height-screenheight),0); 
                maxi=Math.min(mini+screenwidth,curlevel.width); 
                maxj=Math.min(minj+screenheight,curlevel.height); 
                oldflickscreendat=[mini,minj,maxi,maxj]; 
            }  else if (oldflickscreendat.length>0){ 
                mini=oldflickscreendat[0]; 
                minj=oldflickscreendat[1]; 
                maxi=oldflickscreendat[2]; 
                maxj=oldflickscreendat[3]; 
            }          
        } else if (smoothscreen) { 
            if (cameraPositionTarget !== null) { 
                ['x', 'y'].forEach(function (coord) { 
                    var cameraTargetVector = cameraPositionTarget[coord] - cameraPosition[coord]; 
 
                    if (cameraTargetVector === 0) { 
                        return; 
                    } else if (Math.abs(cameraTargetVector) < (0.5 / cellwidth)) { 
                        // Canvas doesn't actually render subpixels, but when the camera is less than half a subpixel away from target, snap to target 
                        cameraPosition[coord] = cameraPositionTarget[coord] 
                        return 
                    } 
 
                    cameraPosition[coord] += cameraTargetVector * state.metadata.smoothscreen.cameraSpeed; 
                    cameraOffset[coord] = cameraPosition[coord] % 1; 
                }) 
 
                mini=Math.max(Math.min(Math.floor(cameraPosition.x)-Math.floor(screenwidth/2), level.width-screenwidth),0); 
                minj=Math.max(Math.min(Math.floor(cameraPosition.y)-Math.floor(screenheight/2), level.height-screenheight),0); 
 
                maxi=Math.min(mini+screenwidth,level.width); 
                maxj=Math.min(minj+screenheight,level.height); 
                oldflickscreendat=[mini,minj,maxi,maxj]; 
            } else if (oldflickscreendat.length>0) { 
                mini=oldflickscreendat[0]; 
                minj=oldflickscreendat[1]; 
                maxi=oldflickscreendat[2]; 
                maxj=oldflickscreendat[3]; 
            } 
 
            ctx.save(); 
            ctx.beginPath(); 
            ctx.moveTo(xoffset, yoffset); 
            ctx.lineTo(xoffset + (maxi - mini) * cellwidth, yoffset); 
            ctx.lineTo(xoffset + (maxi - mini) * cellwidth, yoffset + (maxj - minj) * cellwidth); 
            ctx.lineTo(xoffset, yoffset + (maxj - minj) * cellwidth); 
            ctx.clip(); 
        } 
      
  screenOffsetX = mini; 
  screenOffsetY = minj; 
 
        var renderBorderSize = smoothscreen ? 1 : 0; 
 
        for (var i = Math.max(mini - renderBorderSize, 0); i < Math.min(maxi + renderBorderSize, curlevel.width); i++) { 
            for (var j = Math.max(minj - renderBorderSize, 0); j < Math.min(maxj + renderBorderSize, curlevel.height); j++) { 
                var posIndex = j + i * curlevel.height; 
                var posMask = curlevel.getCellInto(posIndex,_o12);                 
                for (var k = 0; k < state.objectCount; k++) { 
                    if (posMask.get(k) != 0) {                   
                        var sprite = spriteimages[k]; 
                        ctx.drawImage(sprite, Math.floor(xoffset + (i-mini-cameraOffset.x) * cellwidth), Math.floor(yoffset + (j-minj-cameraOffset.y) * cellheight)); 
                    } 
                } 
            } 
        } 
         
        if (diffToVisualize!==null){ 
            //find previous state (this is never called on the very first state, the one before player inputs are applied, so there is always a previous state) 
            var prevstate_lineNumberIndex=diffToVisualize.lineNumber-1; 
            for (;prevstate_lineNumberIndex>=-1;prevstate_lineNumberIndex--) 
            { 
                if (debug_visualisation_array[diffToVisualize.turnIndex].hasOwnProperty(prevstate_lineNumberIndex)){ 
                    break; 
                } 
            } 
 
            var prev_state = debug_visualisation_array[diffToVisualize.turnIndex][prevstate_lineNumberIndex]; 
            var prevlevel = new Level(-1,prev_state.width,prev_state.height,prev_state.layerCount,prev_state.objects); 
            prevlevel.movements = prev_state.movements; 
         
            for (var i = mini; i < maxi; i++) { 
                for (var j = minj; j < maxj; j++) { 
                    var posIndex = j + i * curlevel.height; 
                    var movementbitvec_PREV = prevlevel.getMovements(posIndex); 
                    var movementbitvec = curlevel.getMovements(posIndex); 
                     
                    var posMask_PREV = prevlevel.getCellInto(posIndex,_o11);  
                    var posMask = curlevel.getCellInto(posIndex,_o12);  
                    if (!movementbitvec.equals(movementbitvec_PREV) || !posMask.equals(posMask_PREV)){ 
                        ctx.drawImage(glyphHighlightDiff, xoffset + (i-mini) * cellwidth, yoffset + (j-minj) * cellheight); 
 
                    } 
                } 
            } 
         
            //draw movements! 
            for (var i = mini; i < maxi; i++) { 
                for (var j = minj; j < maxj; j++) { 
                    var posIndex = j + i * curlevel.height; 
                    var movementbitvec = curlevel.getMovements(posIndex); 
                    for (var layer=0;layer<curlevel.layerCount;layer++) { 
                        var layerMovement = movementbitvec.getshiftor(0x1f, 5*layer); 
                        for (var k = 0; k < 5; k++) { 
                            if ((layerMovement&Math.pow(2,k))!==0){ 
                                ctx.drawImage(editorGlyphMovements[k], xoffset + (i-mini) * cellwidth, yoffset + (j-minj) * cellheight); 
                            } 
                        } 
                    }                              
                } 
            } 
        } 
 
        if (smoothscreen) { 
            if (state.metadata.smoothscreen.debug) { 
                drawSmoothScreenDebug(ctx); 
            } 
            ctx.restore(); 
        } 
 
     if (levelEditorOpened) { 
      drawEditorIcons(mini,minj); 
     } 
    } 
} 
 
function drawSmoothScreenDebug(ctx) { 
    ctx.save(); 
 
    var smoothscreenConfig = state.metadata.smoothscreen; 
    var boundarySize = smoothscreenConfig.boundarySize; 
 
    var playerPositions = getPlayerPositions(); 
    if (playerPositions.length > 0) { 
        var playerPosition = { 
            x: (playerPositions[0]/(level.height))|0, 
            y: (playerPositions[0]%level.height)|0 
        }; 
 
        var playerOffsetX = playerPosition.x - cameraPosition.x; 
        var playerOffsetY = playerPosition.y - cameraPosition.y; 
 
        ctx.fillStyle = '#00ff00'; 
        ctx.beginPath(); 
        ctx.arc( 
            xoffset + (Math.floor(screenwidth / 2) + playerOffsetX + 0.5) * cellwidth, 
            yoffset + (Math.floor(screenheight / 2) + playerOffsetY + 0.5) * cellheight, 
            cellwidth / 4, 
            0, 2* Math.PI 
        ); 
        ctx.fill() 
    } 
 
    var targetOffsetX = cameraPositionTarget.x - cameraPosition.x; 
    var targetOffsetY = cameraPositionTarget.y - cameraPosition.y; 
 
    ctx.fillStyle = '#0000ff'; 
    ctx.beginPath(); 
    ctx.arc( 
        xoffset + (Math.floor(screenwidth / 2) + targetOffsetX) * cellwidth, 
        yoffset + (Math.floor(screenheight / 2) + targetOffsetY) * cellheight, 
        cellwidth / 8, 
        0, 2* Math.PI 
    ); 
    ctx.fill() 
 
    ctx.strokeStyle = '#0000ff'; 
    ctx.lineWidth = cellwidth / 16; 
    ctx.strokeRect( 
        xoffset + (Math.floor(screenwidth / 2) + targetOffsetX - Math.floor(boundarySize.width / 2)) * cellwidth, 
        yoffset + (Math.floor(screenheight / 2) + targetOffsetY - Math.floor(boundarySize.height / 2)) * cellheight, 
        boundarySize.width * cellwidth, 
        boundarySize.height * cellheight 
    ); 
 
    ctx.fillStyle = '#ff0000'; 
    ctx.beginPath(); 
    ctx.arc( 
        xoffset + Math.floor(screenwidth / 2) * cellwidth, 
        yoffset + Math.floor(screenheight / 2) * cellheight, 
        cellwidth / 4, 
        0, 2* Math.PI 
    ); 
    ctx.fill() 
 
    ctx.strokeStyle = '#ff0000'; 
    ctx.lineWidth = cellwidth / 8; 
    ctx.strokeRect( 
        xoffset + (Math.floor(screenwidth / 2) - Math.floor(boundarySize.width / 2)) * cellwidth, 
        yoffset + (Math.floor(screenheight / 2) - Math.floor(boundarySize.height / 2)) * cellheight, 
        boundarySize.width * cellwidth, 
        boundarySize.height * cellheight 
    ); 
 
    ctx.restore() 
} 
 
function drawEditorIcons(mini,minj) { 
 var glyphCount = glyphImages.length; 
 var glyphStartIndex=0; 
 var glyphEndIndex = glyphImages.length;/*Math.min( 
       glyphStartIndex+10, 
       screenwidth-2, 
       glyphStartIndex+Math.max(glyphCount-glyphStartIndex,0) 
       );*/ 
 var glyphsToDisplay = glyphEndIndex-glyphStartIndex; 
 
 ctx.drawImage(glyphPrintButton,xoffset-cellwidth,yoffset-cellheight*(1+editorRowCount)); 
 if (mouseCoordY===(-1-editorRowCount)&&mouseCoordX===-1) { 
   ctx.drawImage(glyphMouseOver,xoffset-cellwidth,yoffset-cellheight*(1+editorRowCount));         
 } 
 
 var ypos = editorRowCount-(-mouseCoordY-2)-1; 
 var mouseIndex=mouseCoordX+(screenwidth-1)*ypos; 
 
 for (var i=0;i<glyphsToDisplay;i++) { 
  var glyphIndex = glyphStartIndex+i; 
  var sprite = glyphImages[glyphIndex]; 
        var xpos=i%(screenwidth-1); 
        var ypos=(i/(screenwidth-1))|0; 
  ctx.drawImage(sprite,xoffset+(xpos)*cellwidth,yoffset+ypos*cellheight-cellheight*(1+editorRowCount)); 
  if (mouseCoordX>=0&&mouseCoordX<(screenwidth-1)&&mouseIndex===i) { 
   ctx.drawImage(glyphMouseOver,xoffset+xpos*cellwidth,yoffset+ypos*cellheight-cellheight*(1+editorRowCount));       
  } 
  if (i===glyphSelectedIndex) { 
   ctx.drawImage(glyphHighlight,xoffset+xpos*cellwidth,yoffset+ypos*cellheight-cellheight*(1+editorRowCount)); 
  }    
 } 
 
    //filched from https://raw.githubusercontent.com/ClementSparrow/Pattern-Script/master/src/js/graphics.js 
    var tooltip_string = '' 
    var tooltip_objects = null 
    // prepare tooltip: legend for highlighted editor icon 
    if ( (mouseCoordX >= 0) && (mouseCoordX < screenwidth) && (mouseIndex >= 0) && (mouseIndex < glyphsToDisplay) ) 
    { 
        const glyphIndex = glyphStartIndex + mouseIndex 
        const identifier_index = glyphImagesCorrespondance[glyphIndex] 
        tooltip_string = identifier_index  
        if (identifier_index in state.synonymsDict){ 
            tooltip_string += " = " + state.synonymsDict[identifier_index]; 
        } else if (identifier_index in state.aggregatesDict){ 
            tooltip_string += " = " + state.aggregatesDict[identifier_index].join(" and "); 
             
        } 
    } 
    // prepare tooltip: content of a level's cell 
    else if ( (mouseCoordX >= 0) && (mouseCoordY >= 0) && (mouseCoordX < screenwidth) && (mouseCoordY < screenheight-editorRowCount) ) 
    { 
        const posMask = level.getCellInto((mouseCoordY+minj) + (mouseCoordX+mini)*level.height, _o12); 
        tooltip_objects = state.idDict.filter( (x,k) => (posMask.get(k) != 0) ) 
            // prepare tooltip: object names 
        if (tooltip_objects !== null) 
        { 
            tooltip_string = tooltip_objects.join(', ') 
        } 
    } 
 
    // show tooltip 
    if (tooltip_string.length > 0) 
    { 
        ctx.fillStyle = state.fgcolor; 
        ctx.font = `16px "Source Sans Pro", Helvetica, Arial, sans-serif`; 
        ctx.fillText(tooltip_string, xoffset, yoffset-0.4*cellheight); 
    } 
 
 if (mouseCoordX>=-1&&mouseCoordY>=-1&&mouseCoordX<screenwidth-1&&mouseCoordY<screenheight-1-editorRowCount) { 
  if (mouseCoordX==-1||mouseCoordY==-1||mouseCoordX==screenwidth-2||mouseCoordY===screenheight-2-editorRowCount) { 
   ctx.drawImage(glyphHighlightResize, 
    xoffset+mouseCoordX*cellwidth, 
    yoffset+mouseCoordY*cellheight 
    );         
  } else { 
   ctx.drawImage(glyphHighlight, 
    xoffset+mouseCoordX*cellwidth, 
    yoffset+mouseCoordY*cellheight 
    );     
  } 
 } 
 
} 
 
var lastDownTarget; 
 
var oldcellwidth=0; 
var oldcellheight=0; 
var oldtextmode=-1; 
var oldfgcolor=-1; 
var forceRegenImages=false; 
function canvasResize() { 
    canvas.width = canvas.parentNode.clientWidth; 
    canvas.height = canvas.parentNode.clientHeight; 
 
    screenwidth=level.width; 
    screenheight=level.height; 
    if (state!==undefined){ 
        flickscreen=state.metadata.flickscreen!==undefined; 
        zoomscreen=state.metadata.zoomscreen!==undefined; 
        smoothscreen=state.metadata.smoothscreen!==undefined; 
     if (levelEditorOpened) { 
            screenwidth+=2; 
            var glyphcount = glyphCount(); 
            editorRowCount = Math.ceil(glyphcount/(screenwidth-1)); 
            screenheight+=2+editorRowCount; 
        } else if (flickscreen) { 
         screenwidth=state.metadata.flickscreen[0]; 
         screenheight=state.metadata.flickscreen[1]; 
     } else if (zoomscreen) { 
         screenwidth=state.metadata.zoomscreen[0]; 
         screenheight=state.metadata.zoomscreen[1]; 
     } else if (smoothscreen) { 
         screenwidth=state.metadata.smoothscreen.screenSize.width; 
         screenheight=state.metadata.smoothscreen.screenSize.height; 
     } 
 } 
 
    if (textMode) { 
        screenwidth=titleWidth; 
        screenheight=titleHeight; 
    } 
     
    cellwidth = canvas.width / screenwidth; 
    cellheight = canvas.height / screenheight; 
 
    var w = 5; 
    var h = 5; 
 
    if (sprites.length >= 2) { 
        var w = sprites[1].dat.length; 
        var h = sprites[1].dat.length;//sprites[1].dat[0].length; 
    } 
 
    if (textMode) { 
        w=5 + 1; 
        h=font['X'].length/(w) + 1; 
    } 
 
 
    cellwidth =w * Math.max( ~~(cellwidth / w),1); 
    cellheight = h * Math.max(~~(cellheight / h),1); 
 
    if ((cellwidth == 0 || cellheight == 0) && !textMode) { 
        cellwidth = w; 
        cellheight = h; 
        console.log("Resized below 1"); 
    } 
 
    xoffset = 0; 
    yoffset = 0; 
 
    if (cellwidth / w > cellheight / h  || (textMode && state.metadata.custom_font !== undefined && loadedCustomFont)) { 
        cellwidth = cellheight * w / h; 
        xoffset = (canvas.width - cellwidth * screenwidth) / 2; 
        yoffset = (canvas.height - cellheight * screenheight) / 2; 
    } 
    else { //if (cellheight > cellwidth) { 
        cellheight = cellwidth * h / w; 
        yoffset = (canvas.height - cellheight * screenheight) / 2; 
        xoffset = (canvas.width - cellwidth * screenwidth) / 2; 
    } 
 
    if (levelEditorOpened && !textMode) { 
     xoffset+=cellwidth; 
     yoffset+=cellheight*(1+editorRowCount); 
    } 
 
    cellwidth = cellwidth|0; 
    cellheight = cellheight|0; 
    xoffset = xoffset|0; 
    yoffset = yoffset|0; 
 
    if (oldcellwidth!=cellwidth||oldcellheight!=cellheight||oldtextmode!=textMode||textMode||oldfgcolor!=state.fgcolor||forceRegenImages){ 
     forceRegenImages=false; 
     regenSpriteImages(); 
    } 
 
    oldcellheight=cellheight; 
    oldcellwidth=cellwidth; 
    oldtextmode=textMode; 
    oldfgcolor=state.fgcolor; 
 
    redraw(); 
}</script> <script>var RandomGen = new RNG(); 
 
var intro_template = [ 
  "..................................", 
  "..................................", 
  "..................................", 
  "......Puzzle Script Terminal......", 
  "..............v 1.7+..............", 
  "..................................", 
  "..................................", 
  "..................................", 
  ".........insert cartridge.........", 
  "................................. ", 
  "................................  ", 
  "...............................   ", 
  "..............................    " 
]; 
 
var messagecontainer_template = [ 
  "..................................", 
  "..................................", 
  "..................................", 
  "..................................", 
  "..................................", 
  "..................................", 
  "..................................", 
  "..................................", 
  "..................................", 
  "..................................", 
  "..........X to continue...........", 
  "..................................", 
  ".................................." 
]; 
 
var messagecontainer_template_mouse = [ 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "........Click to continue.........", 
 "..................................", 
 ".................................." 
]; 
 
var titletemplate_firstgo = [ 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..........#.start game.#..........", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 ".................................."]; 
 
var titletemplate_firstgo_selected = [ 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "###########.start game.###########", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 ".................................."]; 
  
var titletemplate_empty = [ 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 "..................................", 
 ".................................."]; 
 
var title_options = [[ 
 ".............continue.............", 
 "...........#.continue.#...........", 
 "############.continue.############" 
], [ 
 "...........level select...........", 
 ".........#.level select.#.........", 
 "##########.level select.##########" 
], [ 
 ".............settings.............", 
 "...........#.settings.#...........", 
 "############.settings.############" 
], [ 
 ".............new game.............", 
 "...........#.new game.#...........", 
 "############.new game.############" 
]]; 
 
var titleImage=[]; 
var titleWidth=titletemplate_empty[0].length; 
var titleHeight=titletemplate_empty.length; 
var textMode=true; 
var titleScreen=true; 
var titleMode=0;//1 means title screen with options, 2 means level select 
var titleSelection=0; 
var titleSelectOptions=2; 
var titleSelected=false; 
 
function showContinueOptionOnTitleScreen(){ 
 return (curlevel>0||curlevelTarget!==null)&&(curlevel in state.levels); 
} 
 
function unloadGame() { 
 state=introstate; 
 level = new Level(0, 5, 5, 2, null, null); 
 level.objects = new Int32Array(0); 
 generateTitleScreen(); 
 canvasResize(); 
 redraw(); 
 titleSelected=true; 
} 
 
function isContinueOptionSelected() { 
 if(state.metadata["continue_is_level_select"] !== undefined) { 
  return false; 
 } 
 
 return titleSelection == 0; // continue option is always 1st 
} 
 
function isNewGameOptionSelected() { 
 return titleSelection == titleSelectOptions-1; // new game option is always on bottom 
} 
 
function isLevelSelectOptionSelected() { 
 if(state.metadata["level_select"] === undefined) { 
  return false; 
 } 
  
 if(state.metadata["continue_is_level_select"] !== undefined) { 
  return titleSelection == 0; // continue option is always 1st 
 } 
 
 return titleSelection == 1; // level select option is always 2nd if it exists 
} 
 
function generateTitleScreen() 
{ 
  tryLoadCustomFont(); 
 
 titleMode=showContinueOptionOnTitleScreen()?1:0; 
 
 if (state.levels.length===0) { 
  titleImage=intro_template; 
  return; 
  } 
 
 var title = "PuzzleScript Game"; 
 if (state.metadata.title!==undefined) { 
  title=state.metadata.title; 
 } 
 
 if (titleMode===0) { 
        titleSelectOptions = 1; 
  if (titleSelected) { 
   titleImage = deepClone(titletemplate_firstgo_selected);   
  } else { 
   titleImage = deepClone(titletemplate_firstgo);      
  } 
 } else { 
 
  var availableOptions = [0]; 
   
  titleSelectOptions = 2; 
 
  if(state.metadata["level_select"] !== undefined && state.metadata["continue_is_level_select"] === undefined) { 
   titleSelectOptions++; 
    
   availableOptions.push(1); 
  } 
 
  if(state.metadata["settings"] !== undefined) { 
   titleSelectOptions++; 
    
   availableOptions.push(2); 
  } 
 
  availableOptions.push(3); 
 
  titleImage = deepClone(titletemplate_empty); 
 
  for(var i = 0; i < titleSelectOptions; i++) { 
   var version = 0; 
   if(titleSelection == i) { 
    if(titleSelected) { 
     version = 2; 
    } else { 
     version = 1; 
    } 
   } 
 
   var j = 0; 
   if(titleSelectOptions == 2 && i == 1) { 
    j = 1; 
   } 
 
   titleImage[5 + i + j] = deepClone(title_options[availableOptions[i]][version]); 
  } 
 } 
 
 var noAction = 'noaction' in state.metadata;  
 var noUndo = 'noundo' in state.metadata; 
 var noRestart = 'norestart' in state.metadata; 
 
 titleImage[10] = ".arrow keys to move..............."; 
 
 if (noAction) { 
  titleImage[11]=".X to select......................"; 
 } else { 
  titleImage[11]=" X to action......................" 
 } 
 
 if (noUndo && noRestart) { 
  titleImage[12]=".................................."; 
 } else if (noUndo) { 
  titleImage[12]=".R to restart....................."; 
 } else if (noRestart) { 
  titleImage[12]=".Z to undo........................"; 
 } else { 
  titleImage[12]=".Z to undo, R to restart.........."; 
 } 
 
 if ("mouse_left" in state.metadata || "mouse_drag" in state.metadata || "mouse_up" in state.metadata) { 
  titleImage[10]=".................................."; 
  titleImage[11]=".MOUSE to interact................"; 
  titleImage[12]=".MMB to undo, R to restart........"; 
 } 
 for (var i=0;i<titleImage.length;i++) 
 { 
  titleImage[i]=titleImage[i].replace(/\./g, ' '); 
 } 
 
 var width = titleImage[0].length; 
 var titlelines=wordwrap(title,titleImage[0].length); 
 if (state.metadata.author!==undefined){ 
  if ( titlelines.length>3){ 
   titlelines.splice(3); 
   logWarning("Game title is too long to fit on screen, truncating to three lines.",undefined,true); 
  } 
 } else { 
  if ( titlelines.length>5){ 
   titlelines.splice(5); 
   logWarning("Game title is too long to fit on screen, truncating to five lines.",undefined,true); 
  } 
 
 } 
 for (var i=0;i<titlelines.length;i++) { 
  var titleline=titlelines[i]; 
  var titleLength=titleline.length; 
  var lmargin = ((width-titleLength)/2)|0; 
  var rmargin = width-titleLength-lmargin; 
  var row = titleImage[1+i]; 
  titleImage[1+i]=row.slice(0,lmargin)+titleline+row.slice(lmargin+titleline.length); 
 } 
 if (state.metadata.author!==undefined) { 
  var attribution="by "+state.metadata.author; 
  var attributionsplit = wordwrap(attribution,titleImage[0].length); 
  if (attributionsplit[0].length<titleImage[0].length){ 
   attributionsplit[0]=" "+attributionsplit[0]; 
  } 
  if (attributionsplit.length>3){ 
   attributionsplit.splice(3); 
   logWarning("Author list too long to fit on screen, truncating to three lines.",undefined,true); 
  } 
  for (var i=0;i<attributionsplit.length;i++) { 
   var line = attributionsplit[i]+" "; 
   if (line.length>width){ 
    line=line.slice(0,width); 
   } 
   var row = titleImage[3+i]; 
   titleImage[3+i]=row.slice(0,width-line.length)+line; 
  } 
 } 
 
} 
 
var levelSelectScrollPos = 0; 
 
function gotoLevelSelectScreen() { 
 if(state.metadata["level_select"] === undefined){ 
  goToTitleScreen(); 
  return; 
 } 
 levelSelectScrollPos = 0; 
 titleSelected = false; 
 timer = 0; 
 quittingTitleScreen = false; 
 quittingMessageScreen = false; 
 messageselected = false; 
 titleMode = 2; 
 titleScreen = true; 
 textMode = true; 
    againing = false; 
 messagetext = ""; 
 
 if (titleSelection == false) { 
  for(var i = 0; i < state.sections.length; i++) { 
   if(state.sections[i].firstLevel > curlevel) { 
    titleSelection = Math.max(0,i-1); 
    break; 
   } 
  } 
  } 
   
  state.metadata = deepClone(state.default_metadata); 
  twiddleMetadataExtras(); 
 
 generateLevelSelectScreen(); 
 redraw(); 
} 
 
function generateLevelSelectScreen() { 
 /* 
 "...........select level...........", 
 "..................................", 
 "[✓].#.Testy section maxxx long.#.O", 
 ".................................|", 
 "[ ]...Unselected section.........|", 
 ".................................|", 
 "[ ]...Another section............|", 
 ".................................|", 
 "[ ]...Another section............|", 
 ".................................|", 
 "[ ]...Another section............|", 
 ".................................|", 
 "[ ]...Another section............|" 
  */ 
 
 titleImage = [ 
  "ESC: Back             Level Select", 
  "                                  " 
 ]; 
 
 amountOfLevelsOnScreen = 9; 
 
 titleSelectOptions = state.sections.length; 
 
 if(titleSelection < levelSelectScrollPos) { //Up 
  levelSelectScrollPos = titleSelection; 
 } else if(titleSelection >= levelSelectScrollPos + amountOfLevelsOnScreen) { //Down 
  levelSelectScrollPos = titleSelection - amountOfLevelsOnScreen + 1; 
 } 
 
 var posOnScreen = titleSelection - levelSelectScrollPos; 
 if (posOnScreen == 0) { 
  levelSelectScrollPos = Math.max(0, levelSelectScrollPos - 1); 
  //console.log("On first position"); 
 } 
 
 if (posOnScreen == amountOfLevelsOnScreen-1) { 
  levelSelectScrollPos = Math.min(state.sections.length-amountOfLevelsOnScreen, levelSelectScrollPos + 1); 
  //console.log("On last position"); 
 } 
 
 if (levelSelectScrollPos != 0) { 
  titleImage.push("            [ PREV ]              "); 
 } else { 
  titleImage.push("                                  "); 
 } 
 
 var unlockedUntil = -1; 
 if(state.metadata["level_select_lock"] !== undefined) { 
  // find last solved section: 
  var unsolvedSections = 0; 
  for(var i = 0; i < state.sections.length; i++) { 
   if(solvedSections.indexOf(state.sections[i].name) >= 0) { 
    unlockedUntil = i; 
   } else { 
    unsolvedSections++; 
   } 
  } 
 
  if(state.metadata.level_select_unlocked_ahead === undefined) { 
   unlockedUntil += 1; 
  } else { 
   unlockedUntil += Number(state.metadata.level_select_unlocked_ahead); 
  }  
 } 
 //console.log(unlockedUntil); 
 
 for(var i = levelSelectScrollPos; i < levelSelectScrollPos + amountOfLevelsOnScreen; i++) { 
  if(i < 0 || i >= state.sections.length) { 
   break; 
  } 
 
  var section = state.sections[i]; 
  var solved = (solvedSections.indexOf(section.name) >= 0); 
  var selected = (i == titleSelection); 
  var locked = (unlockedUntil >= 0 && i > unlockedUntil); 
 
  var name = section.name.substring(0, 24); 
   
  if(locked) { 
   if(selected && titleSelected) { 
    titleSelected = false; 
    quittingTitleScreen = false; 
   } 
 
   var l = name.length; 
   name = ""; 
   for(var j = 0; j < l; j++) { 
    name += "*"; 
   } 
  } 
   
  var solved_symbol = "✓"; 
  if(state.metadata.level_select_solve_symbol !== undefined) { 
   solved_symbol = state.metadata.level_select_solve_symbol; 
  } 
  var line = (solved ? solved_symbol : " ") + " "; 
   
  line += (selected ? "#" : " ") + " " + name; 
  for(var j = name.length; j < 25; j++) { 
   if(selected && titleSelected && j != name.length) { 
    line += "#"; 
   } else { 
    line += " "; 
   } 
  } 
  line += (selected ? "#" : " ") + "  "; 
 
  titleImage.push(line); 
 } 
 
 if (levelSelectScrollPos != titleSelectOptions - amountOfLevelsOnScreen && titleSelectOptions - amountOfLevelsOnScreen > 0) { 
  titleImage.push("            [ NEXT ]              ") 
 } else { 
  titleImage.push("                                  "); 
 } 
 
 for(var i = titleImage.length; i < 13; i++) { 
  titleImage.push("                                  "); 
 } 
} 
 
function gotoLevel(sectionIndex) { 
  if (solving) { 
    return; 
  } 
 
  if (sectionIndex < 0) {return;} //Invalid index 
 
  console.log(sectionIndex); 
 
 againing = false; 
 messagetext = ""; 
 
 curlevel = state.sections[sectionIndex].firstLevel; 
 
 loadLevelFromStateOrTarget(); 
 
 updateLocalStorage(); 
 resetFlickDat(); 
 canvasResize();  
 clearInputHistory(); 
} 
 
var introstate = { 
  title: "EMPTY GAME", 
  attribution: "increpare", 
    objectCount: 2, 
    metadata:[], 
    levels:[], 
    bgcolor:"#000000", 
    fgcolor:"#FFFFFF" 
}; 
 
var state = introstate; 
 
function deepClone(item) { 
    if (!item) { return item; } // null, undefined values check 
 
    var types = [ Number, String, Boolean ],  
        result; 
 
    // normalizing primitives if someone did new String('aaa'), or new Number('444'); 
    types.forEach(function(type) { 
        if (item instanceof type) { 
            result = type( item ); 
        } 
    }); 
 
    if (typeof result == "undefined") { 
        if (Object.prototype.toString.call( item ) === "[object Array]") { 
            result = []; 
            item.forEach(function(child, index, array) {  
                result[index] = deepClone( child ); 
            }); 
        } else if (typeof item == "object") { 
            // testing that this is DOM 
            if (item.nodeType && typeof item.cloneNode == "function") { 
                var result = item.cloneNode( true );     
            } else if (!item.prototype) { // check that this is a literal 
                if (item instanceof Date) { 
                    result = new Date(item); 
                } else { 
                    // it is an object literal 
                    result = {}; 
                    for (var i in item) { 
                        result[i] = deepClone( item[i] ); 
                    } 
                } 
            } else { 
                // depending what you would like here, 
                // just keep the reference, or create new object 
/*                if (false && item.constructor) { 
                    // would not advice to do that, reason? Read below 
                    result = new item.constructor(); 
                } else */{ 
                    result = item; 
                } 
            } 
        } else { 
            result = item; 
        } 
    } 
 
    return result; 
} 
 
function wordwrap( str, width ) { 
  
    width = width || 75; 
    var cut = true; 
  
    if (!str) { return str; } 
  
    var regex = '.{1,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)'); 
  
    return str.match( RegExp(regex, 'g') ); 
  
} 
 
var splitMessage=[]; 
 
function drawMessageScreen() { 
 tryLoadCustomFont(); 
 
 titleMode=0; 
 textMode=true; 
 if ("mouse_left" in state.metadata || "mouse_drag" in state.metadata || "mouse_up" in state.metadata) 
  titleImage = deepClone(messagecontainer_template_mouse); 
 else 
  titleImage = deepClone(messagecontainer_template); 
 
 for (var i=0;i<titleImage.length;i++) 
 { 
  titleImage[i]=titleImage[i].replace(/\./g, ' '); 
 } 
 
 var emptyLineStr = titleImage[9]; 
 var xToContinueStr = titleImage[10]; 
 
 titleImage[10]=emptyLineStr; 
 
 var width = titleImage[0].length; 
 
 var message; 
 if (messagetext==="") { 
  var leveldat = state.levels[curlevel]; 
  message = leveldat.message.trim(); 
 } else { 
  message = messagetext; 
 } 
  
 splitMessage = wordwrap(message,titleImage[0].length); 
 
 
 var offset = 5-((splitMessage.length/2)|0); 
 if (offset<0){ 
  offset=0; 
 } 
 
 var count = Math.min(splitMessage.length,12); 
 for (var i=0;i<count;i++) { 
  var m = splitMessage[i]; 
  var row = offset+i;  
  var messageLength=m.length; 
  var lmargin = ((width-messageLength)/2)|0; 
  var rmargin = width-messageLength-lmargin; 
  var rowtext = titleImage[row]; 
  titleImage[row]=rowtext.slice(0,lmargin)+m+rowtext.slice(lmargin+m.length);   
 } 
 
 var endPos = 10; 
 if (count>=10) { 
  if (count<12){ 
   endPos = count + 1; 
  } else { 
   endPos = 12; 
  } 
        } 
  if (quittingMessageScreen) { 
    titleImage[endPos]=emptyLineStr; 
  } else { 
    titleImage[endPos]=xToContinueStr; 
  } 
   
  canvasResize(); 
} 
 
var loadedLevelSeed=0; 
 
function loadLevelFromLevelDat(state,leveldat,randomseed,clearinputhistory) {  
 if (randomseed==null) { 
  randomseed = (Math.random() + Date.now()).toString(); 
 } 
 loadedLevelSeed = randomseed; 
 RandomGen = new RNG(loadedLevelSeed); 
 forceRegenImages=true; 
 ignoreNotJustPressedAction=true; 
 titleScreen=false; 
 titleMode=showContinueOptionOnTitleScreen()?1:0; 
 titleSelection=showContinueOptionOnTitleScreen()?1:0; 
  titleSelected=false; 
  state.metadata = deepClone(state.default_metadata); 
    againing=false; 
    if (leveldat===undefined) { 
     consolePrint("Trying to access a level that doesn't exist.",true); 
  goToTitleScreen(); 
     return; 
    } 
    if (leveldat.message !== undefined) { 
      // This "level" is actually a message. 
      ignoreNotJustPressedAction=true; 
      tryPlayShowMessageSound(); 
      drawMessageScreen(); 
      canvasResize(); 
      clearInputHistory(); 
    } else if (leveldat.target !== undefined) { 
      // This "level" is actually a goto. 
      //tryPlayGotoSound(); 
      setSectionSolved(state.levels[Number(curlevel)].section) 
      gotoLevel(leveldat.target); 
    } else { 
      titleMode=0; 
      textMode=false; 
    level = leveldat.clone(); 
    RebuildLevelArrays(); 
        if (state!==undefined) { 
         if (state.metadata.flickscreen!==undefined){ 
             oldflickscreendat=[ 
              0, 
              0, 
              Math.min(state.metadata.flickscreen[0],level.width), 
              Math.min(state.metadata.flickscreen[1],level.height) 
             ]; 
         } else if (state.metadata.zoomscreen!==undefined){ 
             oldflickscreendat=[ 
              0, 
              0, 
              Math.min(state.metadata.zoomscreen[0],level.width), 
              Math.min(state.metadata.zoomscreen[1],level.height) 
             ]; 
         } else if (state.metadata.smoothscreen!==undefined){ 
             oldflickscreendat=[ 
              0, 
              0, 
              Math.min(state.metadata.smoothscreen.screenSize.width,level.width), 
              Math.min(state.metadata.smoothscreen.screenSize.height,level.height) 
             ]; 
         } 
        } 
 
      initSmoothCamera(); 
      twiddleMetadataExtras(); 
 
     backups=[] 
     restartTarget=backupLevel(); 
  keybuffer=[]; 
 
     if ('run_rules_on_level_start' in state.metadata) { 
   runrulesonlevelstart_phase=true; 
   processInput(-1,true); 
   runrulesonlevelstart_phase=false; 
     } 
 } 
 
 if (clearinputhistory===true){ 
  clearInputHistory(); 
 } 
} 
 
function loadLevelFromStateTarget(state,levelindex,target,randomseed) {  
    var leveldat = target;     
  curlevel=levelindex; 
  curlevelTarget=target; 
    if (leveldat.message===undefined) { 
      if (levelindex=== 0){  
      tryPlayStartLevelSound(); 
    } else { 
      tryPlayStartLevelSound();      
    } 
    } 
    loadLevelFromLevelDat(state,state.levels[levelindex],randomseed); 
    restoreLevel(target, true); 
    restartTarget=target; 
} 
 
function loadLevelFromState(state,levelindex,randomseed) {   
    var leveldat = state.levels[levelindex];     
  curlevel=levelindex; 
  curlevelTarget=null; 
    if (leveldat!==undefined && leveldat.message===undefined) { 
      if (levelindex=== 0){  
      tryPlayStartLevelSound(); 
    } else { 
      tryPlayStartLevelSound();      
    } 
    } 
    loadLevelFromLevelDat(state,leveldat,randomseed); 
} 
 
var sprites = [ 
{ 
    color: '#423563', 
    dat: [ 
        [1, 1, 1, 1, 1], 
        [1, 0, 0, 0, 1], 
        [1, 0, 0, 0, 1], 
        [1, 0, 0, 0, 1], 
        [1, 1, 1, 1, 1] 
    ] 
}, 
{ 
    color: '#252342', 
    dat: [ 
        [0, 0, 1, 0, 0], 
        [1, 1, 1, 1, 1], 
        [0, 0, 1, 0, 0], 
        [0, 1, 1, 1, 0], 
        [0, 1, 0, 1, 0] 
    ] 
} 
]; 
 
loadedCustomFont = false; 
 
function tryLoadCustomFont() { 
 if(state == null || state.metadata == null || state.metadata.custom_font == undefined || loadedCustomFont) { 
  return; 
 } 
 
 var custom_font = new FontFace('PuzzleCustomFont', 'url('+state.metadata.custom_font+')'); 
 custom_font.load().then(function(loaded_face) { 
  document.fonts.add(loaded_face); 
  loadedCustomFont = true; 
  canvasResize(); 
  redraw(); 
 }).catch(function(error) {alert("Unable to load font!");}); 
} 
 
tryLoadCustomFont(); 
 
generateTitleScreen(); 
if (titleMode>0){ 
 titleSelection=0; 
} 
 
canvasResize(); 
 
function tryPlaySimpleSound(soundname) { 
  if (state.sfx_Events[soundname]!==undefined) { 
    var seed = state.sfx_Events[soundname]; 
    playSound(seed); 
  } 
} 
function tryPlayTitleSound() { 
  tryPlaySimpleSound("titlescreen"); 
} 
 
function tryPlayStartGameSound() { 
  tryPlaySimpleSound("startgame"); 
} 
 
function tryPlayEndGameSound() { 
  tryPlaySimpleSound("endgame"); 
} 
 
function tryPlayCancelSound() { 
  tryPlaySimpleSound("cancel"); 
} 
 
function tryPlayStartLevelSound() { 
  tryPlaySimpleSound("startlevel"); 
} 
 
function tryPlayEndLevelSound() { 
  tryPlaySimpleSound("endlevel"); 
} 
 
function tryPlayUndoSound(){ 
  tryPlaySimpleSound("undo"); 
} 
 
function tryPlayRestartSound(){ 
  tryPlaySimpleSound("restart"); 
} 
 
function tryPlayShowMessageSound(){ 
  tryPlaySimpleSound("showmessage"); 
} 
 
function tryPlayCloseMessageSound(){ 
  tryPlaySimpleSound("closemessage"); 
} 
 
var backups=[]; 
var restartTarget; 
 
function backupLevel() { 
 var ret = { 
  dat : new Int32Array(level.objects), 
  width : level.width, 
  height : level.height, 
  oldflickscreendat: oldflickscreendat.concat([]), 
    cameraPositionTarget: Object.assign({}, cameraPositionTarget) 
 }; 
 if (state.metadata.runtime_metadata_twiddling !== undefined) { 
      var metadata = deepClone(state.metadata) 
      delete metadata.custom_font; 
      ret.metadata = metadata; 
    } 
 return ret; 
} 
 
function level4Serialization() { 
 var ret = { 
  dat : Array.from(level.objects), 
  width : level.width, 
  height : level.height, 
  oldflickscreendat: oldflickscreendat.concat([]), 
    cameraPositionTarget: Object.assign({}, cameraPositionTarget) 
 }; 
 return ret; 
} 
 
 
function tryDeactivateYoutube(){ 
  var youtubeFrame = document.getElementById("youtubeFrame"); 
  if (youtubeFrame){ 
    document.body.removeChild(youtubeFrame); 
  } 
} 
 
function tryActivateYoutube(){ 
  var youtubeFrame = document.getElementById("youtubeFrame"); 
  if (youtubeFrame){ 
    return; 
  } 
  if (canYoutube) { 
    if ('youtube' in state.metadata) { 
      var youtubeid=state.metadata['youtube']; 
      var url = "https://www.youtube.com/embed/"+youtubeid+"?autoplay=1&loop=1&playlist="+youtubeid; 
      ifrm = document.createElement("IFRAME"); 
      ifrm.setAttribute("src",url); 
      ifrm.setAttribute("id","youtubeFrame"); 
      ifrm.style.visibility="hidden"; 
      ifrm.style.width="500px"; 
      ifrm.style.height="500px"; 
      ifrm.style.position="absolute"; 
      ifrm.style.top="-1000px"; 
      ifrm.style.left="-1000px"; 
      document.body.appendChild(ifrm); 
    } 
  } 
} 
 
function setGameState(_state, command, randomseed) { 
  oldflickscreendat=[]; 
  timer=0; 
  autotick=0; 
  winning=false; 
  againing=false; 
    messageselected=false; 
    STRIDE_MOV=_state.STRIDE_MOV; 
    STRIDE_OBJ=_state.STRIDE_OBJ; 
     
    sfxCreateMask=new BitVec(STRIDE_OBJ); 
    sfxDestroyMask=new BitVec(STRIDE_OBJ); 
 
  if (command===undefined) { 
    command=["restart"]; 
  } 
  if ((state.levels.length===0 || _state.levels.length===0) && command.length>0 && command[0]==="rebuild")  { 
    command=["restart"]; 
  } 
  if (randomseed===undefined) { 
    randomseed=null; 
  } 
  RandomGen = new RNG(randomseed); 
 
  state = _state; 
 
    if (command[0]!=="rebuild"){ 
      backups=[]; 
    } 
    //set sprites 
    sprites = []; 
    for (var n in state.objects) { 
        if (state.objects.hasOwnProperty(n)) { 
            var object = state.objects[n]; 
            var sprite = { 
                colors: object.colors, 
                dat: object.spritematrix 
            }; 
            sprites[object.id] = sprite; 
        } 
    } 
    if (state.metadata.realtime_interval!==undefined) { 
      autotick=0; 
      autotickinterval=state.metadata.realtime_interval*1000; 
    } else { 
      autotick=0; 
      autotickinterval=0; 
    } 
 
    if (state.metadata.key_repeat_interval!==undefined) { 
    repeatinterval=state.metadata.key_repeat_interval*1000; 
    } else { 
      repeatinterval=150; 
    } 
 
    if (state.metadata.again_interval!==undefined) { 
    againinterval=state.metadata.again_interval*1000; 
    } else { 
      againinterval=150; 
    } 
    if (throttle_movement && autotickinterval===0) { 
      logWarning("throttle_movement is designed for use in conjunction with realtime_interval. Using it in other situations makes games gross and unresponsive, broadly speaking.  Please don't."); 
    } 
    norepeat_action = state.metadata.norepeat_action!==undefined; 
     
    switch(command[0]){ 
     case "restart": 
     { 
      if (restarting==true){ 
       logWarning('A "restart" command is being triggered in the "run_rules_on_level_start" section of level creation, which would cause an infinite loop if it was actually triggered, but it\'s being ignored, so it\'s not.'); 
       break; 
      } 
      winning=false; 
      timer=0; 
      titleScreen=true; 
      tryPlayTitleSound(); 
      textMode=true; 
      titleSelection=showContinueOptionOnTitleScreen()?1:0; 
      titleSelected=false; 
      quittingMessageScreen=false; 
      quittingTitleScreen=false; 
      messageselected=false; 
      titleMode = 0; 
      if (showContinueOptionOnTitleScreen()) { 
       titleMode=1; 
      } 
      generateTitleScreen(); 
      break; 
  } 
  case "rebuild": 
  { 
   //do nothing 
   break; 
  } 
  case "loadFirstNonMessageLevel":{ 
   for (var i=0;i<state.levels.length;i++){ 
    if (state.levels[i].hasOwnProperty("message")){ 
     continue; 
    } 
    var targetLevel = i; 
    curlevel=targetLevel; 
    curlevelTarget=null; 
       winning=false; 
       timer=0; 
       titleScreen=false; 
       textMode=false; 
       //titleSelection=showContinueOptionOnTitleScreen()?1:0; 
       titleSelected=false; 
       quittingMessageScreen=false; 
       quittingTitleScreen=false; 
       messageselected=false; 
       titleMode = 0; 
    loadLevelFromState(state,targetLevel,randomseed); 
    break; 
   } 
   break;  
  } 
  case "loadLevel": 
  { 
   var targetLevel = command[1]; 
   curlevel=targetLevel; 
   curlevelTarget=null; 
      winning=false; 
      timer=0; 
      titleScreen=false; 
      textMode=false; 
      //titleSelection=showContinueOptionOnTitleScreen()?1:0; 
      titleSelected=false; 
      quittingMessageScreen=false; 
      quittingTitleScreen=false; 
      messageselected=false; 
      titleMode = 0; 
   loadLevelFromState(state,targetLevel,randomseed); 
   break; 
  } 
  case "levelline": 
  { 
   var targetLine = command[1]; 
   for (var i=state.levels.length-1;i>=0;i--) { 
    var level= state.levels[i]; 
    if(level.lineNumber<=targetLine+1) { 
     curlevel=i; 
     curlevelTarget=null; 
        winning=false; 
        timer=0; 
        titleScreen=false; 
        textMode=false; 
        //titleSelection=showContinueOptionOnTitleScreen()?1:0; 
        titleSelected=false; 
        quittingMessageScreen=false; 
        quittingTitleScreen=false; 
        messageselected=false; 
        titleMode = 0; 
     loadLevelFromState(state,i); 
     break; 
    } 
   } 
   break; 
  } 
 } 
 
 if(command[0] !== "rebuild") { 
  clearInputHistory(); 
 } 
 canvasResize(); 
 
 if (state.sounds.length==0&&state.metadata.youtube==null){ 
  killAudioButton(); 
 } else { 
  showAudioButton(); 
 } 
} 
 
function RebuildLevelArrays() { 
  level.movements = new Int32Array(level.n_tiles * STRIDE_MOV); 
 
    level.rigidMovementAppliedMask = []; 
    level.rigidGroupIndexMask = []; 
 level.rowCellContents = []; 
 level.rowCellContents_Movements = []; 
 level.colCellContents = []; 
 level.colCellContents_Movements = []; 
 level.mapCellContents = new BitVec(STRIDE_OBJ); 
 level.mapCellContents_Movements = new BitVec(STRIDE_MOV); 
 
 _movementVecs = [new BitVec(STRIDE_MOV),new BitVec(STRIDE_MOV),new BitVec(STRIDE_MOV)]; 
 
 _o1 = new BitVec(STRIDE_OBJ); 
 _o2 = new BitVec(STRIDE_OBJ); 
 _o2_5 = new BitVec(STRIDE_OBJ); 
 _o3 = new BitVec(STRIDE_OBJ); 
 _o4 = new BitVec(STRIDE_OBJ); 
 _o5 = new BitVec(STRIDE_OBJ); 
 _o6 = new BitVec(STRIDE_OBJ); 
 _o7 = new BitVec(STRIDE_OBJ); 
 _o8 = new BitVec(STRIDE_OBJ); 
 _o9 = new BitVec(STRIDE_OBJ); 
 _o10 = new BitVec(STRIDE_OBJ); 
 _o11 = new BitVec(STRIDE_OBJ); 
 _o12 = new BitVec(STRIDE_OBJ); 
 _m1 = new BitVec(STRIDE_MOV); 
 _m2 = new BitVec(STRIDE_MOV); 
 _m3 = new BitVec(STRIDE_MOV); 
  
 
    for (var i=0;i<level.height;i++) { 
      level.rowCellContents[i]=new BitVec(STRIDE_OBJ);         
    } 
    for (var i=0;i<level.width;i++) { 
      level.colCellContents[i]=new BitVec(STRIDE_OBJ);         
    } 
 
    for (var i=0;i<level.height;i++) { 
     level.rowCellContents_Movements[i]=new BitVec(STRIDE_MOV);       
    } 
    for (var i=0;i<level.width;i++) { 
     level.colCellContents_Movements[i]=new BitVec(STRIDE_MOV);       
    } 
 
    for (var i=0;i<level.n_tiles;i++) 
    { 
        level.rigidMovementAppliedMask[i]=new BitVec(STRIDE_MOV); 
        level.rigidGroupIndexMask[i]=new BitVec(STRIDE_MOV); 
    } 
} 
 
var messagetext=""; 
function restoreLevel(lev, snapCamera) { 
 var diffing = lev.hasOwnProperty("diff"); 
 
 oldflickscreendat=lev.oldflickscreendat.concat([]); 
 
 if (diffing){ 
  var index=0; 
  while (index<lev.dat.length){ 
   var start_index = lev.dat[index]; 
   var copy_length = lev.dat[index+1]; 
   if (copy_length===0){ 
    break;//tail of buffer is all 0s 
   } 
   for (j=0;j<copy_length;j++){ 
    level.objects[start_index+j]=lev.dat[index+2+j]; 
   } 
   index += 2 + copy_length; 
  } 
 } else {  
  level.objects = new Int32Array(lev.dat); 
 } 
 
 if (level.width !== lev.width || level.height !== lev.height) { 
  level.width = lev.width; 
  level.height = lev.height; 
  level.n_tiles = lev.width * lev.height; 
  RebuildLevelArrays(); 
  //regenerate all other stride-related stuff 
 } 
 else  
 { 
 // layercount doesn't change 
 
  for (var i=0;i<level.n_tiles;i++) { 
   level.movements[i]=0; 
   level.rigidMovementAppliedMask[i]=0; 
   level.rigidGroupIndexMask[i]=0; 
  }  
 
     for (var i=0;i<level.height;i++) { 
      var rcc = level.rowCellContents[i]; 
      rcc.setZero(); 
     } 
     for (var i=0;i<level.width;i++) { 
      var ccc = level.colCellContents[i]; 
      ccc.setZero(); 
     } 
 } 
 
    if (lev.cameraPositionTarget) { 
      cameraPositionTarget = Object.assign({}, lev.cameraPositionTarget); 
 
      if (snapCamera) { 
        cameraPosition = Object.assign({}, cameraPositionTarget) 
      } 
    } 
     
    if (state.metadata.runtime_metadata_twiddling !== undefined) { 
     state.metadata = deepClone(lev.metadata); 
     twiddleMetadataExtras(); 
    } 
 
    againing=false; 
    level.commandQueue=[]; 
    level.commandQueueSourceRules=[]; 
} 
 
var zoomscreen=false; 
var flickscreen=false; 
var smoothscreen=false; 
var screenwidth=0; 
var screenheight=0; 
 
//compresses 'before' into diff 
function consolidateDiff(before,after){ 
 if (before.width !== after.width || before.height!==after.height || before.dat.length!==after.dat.length){ 
  return before; 
 } 
 if (before.hasOwnProperty("diff")||after.hasOwnProperty("diff")){ 
  return before; 
 } 
 //only generate diffs if level size is bigger than this 
 if (before.dat.length<1024){ 
  return before; 
 } 
 //diff structure: repeating ( start,length, [ data ] ) 
 var result = new Int32Array(128); 
 var position=0; 
 var chain=false; 
 var chain_start_idx_in_diff=-1; 
 var before_dat = before.dat; 
 var after_dat = after.dat; 
 for (var i=0;i<before_dat.length;i++){ 
  if (chain===false){ 
   if (before_dat[i]!==after_dat[i]){ 
    chain=true; 
    chain_start_idx_in_diff = position; 
 
    if (result.length<position+4){ 
     var doubled = new Int32Array(2*result.length); 
     doubled.set(result); 
     result = doubled; 
    } 
 
    result[position+0]=i; 
    result[position+1]=1; 
    result[position+2]=before_dat[i]; 
    position+=3; 
   } 
  } else { 
   if (before_dat[i]!==after_dat[i]){ 
     
    if (position+1>=result.length){ 
     if (result.length<position+4){ 
      var doubled = new Int32Array(2*result.length); 
      doubled.set(result); 
      result = doubled; 
     }  
    } 
    result[chain_start_idx_in_diff+1]++; 
    result[position]=before_dat[i]; 
    position++; 
   } else { 
    chain=false; 
   } 
  } 
 } 
 return {   
  diff : true, 
  dat : result, 
  width : before.width, 
  height : before.height, 
  oldflickscreendat: before.oldflickscreendat 
 } 
} 
 
function addUndoState(state){ 
 backups.push(state); 
 if(backups.length>2 && !backups[backups.length-1].hasOwnProperty("diff")){ 
  backups[backups.length-3]=consolidateDiff(backups[backups.length-3],backups[backups.length-2]); 
 } 
} 
 
function DoRestart(force) { 
 if (restarting===true){ 
  return; 
 } 
 if (force!==true && ('norestart' in state.metadata)) { 
  return; 
 } 
 restarting=true; 
 if (force!==true) { 
  addUndoState(backupLevel()); 
 } 
 
 if (verbose_logging) { 
  consolePrint("--- restarting ---",true); 
 } 
 
 restoreLevel(restartTarget, true); 
 tryPlayRestartSound(); 
 
 if ('run_rules_on_level_start' in state.metadata) { 
     processInput(-1,true); 
  } 
   
  twiddleMetadataExtras(); 
  
 level.commandQueue=[]; 
 level.commandQueueSourceRules=[]; 
 restarting=false; 
} 
 
function backupDiffers(){ 
 if (backups.length==0){ 
  return true; 
 } 
 var bak = backups[backups.length-1]; 
 
 if (bak.hasOwnProperty("diff")){ 
  return bak.dat.length!==0 && bak.dat[1]!==0;//if it's empty or if it's all 0s 
 } else { 
  for (var i=0;i<level.objects.length;i++) { 
   if (level.objects[i]!==bak.dat[i]) { 
    return true; 
   } 
  } 
  return false; 
 } 
} 
 
function DoUndo(force,ignoreDuplicates) { 
  if ((!levelEditorOpened)&&('noundo' in state.metadata && force!==true)) { 
    return; 
  } 
  if (verbose_logging) { 
    consolePrint("--- undoing ---",true); 
  } 
 
  if (ignoreDuplicates){ 
    while (backupDiffers()==false){ 
      backups.pop(); 
    } 
  } 
 
  if (backups.length>0) { 
    var torestore = backups[backups.length-1]; 
    restoreLevel(torestore); 
    backups = backups.splice(0,backups.length-1); 
    if (! force) { 
      tryPlayUndoSound(); 
    } 
  } 
} 
 
function getPlayerPositions() { 
    var result=[]; 
    var playerMask = state.playerMask; 
    for (i=0;i<level.n_tiles;i++) { 
        level.getCellInto(i,_o11); 
        if (playerMask.anyBitsInCommon(_o11)) { 
            result.push(i); 
        } 
    } 
    return result; 
} 
 
function getLayersOfMask(cellMask) { 
    var layers=[]; 
    for (var i=0;i<state.objectCount;i++) { 
        if (cellMask.get(i)) { 
            var n = state.idDict[i]; 
            var o = state.objects[n]; 
            layers.push(o.layer) 
        } 
    } 
    return layers; 
} 
 
function moveEntitiesAtIndex(positionIndex, entityMask, dirMask) { 
    var cellMask = level.getCell(positionIndex); 
    cellMask.iand(entityMask); 
    var layers = getLayersOfMask(cellMask); 
 
    var movementMask = level.getMovements(positionIndex); 
    for (var i=0;i<layers.length;i++) { 
      movementMask.ishiftor(dirMask, 5 * layers[i]); 
    } 
    level.setMovements(positionIndex, movementMask); 
 
 var colIndex=(positionIndex/level.height)|0; 
 var rowIndex=(positionIndex%level.height); 
 level.colCellContents_Movements[colIndex].ior(movementMask); 
 level.rowCellContents_Movements[rowIndex].ior(movementMask); 
 level.mapCellContents_Movements.ior(movementMask); 
} 
 
 
function startMovement(dir) { 
  var movedany=false; 
    var playerPositions = getPlayerPositions(); 
    for (var i=0;i<playerPositions.length;i++) { 
        var playerPosIndex = playerPositions[i]; 
        moveEntitiesAtIndex(playerPosIndex,state.playerMask,dir); 
    } 
    return playerPositions; 
} 
 
var dirMasksDelta = { 
     1:[0,-1],//up 
     2:[0,1],//'down'  :  
     4:[-1,0],//'left'  :  
     8:[1,0],//'right' :  
     15:[0,0],//'?' :  
     16:[0,0],//'action' :  
     3:[0,0]//'no' 
}; 
 
var dirMaskName = { 
     1:'up', 
     2:'down'  , 
     4:'left'  ,  
     8:'right',   
     15:'?' , 
     16:'action', 
     3:'no' 
}; 
 
var seedsToPlay_CanMove=[]; 
var seedsToPlay_CantMove=[]; 
 
function repositionEntitiesOnLayer(positionIndex,layer,dirMask)  
{ 
    var delta = dirMasksDelta[dirMask]; 
 
    var dx = delta[0]; 
    var dy = delta[1]; 
    var tx = ((positionIndex/level.height)|0); 
    var ty = ((positionIndex%level.height)); 
    var maxx = level.width-1; 
    var maxy = level.height-1; 
 
    if ( (tx===0&&dx<0) || (tx===maxx&&dx>0) || (ty===0&&dy<0) || (ty===maxy&&dy>0)) { 
      return false; 
    } 
 
    var targetIndex = (positionIndex+delta[1]+delta[0]*level.height); 
 
    var layerMask = state.layerMasks[layer]; 
    var targetMask = level.getCellInto(targetIndex,_o7); 
    var sourceMask = level.getCellInto(positionIndex,_o8); 
 
    if (layerMask.anyBitsInCommon(targetMask) && (dirMask!=16)) { 
        return false; 
    } 
 
  for (var i=0;i<state.sfx_MovementMasks.length;i++) { 
    var o = state.sfx_MovementMasks[i]; 
    var objectMask = o.objectMask; 
    if (objectMask.anyBitsInCommon(sourceMask)) { 
      var movementMask = level.getMovements(positionIndex); 
      var directionMask = o.directionMask; 
      if (movementMask.anyBitsInCommon(directionMask) && seedsToPlay_CanMove.indexOf(o.seed)===-1) { 
        seedsToPlay_CanMove.push(o.seed); 
      } 
    } 
  } 
 
    var movingEntities = sourceMask.clone(); 
    sourceMask.iclear(layerMask); 
    movingEntities.iand(layerMask); 
    targetMask.ior(movingEntities); 
 
    level.setCell(positionIndex, sourceMask); 
    level.setCell(targetIndex, targetMask); 
  
    var colIndex=(targetIndex/level.height)|0; 
 var rowIndex=(targetIndex%level.height); 
  
    level.colCellContents[colIndex].ior(movingEntities); 
    level.rowCellContents[rowIndex].ior(movingEntities); 
    level.mapCellContents.ior(movingEntities); 
 //corresponding movement stuff in setmovements 
    return true; 
} 
 
function repositionEntitiesAtCell(positionIndex) { 
    var movementMask = level.getMovements(positionIndex); 
    if (movementMask.iszero()) 
        return false; 
 
    var moved=false; 
    for (var layer=0;layer<level.layerCount;layer++) { 
        var layerMovement = movementMask.getshiftor(0x1f, 5*layer); 
        if (layerMovement!==0) { 
            var thismoved = repositionEntitiesOnLayer(positionIndex,layer,layerMovement); 
            if (thismoved) { 
                movementMask.ishiftclear(layerMovement, 5*layer); 
                moved = true; 
            } 
        } 
    } 
 
    level.setMovements(positionIndex, movementMask); 
 
    return moved; 
} 
 
 
function Level(lineNumber, width, height, layerCount, objects, section) { 
 this.lineNumber = lineNumber; 
 this.width = width; 
 this.height = height; 
 this.n_tiles = width * height; 
 this.objects = objects; 
 this.section = section; 
 this.layerCount = layerCount; 
 this.commandQueue = []; 
 this.commandQueueSourceRules = []; 
} 
 
Level.prototype.delta_index = function(direction) 
{ 
 const [dx, dy] = dirMasksDelta[direction] 
 return dx*this.height + dy 
} 
 
Level.prototype.clone = function() { 
 var clone = new Level(this.lineNumber, this.width, this.height, this.layerCount, null, this.section); 
 clone.objects = new Int32Array(this.objects); 
 return clone; 
} 
 
Level.prototype.getCell = function(index) { 
  return new BitVec(this.objects.subarray(index * STRIDE_OBJ, index * STRIDE_OBJ + STRIDE_OBJ)); 
} 
 
Level.prototype.getCellInto = function(index,targetarray) { 
  for (var i=0;i<STRIDE_OBJ;i++) { 
    targetarray.data[i]=this.objects[index*STRIDE_OBJ+i];  
  } 
  return targetarray; 
} 
 
Level.prototype.setCell = function(index, vec) { 
  for (var i = 0; i < vec.data.length; ++i) { 
    this.objects[index * STRIDE_OBJ + i] = vec.data[i]; 
  } 
} 
 
var _movementVecs; 
var _movementVecIndex=0; 
Level.prototype.getMovements = function(index) { 
  var _movementsVec=_movementVecs[_movementVecIndex]; 
  _movementVecIndex=(_movementVecIndex+1)%_movementVecs.length; 
 
  for (var i=0;i<STRIDE_MOV;i++) { 
    _movementsVec.data[i]=this.movements[index*STRIDE_MOV+i];  
  } 
  return _movementsVec; 
} 
 
Level.prototype.getMovementsInto = function(index,targetarray) { 
 var _movementsVec=targetarray; 
 
 for (var i=0;i<STRIDE_MOV;i++) { 
  _movementsVec.data[i]=this.movements[index*STRIDE_MOV+i];  
 } 
 return _movementsVec; 
} 
 
Level.prototype.setMovements = function(index, vec) { 
 for (var i = 0; i < vec.data.length; ++i) { 
  this.movements[index * STRIDE_MOV + i] = vec.data[i]; 
 } 
 
 var targetIndex = index*STRIDE_MOV + i; 
   
 //corresponding object stuff in repositionEntitiesOnLayer 
 var colIndex=(index/this.height)|0; 
 var rowIndex=(index%this.height); 
 level.colCellContents_Movements[colIndex].ior(vec); 
 level.rowCellContents_Movements[rowIndex].ior(vec); 
 level.mapCellContents_Movements.ior(vec); 
 
 
} 
 
var ellipsisPattern = ['ellipsis']; 
 
function BitVec(init) { 
  this.data = new Int32Array(init); 
  return this; 
} 
 
BitVec.prototype.cloneInto = function(target) { 
  for (var i=0;i<this.data.length;++i) { 
    target.data[i]=this.data[i]; 
  } 
  return target; 
} 
BitVec.prototype.clone = function() { 
  return new BitVec(this.data); 
} 
 
BitVec.prototype.iand = function(other) { 
  for (var i = 0; i < this.data.length; ++i) { 
    this.data[i] &= other.data[i]; 
  } 
} 
 
 
BitVec.prototype.inot = function() { 
 for (var i = 0; i < this.data.length; ++i) { 
  this.data[i] = ~this.data[i]; 
 } 
} 
 
BitVec.prototype.ior = function(other) { 
  for (var i = 0; i < this.data.length; ++i) { 
    this.data[i] |= other.data[i]; 
  } 
} 
 
BitVec.prototype.iclear = function(other) { 
  for (var i = 0; i < this.data.length; ++i) { 
    this.data[i] &= ~other.data[i]; 
  } 
} 
 
BitVec.prototype.ibitset = function(ind) { 
  this.data[ind>>5] |= 1 << (ind & 31); 
} 
 
BitVec.prototype.ibitclear = function(ind) { 
  this.data[ind>>5] &= ~(1 << (ind & 31)); 
} 
 
BitVec.prototype.get = function(ind) { 
  return (this.data[ind>>5] & 1 << (ind & 31)) !== 0; 
} 
 
BitVec.prototype.getshiftor = function(mask, shift) { 
  var toshift = shift & 31; 
  var ret = this.data[shift>>5] >>> (toshift); 
  if (toshift) { 
    ret |= this.data[(shift>>5)+1] << (32 - toshift); 
  } 
  return ret & mask; 
} 
 
BitVec.prototype.ishiftor = function(mask, shift) { 
  var toshift = shift&31; 
  var low = mask << toshift; 
  this.data[shift>>5] |= low; 
  if (toshift) { 
    var high = mask >> (32 - toshift); 
    this.data[(shift>>5)+1] |= high; 
  } 
} 
 
BitVec.prototype.ishiftclear = function(mask, shift) { 
  var toshift = shift & 31; 
  var low = mask << toshift; 
  this.data[shift>>5] &= ~low; 
  if (toshift){ 
    var high = mask >> (32 - (shift & 31)); 
    this.data[(shift>>5)+1] &= ~high; 
  } 
} 
 
BitVec.prototype.equals = function(other) { 
  if (this.data.length !== other.data.length) 
    return false; 
  for (var i = 0; i < this.data.length; ++i) { 
    if (this.data[i] !== other.data[i]) 
      return false; 
  } 
  return true; 
} 
 
BitVec.prototype.setZero = function() { 
  for (var i = 0; i < this.data.length; ++i) { 
    this.data[i]=0; 
  } 
} 
 
BitVec.prototype.iszero = function() { 
  for (var i = 0; i < this.data.length; ++i) { 
    if (this.data[i]) 
      return false; 
  } 
  return true; 
} 
 
BitVec.prototype.bitsSetInArray = function(arr) { 
  for (var i = 0; i < this.data.length; ++i) { 
    if ((this.data[i] & arr[i]) !== this.data[i]) { 
      return false; 
    } 
  } 
  return true; 
} 
 
BitVec.prototype.bitsClearInArray = function(arr) { 
  for (var i = 0; i < this.data.length; ++i) { 
    if (this.data[i] & arr[i]) { 
      return false; 
    } 
  } 
  return true; 
} 
 
BitVec.prototype.anyBitsInCommon = function(other) { 
  return !this.bitsClearInArray(other.data); 
} 
 
function Rule(rule) { 
 this.direction = rule[0];   /* direction rule scans in */ 
 this.patterns = rule[1];  /* lists of CellPatterns to match */ 
 this.hasReplacements = rule[2]; 
 this.lineNumber = rule[3];  /* rule source for debugging */ 
 this.isEllipsis = rule[4];  /* true if pattern has ellipsis */ 
 this.groupNumber = rule[5];  /* execution group number of rule */ 
 this.isRigid = rule[6]; 
 this.commands = rule[7];  /* cancel, restart, sfx, etc */ 
 this.isRandom = rule[8]; 
 this.cellRowMasks = rule[9]; 
  this.cellRowMasks_Movements = rule[10]; 
  this.isGlobal = rule[11]; 
 this.ruleMask = this.cellRowMasks.reduce( (acc, m) => { acc.ior(m); return acc }, new BitVec(STRIDE_OBJ) ); 
 
 /*I tried out doing a ruleMask_movements as well along the lines of the above, 
 but it didn't help at all - I guess because almost every tick there are movements  
 somewhere on the board - move filtering works well at a row/col level, but is pretty  
 useless (or worse than useless) on a boardwide level*/ 
 
 this.cellRowMatches = []; 
 for (var i=0;i<this.patterns.length;i++) { 
  this.cellRowMatches.push(this.generateCellRowMatchesFunction(this.patterns[i],this.isEllipsis[i])); 
 } 
 /* TODO: eliminate isRigid, groupNumber, isRandom 
 from this class by moving them up into a RuleGroup class */ 
} 
 
 
Rule.prototype.generateCellRowMatchesFunction = function(cellRow,hasEllipsis)  { 
 if (hasEllipsis==false) { 
  var cr_l = cellRow.length; 
 
  /* 
  hard substitute in the first one - if I substitute in all of them, firefox chokes. 
  */ 
  var fn = ""; 
  var mul = STRIDE_OBJ === 1 ? '' : '*'+STRIDE_OBJ;  
  for (var i = 0; i < STRIDE_OBJ; ++i) { 
   fn += 'var cellObjects' + i + ' = objects[i' + mul + (i ? '+'+i: '') + '];\n'; 
  } 
  mul = STRIDE_MOV === 1 ? '' : '*'+STRIDE_MOV; 
  for (var i = 0; i < STRIDE_MOV; ++i) { 
   fn += 'var cellMovements' + i + ' = movements[i' + mul + (i ? '+'+i: '') + '];\n'; 
  } 
  fn += "return "+cellRow[0].generateMatchString('0_');// cellRow[0].matches(i)"; 
  for (var cellIndex=1;cellIndex<cr_l;cellIndex++) { 
   fn+="&&cellRow["+cellIndex+"].matches(i+"+cellIndex+"*d, objects, movements)"; 
  } 
  fn+=";"; 
 
  if (fn in matchCache) { 
   return matchCache[fn]; 
  } 
  //console.log(fn.replace(/\s+/g, ' ')); 
  return matchCache[fn] = new Function("cellRow","i", 'd', 'objects', 'movements',fn); 
 } else { 
  var cr_l = cellRow.length; 
 
  var fn = "var result = [];\n" 
  fn += "if(cellRow[0].matches(i, objects, movements)"; 
  var cellIndex=1; 
  for (;cellRow[cellIndex]!==ellipsisPattern;cellIndex++) { 
   fn+="&&cellRow["+cellIndex+"].matches(i+"+cellIndex+"*d, objects, movements)"; 
  } 
  cellIndex++; 
  fn+=") {\n"; 
  fn+="\tfor (var k=kmin;k<kmax;k++) {\n" 
  fn+="\t\tif(cellRow["+cellIndex+"].matches((i+d*(k+"+(cellIndex-1)+")), objects, movements)"; 
  cellIndex++; 
  for (;cellIndex<cr_l;cellIndex++) { 
   fn+="&&cellRow["+cellIndex+"].matches((i+d*(k+"+(cellIndex-1)+")), objects, movements)";    
  } 
  fn+="){\n"; 
  fn+="\t\t\tresult.push([i,k]);\n"; 
  fn+="\t\t}\n" 
  fn+="\t}\n";     
  fn+="}\n";   
  fn+="return result;" 
 
 
  if (fn in matchCache) { 
   return matchCache[fn]; 
  } 
  //console.log(fn.replace(/\s+/g, ' ')); 
  return matchCache[fn] = new Function("cellRow","i","kmax","kmin", 'd', "objects", "movements",fn); 
 } 
//say cellRow has length 3, with a split in the middle 
/* 
function cellRowMatchesWildcardFunctionGenerate(direction,cellRow,i, maxk, mink) { 
  var result = []; 
  var matchfirsthalf = cellRow[0].matches(i); 
  if (matchfirsthalf) { 
    for (var k=mink;k<maxk;k++) { 
      if (cellRow[2].matches((i+d*(k+0)))) { 
        result.push([i,k]); 
      } 
    } 
  } 
  return result; 
} 
*/ 
   
 
} 
 
 
Rule.prototype.toJSON = function() { 
 /* match construction order for easy deserialization */ 
 return [ 
  this.direction, this.patterns, this.hasReplacements, this.lineNumber, this.isEllipsis, 
  this.groupNumber, this.isRigid, this.commands, this.isRandom, this.cellRowMasks, 
  this.cellRowMasks_Movements 
 ]; 
}; 
 
var STRIDE_OBJ = 1; 
var STRIDE_MOV = 1; 
 
function CellPattern(row) { 
  this.objectsPresent = row[0]; 
  this.objectsMissing = row[1]; 
  this.anyObjectsPresent = row[2]; 
  this.movementsPresent = row[3]; 
  this.movementsMissing = row[4]; 
  this.matches = this.generateMatchFunction(); 
  this.replacement = row[5]; 
}; 
 
function CellReplacement(row) { 
  this.objectsClear = row[0]; 
  this.objectsSet = row[1]; 
  this.movementsClear = row[2]; 
  this.movementsSet = row[3]; 
  this.movementsLayerMask = row[4]; 
  this.randomEntityMask = row[5]; 
  this.randomDirMask = row[6]; 
}; 
 
 
var matchCache = {}; 
 
 
 
CellPattern.prototype.generateMatchString = function() { 
  var fn = "(true"; 
  for (var i = 0; i < Math.max(STRIDE_OBJ, STRIDE_MOV); ++i) { 
    var co = 'cellObjects' + i; 
    var cm = 'cellMovements' + i; 
    var op = this.objectsPresent.data[i]; 
    var om = this.objectsMissing.data[i]; 
    var mp = this.movementsPresent.data[i]; 
    var mm = this.movementsMissing.data[i]; 
    if (op) { 
      if (op&(op-1)) 
        fn += '\t\t&& ((' + co + '&' + op + ')===' + op + ')\n'; 
      else 
        fn += '\t\t&& (' + co + '&' + op + ')\n'; 
    } 
    if (om) 
      fn += '\t\t&& !(' + co + '&' + om + ')\n'; 
    if (mp) { 
      if (mp&(mp-1)) 
        fn += '\t\t&& ((' + cm + '&' + mp + ')===' + mp + ')\n'; 
      else 
        fn += '\t\t&& (' + cm + '&' + mp + ')\n'; 
    } 
    if (mm) 
      fn += '\t\t&& !(' + cm + '&' + mm + ')\n'; 
  } 
  for (var j = 0; j < this.anyObjectsPresent.length; j++) { 
    fn += "\t\t&& (0"; 
    for (var i = 0; i < STRIDE_OBJ; ++i) { 
      var aop = this.anyObjectsPresent[j].data[i]; 
      if (aop) 
        fn += "|(cellObjects" + i + "&" + aop + ")"; 
    } 
    fn += ")"; 
  } 
  fn += '\t)'; 
  return fn; 
} 
 
CellPattern.prototype.generateMatchFunction = function() { 
 var i; 
 var fn = ''; 
 var mul = STRIDE_OBJ === 1 ? '' : '*'+STRIDE_OBJ;  
 for (var i = 0; i < STRIDE_OBJ; ++i) { 
  fn += '\tvar cellObjects' + i + ' = objects[i' + mul + (i ? '+'+i: '') + '];\n'; 
 } 
 mul = STRIDE_MOV === 1 ? '' : '*'+STRIDE_MOV; 
 for (var i = 0; i < STRIDE_MOV; ++i) { 
  fn += '\tvar cellMovements' + i + ' = movements[i' + mul + (i ? '+'+i: '') + '];\n'; 
 } 
 fn += "return " + this.generateMatchString()+';'; 
 if (fn in matchCache) { 
  return matchCache[fn]; 
 } 
 //console.log(fn.replace(/\s+/g, ' ')); 
 return matchCache[fn] = new Function("i", "objects", "movements", fn); 
} 
 
CellPattern.prototype.toJSON = function() { 
  return [ 
    this.movementMask, this.cellMask, this.nonExistenceMask, 
    this.moveNonExistenceMask, this.moveStationaryMask, this.randomDirOrEntityMask, 
    this.movementsToRemove 
  ]; 
}; 
 
var _o1,_o2,_o2_5,_o3,_o4,_o5,_o6,_o7,_o8,_o9,_o10,_o11,_o12; 
var _m1,_m2,_m3; 
 
CellPattern.prototype.replace = function(rule, currentIndex) { 
  var replace = this.replacement; 
 
  if (replace === null) { 
    return false; 
  } 
 
  var replace_RandomEntityMask = replace.randomEntityMask; 
  var replace_RandomDirMask = replace.randomDirMask; 
 
  var objectsSet = replace.objectsSet.cloneInto(_o1); 
  var objectsClear = replace.objectsClear.cloneInto(_o2); 
 
  var movementsSet = replace.movementsSet.cloneInto(_m1); 
  var movementsClear = replace.movementsClear.cloneInto(_m2); 
  movementsClear.ior(replace.movementsLayerMask); 
 
  if (!replace_RandomEntityMask.iszero()) { 
    var choices=[]; 
    for (var i=0;i<32*STRIDE_OBJ;i++) { 
      if (replace_RandomEntityMask.get(i)) { 
        choices.push(i); 
      } 
    } 
    var rand = choices[Math.floor(RandomGen.uniform() * choices.length)]; 
    var n = state.idDict[rand]; 
    var o = state.objects[n]; 
    objectsSet.ibitset(rand); 
    objectsClear.ior(state.layerMasks[o.layer]); 
    movementsClear.ishiftor(0x1f, 5 * o.layer); 
  } 
  if (!replace_RandomDirMask.iszero()) { 
    for (var layerIndex=0;layerIndex<level.layerCount;layerIndex++){ 
      if (replace_RandomDirMask.get(5*layerIndex)) { 
        var randomDir = Math.floor(RandomGen.uniform()*4); 
        movementsSet.ibitset(randomDir + 5 * layerIndex); 
      } 
    } 
  } 
   
  var curCellMask = level.getCellInto(currentIndex,_o2_5); 
  var curMovementMask = level.getMovements(currentIndex); 
 
  var oldCellMask = curCellMask.cloneInto(_o3); 
  var oldMovementMask = curMovementMask.cloneInto(_m3); 
 
  curCellMask.iclear(objectsClear); 
  curCellMask.ior(objectsSet); 
 
  curMovementMask.iclear(movementsClear); 
  curMovementMask.ior(movementsSet); 
 
  var rigidchange=false; 
  var curRigidGroupIndexMask =0; 
  var curRigidMovementAppliedMask =0; 
  if (rule.isRigid) { 
    var rigidGroupIndex = state.groupNumber_to_RigidGroupIndex[rule.groupNumber]; 
    rigidGroupIndex++;//don't forget to -- it when decoding :O 
    var rigidMask = new BitVec(STRIDE_MOV); 
    for (var layer = 0; layer < level.layerCount; layer++) { 
      rigidMask.ishiftor(rigidGroupIndex, layer * 5); 
    } 
    rigidMask.iand(replace.movementsLayerMask); 
    curRigidGroupIndexMask = level.rigidGroupIndexMask[currentIndex] || new BitVec(STRIDE_MOV); 
    curRigidMovementAppliedMask = level.rigidMovementAppliedMask[currentIndex] || new BitVec(STRIDE_MOV); 
 
    if (!rigidMask.bitsSetInArray(curRigidGroupIndexMask.data) && 
      !replace.movementsLayerMask.bitsSetInArray(curRigidMovementAppliedMask.data) ) { 
      curRigidGroupIndexMask.ior(rigidMask); 
      curRigidMovementAppliedMask.ior(replace.movementsLayerMask); 
      rigidchange=true; 
 
    } 
  } 
 
  var result = false; 
 
  //check if it's changed 
  if (!oldCellMask.equals(curCellMask) || !oldMovementMask.equals(curMovementMask) || rigidchange) {  
    result=true; 
    if (rigidchange) { 
      level.rigidGroupIndexMask[currentIndex] = curRigidGroupIndexMask; 
      level.rigidMovementAppliedMask[currentIndex] = curRigidMovementAppliedMask; 
    } 
 
  var created = curCellMask.cloneInto(_o4); 
  created.iclear(oldCellMask); 
  sfxCreateMask.ior(created); 
  var destroyed = oldCellMask.cloneInto(_o5); 
  destroyed.iclear(curCellMask); 
  sfxDestroyMask.ior(destroyed); 
 
  level.setCell(currentIndex, curCellMask); 
  level.setMovements(currentIndex, curMovementMask); 
 
  var colIndex=(currentIndex/level.height)|0; 
  var rowIndex=(currentIndex%level.height); 
  level.colCellContents[colIndex].ior(curCellMask); 
  level.rowCellContents[rowIndex].ior(curCellMask); 
  level.mapCellContents.ior(curCellMask); 
 
  level.colCellContents_Movements[colIndex].ior(curMovementMask); 
  level.rowCellContents_Movements[rowIndex].ior(curMovementMask); 
  level.mapCellContents_Movements.ior(curMovementMask); 
 
 } 
 
  return result; 
} 
 
 
//say cellRow has length 5, with a split in the middle 
/* 
function cellRowMatchesWildcardFunctionGenerate(direction,cellRow,i, maxk, mink) { 
  var result = []; 
  var matchfirsthalf = cellRow[0].matches(i)&&cellRow[1].matches((i+d)%level.n_tiles); 
  if (matchfirsthalf) { 
    for (var k=mink,kmaxk;k++) { 
      if (cellRow[2].matches((i+d*(k+0))%level.n_tiles)&&cellRow[2].matches((i+d*(k+1))%level.n_tiles)) { 
        result.push([i,k]); 
      } 
    } 
  } 
  return result; 
} 
*/ 
/* 
function DoesCellRowMatchWildCard(direction,cellRow,i,maxk,mink) { 
  if (mink === undefined) { 
    mink = 0; 
  } 
 
  var cellPattern = cellRow[0]; 
 
    //var result=[]; 
 
    if (cellPattern.matches(i)){ 
      var delta = dirMasksDelta[direction]; 
      var d0 = delta[0]*level.height; 
    var d1 = delta[1]; 
        var targetIndex = i; 
 
        for (var j=1;j<cellRow.length;j+=1) { 
            targetIndex = (targetIndex+d1+d0); 
 
            var cellPattern = cellRow[j] 
            if (cellPattern === ellipsisPattern) { 
              //BAM inner loop time 
              for (var k=mink;k<maxk;k++) { 
                var targetIndex2=targetIndex; 
                    targetIndex2 = (targetIndex2+(d1+d0)*(k)+level.n_tiles)%level.n_tiles; 
                for (var j2=j+1;j2<cellRow.length;j2++) { 
                    cellPattern = cellRow[j2]; 
              if (!cellPattern.matches(targetIndex2)) { 
                break; 
              } 
                        targetIndex2 = (targetIndex2+d1+d0); 
                } 
 
                if (j2>=cellRow.length) { 
                  return true; 
                    //result.push([i,k]); 
                } 
              } 
              break; 
            } else if (!cellPattern.matches(targetIndex)) { 
        break; 
            } 
        }                
    }   
    return false; 
} 
*/ 
//say cellRow has length 3 
/* 
CellRow Matches can be specialized to look something like: 
function cellRowMatchesFunctionGenerate(direction,cellRow,i) { 
  var delta = dirMasksDelta[direction]; 
  var d = delta[1]+delta[0]*level.height; 
  return cellRow[0].matches(i)&&cellRow[1].matches((i+d)%level.n_tiles)&&cellRow[2].matches((i+2*d)%level.n_tiles); 
} 
*/ 
/* 
function DoesCellRowMatch(direction,cellRow,i,k) { 
  var cellPattern = cellRow[0]; 
    if (cellPattern.matches(i)) { 
 
      var delta = dirMasksDelta[direction]; 
      var d0 = delta[0]*level.height; 
    var d1 = delta[1]; 
    var cr_l = cellRow.length; 
 
        var targetIndex = i; 
        for (var j=1;j<cr_l;j++) { 
            targetIndex = (targetIndex+d1+d0); 
            cellPattern = cellRow[j]; 
      if (cellPattern === ellipsisPattern) { 
          //only for once off verifications 
              targetIndex = (targetIndex+(d1+d0)*k);           
            } 
        if (!cellPattern.matches(targetIndex)) { 
                break; 
            } 
        }    
         
        if (j>=cellRow.length) { 
            return true; 
        } 
 
    }   
    return false; 
} 
*/ 
function matchCellRow(direction, cellRowMatch, cellRow, cellRowMask,cellRowMask_Movements,d, isGlobal) {  
 var result=[]; 
  
 if ((!cellRowMask.bitsSetInArray(level.mapCellContents.data))|| 
 (!cellRowMask_Movements.bitsSetInArray(level.mapCellContents_Movements.data))) { 
  return result; 
 } 
 
  if(isGlobal || state.metadata.local_radius === undefined){ 
    xmin=0; 
    xmax=level.width; 
    ymin=0; 
    ymax=level.height; 
  } 
  else{ 
    var localradius = parseInt(state.metadata.local_radius); 
    xmin=Math.max(0, (playerPositions[0]/level.height|0) - localradius); 
    xmax=Math.min(level.width, (playerPositions[0]/level.height|0) + localradius +1); 
    ymin=Math.max(0, playerPositions[0]%level.height - localradius); 
    ymax=Math.min(level.height, playerPositions[0]%level.height + localradius+1); 
 
  } 
 
    var len=cellRow.length; 
 
    switch(direction) { 
      case 1://up 
      { 
        ymin+=(len-1); 
        break; 
      } 
      case 2: //down  
      { 
      ymax-=(len-1); 
      break; 
      } 
      case 4: //left 
      { 
        xmin+=(len-1); 
        break; 
      } 
      case 8: //right 
    { 
      xmax-=(len-1);   
      break; 
    } 
      default: 
      { 
        window.console.log("EEEP "+direction); 
      } 
    } 
 
    var horizontal=direction>2; 
    if (horizontal) { 
  for (var y=ymin;y<ymax;y++) { 
   if (!cellRowMask.bitsSetInArray(level.rowCellContents[y].data)  
   || !cellRowMask_Movements.bitsSetInArray(level.rowCellContents_Movements[y].data)) { 
    continue; 
   } 
 
   for (var x=xmin;x<xmax;x++) { 
    var i = x*level.height+y; 
    if (cellRowMatch(cellRow,i,d, level.objects, level.movements)) 
    { 
     result.push(i); 
    } 
   } 
  } 
 } else { 
  for (var x=xmin;x<xmax;x++) { 
   if (!cellRowMask.bitsSetInArray(level.colCellContents[x].data) 
   || !cellRowMask_Movements.bitsSetInArray(level.colCellContents_Movements[x].data)) { 
    continue; 
   } 
 
   for (var y=ymin;y<ymax;y++) { 
    var i = x*level.height+y; 
    if (cellRowMatch( cellRow,i, d, level.objects, level.movements)) 
    { 
     result.push(i); 
    } 
   } 
  }   
 } 
 
  return result; 
} 
 
 
function matchCellRowWildCard(direction, cellRowMatch, cellRow,cellRowMask,cellRowMask_Movements,d) { 
 var result=[]; 
 if ((!cellRowMask.bitsSetInArray(level.mapCellContents.data)) 
 || (!cellRowMask_Movements.bitsSetInArray(level.mapCellContents_Movements.data))) { 
  return result; 
 } 
  
 var xmin=0; 
 var xmax=level.width; 
 var ymin=0; 
 var ymax=level.height; 
 
  var len=cellRow.length-1;//remove one to deal with wildcard 
    switch(direction) { 
      case 1://up 
      { 
        ymin+=(len-1); 
        break; 
      } 
      case 2: //down  
      { 
      ymax-=(len-1); 
      break; 
      } 
      case 4: //left 
      { 
        xmin+=(len-1); 
        break; 
      } 
      case 8: //right 
    { 
      xmax-=(len-1);   
      break; 
    } 
      default: 
      { 
        window.console.log("EEEP2 "+direction); 
      } 
    } 
 
 
 
    var horizontal=direction>2; 
    if (horizontal) { 
  for (var y=ymin;y<ymax;y++) { 
   if (!cellRowMask.bitsSetInArray(level.rowCellContents[y].data) 
   || !cellRowMask_Movements.bitsSetInArray(level.rowCellContents_Movements[y].data) ) { 
    continue; 
   } 
 
   for (var x=xmin;x<xmax;x++) { 
    var i = x*level.height+y; 
    var kmax; 
 
    if (direction === 4) { //left 
     kmax=x-len+2; 
    } else if (direction === 8) { //right 
     kmax=level.width-(x+len)+1;  
    } else { 
     window.console.log("EEEP2 "+direction);      
    } 
 
    result.push.apply(result, cellRowMatch(cellRow,i,kmax,0, d, level.objects, level.movements)); 
   } 
  } 
 } else { 
  for (var x=xmin;x<xmax;x++) { 
   if (!cellRowMask.bitsSetInArray(level.colCellContents[x].data) 
   || !cellRowMask_Movements.bitsSetInArray(level.colCellContents_Movements[x].data)) { 
    continue; 
   } 
 
   for (var y=ymin;y<ymax;y++) { 
    var i = x*level.height+y; 
    var kmax; 
 
 
        if (direction === 2) { // down 
          kmax=level.height-(y+len)+1; 
        } else if (direction === 1) { // up 
          kmax=y-len+2;          
        } else { 
          window.console.log("EEEP2 "+direction); 
        } 
        result.push.apply(result, cellRowMatch(cellRow,i,kmax,0, d, level.objects, level.movements)); 
      } 
    }    
  } 
 
  return result; 
} 
 
function generateTuples(lists) { 
    var tuples=[[]]; 
 
    for (var i=0;i<lists.length;i++) 
    { 
        var row = lists[i]; 
        var newtuples=[]; 
        for (var j=0;j<row.length;j++) { 
            var valtoappend = row[j]; 
            for (var k=0;k<tuples.length;k++) { 
                var tuple=tuples[k]; 
                var newtuple = tuple.concat([valtoappend]); 
                newtuples.push(newtuple); 
            } 
        } 
        tuples=newtuples; 
    } 
    return tuples; 
} 
 
 
Rule.prototype.findMatches = function() {  
 if ( ! this.ruleMask.bitsSetInArray(level.mapCellContents.data) ) 
  return []; 
 
 const d = level.delta_index(this.direction) 
 
 var matches=[]; 
 var cellRowMasks=this.cellRowMasks; 
 var cellRowMasks_Movements=this.cellRowMasks_Movements; 
    for (var cellRowIndex=0;cellRowIndex<this.patterns.length;cellRowIndex++) { 
        var cellRow = this.patterns[cellRowIndex]; 
        var matchFunction = this.cellRowMatches[cellRowIndex]; 
        if (this.isEllipsis[cellRowIndex]) {//if ellipsis      
         var match = matchCellRowWildCard(this.direction,matchFunction,cellRow,cellRowMasks[cellRowIndex],cellRowMasks_Movements[cellRowIndex],d);   
        } else { 
         var match = matchCellRow(this.direction,matchFunction,cellRow,cellRowMasks[cellRowIndex],cellRowMasks_Movements[cellRowIndex],d, this.isGlobal);                 
        } 
        if (match.length===0) { 
            return []; 
        } else { 
            matches.push(match); 
        } 
    } 
    return matches; 
}; 
 
Rule.prototype.directional = function(){ 
  //Check if other rules in its rulegroup with the same line number. 
  for (var i=0;i<state.rules.length;i++){ 
    var rg = state.rules[i]; 
    var copyCount=0; 
    for (var j=0;j<rg.length;j++){ 
      if (this.lineNumber===rg[j].lineNumber){ 
        copyCount++; 
      } 
      if (copyCount>1){ 
        return true; 
      } 
    } 
  } 
 
    return false; 
} 
 
Rule.prototype.applyAt = function(level,tuple,check,delta) { 
 var rule = this; 
 //have to double check they apply  
 //(cf test ellipsis bug: rule matches two candidates, first replacement invalidates second) 
 if (check) 
 { 
  for (var cellRowIndex=0; cellRowIndex<this.patterns.length; cellRowIndex++) 
  { 
   if (this.isEllipsis[cellRowIndex]) //if ellipsis 
   { 
    if ( this.cellRowMatches[cellRowIndex](this.patterns[cellRowIndex], tuple[cellRowIndex][0], tuple[cellRowIndex][1]+1, tuple[cellRowIndex][1], delta, level.objects, level.movements).length == 0 ) 
     return false 
   } 
   else if ( ! this.cellRowMatches[cellRowIndex](this.patterns[cellRowIndex], tuple[cellRowIndex], delta, level.objects, level.movements) ) 
    return false 
  } 
 } 
 
 
    var result=false; 
     
    //APPLY THE RULE 
    for (var cellRowIndex=0;cellRowIndex<rule.patterns.length;cellRowIndex++) { 
        var preRow = rule.patterns[cellRowIndex]; 
         
        var currentIndex = rule.isEllipsis[cellRowIndex] ? tuple[cellRowIndex][0] : tuple[cellRowIndex]; 
        for (var cellIndex=0;cellIndex<preRow.length;cellIndex++) { 
            var preCell = preRow[cellIndex]; 
 
            if (preCell === ellipsisPattern) { 
             var k = tuple[cellRowIndex][1]; 
             currentIndex += delta*k; 
             continue; 
            } 
 
            result = preCell.replace(rule, currentIndex) || result; 
 
            currentIndex += delta; 
        } 
    } 
 
  if (verbose_logging && result){ 
    var ruleDirection = dirMaskName[rule.direction]; 
    if (!rule.directional()){ 
      ruleDirection=""; 
    } 
 
  var inspect_ID =  addToDebugTimeline(level,rule.lineNumber); 
  var logString = `<font color="green">Rule <a onclick="jumpToLine(${rule.lineNumber});"  href="javascript:void(0);">${rule.lineNumber}</a> ${ruleDirection} applied.</font>`; 
  consolePrint(logString,false,rule.lineNumber,inspect_ID); 
   
 } 
 
    return result; 
}; 
 
Rule.prototype.tryApply = function(level) { 
 const delta = level.delta_index(this.direction); 
 
    //get all cellrow matches 
    var matches = this.findMatches(); 
    if (matches.length===0) { 
      return false; 
    } 
 
    var result=false;  
 if (this.hasReplacements) { 
     var tuples = generateTuples(matches); 
     for (var tupleIndex=0;tupleIndex<tuples.length;tupleIndex++) { 
         var tuple = tuples[tupleIndex]; 
         var shouldCheck=tupleIndex>0; 
         var success = this.applyAt(level,tuple,shouldCheck,delta); 
         result = success || result; 
     } 
 } 
 
    if (matches.length>0) { 
      this.queueCommands(); 
    } 
    return result; 
}; 
 
Rule.prototype.queueCommands = function() { 
 var commands = this.commands; 
  
 if (commands.length==0){ 
  return; 
 } 
 
 //commandQueue is an array of strings, message.commands is an array of array of strings (For messagetext parameter), so I search through them differently 
 var preexisting_cancel=level.commandQueue.indexOf("cancel")>=0; 
 var preexisting_restart=level.commandQueue.indexOf("restart")>=0; 
  
 var currule_cancel = false; 
 var currule_restart = false; 
 for (var i=0;i<commands.length;i++){ 
  var cmd = commands[i][0]; 
  if (cmd==="cancel"){ 
   currule_cancel=true; 
  } else if (cmd==="restart"){ 
   currule_restart=true; 
  } 
 } 
 
 //priority cancel > restart > everything else 
 //if cancel is the queue from other rules, ignore everything 
 if (preexisting_cancel){ 
  return; 
 } 
 //if restart is in the queue from other rules, only apply if there's a cancel present here 
 if (preexisting_restart && !currule_cancel){ 
  return; 
 } 
 
 //if you are writing a cancel or restart, clear the current queue 
 if (currule_cancel || currule_restart){ 
  level.commandQueue=[]; 
        level.commandQueueSourceRules=[]; 
  messagetext=""; 
 } 
 
 for(var i=0;i<commands.length;i++) { 
  var command=commands[i]; 
  var already=false; 
  if (level.commandQueue.indexOf(command[0])>=0) { 
   continue; 
  } 
  level.commandQueue.push(command[0]); 
  level.commandQueueSourceRules.push(this); 
 
  if (verbose_logging){ 
   var lineNumber = this.lineNumber; 
   var ruleDirection = dirMaskName[this.direction]; 
   var logString = '<font color="green">Rule <a onclick="jumpToLine(' + lineNumber.toString() + ');"  href="javascript:void(0);">' + lineNumber.toString() + '</a> triggers command '+command[0]+'.</font>'; 
   consolePrint(logString,false,lineNumber,null); 
  } 
 
    if (command[0]==='message') {      
      messagetext=command[1]; 
    } 
  
    if (command[0]==='goto') { 
      gotoLevel(command[1]); 
    } 
 
    if (state.metadata.runtime_metadata_twiddling !== undefined && twiddleable_params.includes(command[0])) { 
 
      value = command[1]; 
 
      if (value == "wipe") { 
        delete state.metadata[command[0]]; //value = undefined; 
        value = null; 
      } else if (value == "default") { 
        value = deepClone(state.default_metadata[command[0]]); 
      } 
 
      if (value != null) { 
        state.metadata[command[0]] = value; 
      } 
       
      if (command[0] === "zoomscreen" || command[0] === "flickscreen") { 
        twiddleMetaData(state, true); 
        canvasResize(); 
      } 
 
      if (command[0] === "smoothscreen") { 
        if (value !== undefined) { 
          twiddleMetaData(state, true); 
          initSmoothCamera() 
        } else { 
          smoothscreen = false; 
        } 
        canvasResize(); 
      } 
 
      twiddleMetadataExtras() 
 
      if (state.metadata.runtime_metadata_twiddling_debug !== undefined) { 
        var log = "Metadata twiddled: Flag "+command[0] + " set to " + value; 
        if (value != command[1]) { 
          log += " ("+command[1]+")" 
        } 
        consolePrintFromRule(log,this,true); 
      } 
    }    
  } 
}; 
 
function twiddleMetadataExtras() { 
  if (state.metadata.realtime_interval!==undefined) { 
    autotick=0; 
    autotickinterval=state.metadata.realtime_interval*1000; 
  } else { 
    autotick=0; 
    autotickinterval=0; 
  } 
 
  if (state.metadata.again_interval!==undefined) { 
    againinterval=state.metadata.again_interval*1000; 
  } else { 
    againinterval=150; 
  } 
 
  if (state.metadata.key_repeat_interval!==undefined) { 
    repeatinterval=state.metadata.key_repeat_interval*1000; 
  } else { 
    repeatinterval=150; 
  } 
 
  if ('background_color' in state.metadata) { 
    state.bgcolor=colorToHex(colorPalette,state.metadata.background_color); 
  } else { 
    state.bgcolor="#000000"; 
  } 
 
  if ('text_color' in state.metadata) { 
    state.fgcolor=colorToHex(colorPalette,state.metadata.text_color); 
  } else { 
    state.fgcolor="#FFFFFF"; 
  } 
} 
 
function showTempMessage() { 
 keybuffer=[]; 
 textMode=true; 
 titleScreen=false; 
 quittingMessageScreen=false; 
 messageselected=false; 
 ignoreNotJustPressedAction=true; 
 tryPlayShowMessageSound(); 
 drawMessageScreen(); 
 canvasResize(); 
} 
 
function processOutputCommands(commands) { 
 for (var i=0;i<commands.length;i++) { 
  var command = commands[i]; 
  if (command.charAt(1)==='f')  {//identifies sfxN 
   tryPlaySimpleSound(command); 
  } 
  if (unitTesting===false) { 
   if (command==='message') { 
    showTempMessage(); 
   } 
  } 
 } 
} 
 
function applyRandomRuleGroup(level,ruleGroup) { 
 var propagated=false; 
 
 var matches=[]; 
 for (var ruleIndex=0;ruleIndex<ruleGroup.length;ruleIndex++) { 
  var rule=ruleGroup[ruleIndex]; 
  var ruleMatches = rule.findMatches(); 
  if (ruleMatches.length>0) { 
      var tuples  = generateTuples(ruleMatches); 
      for (var j=0;j<tuples.length;j++) { 
       var tuple=tuples[j]; 
    matches.push([ruleIndex,tuple]); 
      } 
  }   
 } 
 
  if (matches.length===0) 
  { 
    return false; 
  }  
 
 var match = matches[Math.floor(RandomGen.uniform()*matches.length)]; 
 var ruleIndex=match[0]; 
 var rule=ruleGroup[ruleIndex]; 
 var tuple=match[1]; 
 var check=false; 
 const delta = level.delta_index(rule.direction) 
 var modified = rule.applyAt(level,tuple,check,delta); 
 
    rule.queueCommands(); 
 
  return modified; 
} 
 
 
function applyRuleGroup(ruleGroup) { 
 if (ruleGroup[0].isRandom) { 
  return applyRandomRuleGroup(level,ruleGroup); 
 } 
 
  var loopPropagated=false; 
    var propagated=true; 
    var loopcount=0; 
 var nothing_happened_counter = -1; 
    while(propagated) { 
      loopcount++; 
      if (loopcount>200)  
      { 
        logErrorCacheable("Got caught looping lots in a rule group :O",ruleGroup[0].lineNumber,true); 
        break; 
      } 
        propagated=false; 
 
        for (var ruleIndex=0;ruleIndex<ruleGroup.length;ruleIndex++) { 
            var rule = ruleGroup[ruleIndex];      
   if (rule.tryApply(level)){ 
    propagated=true; 
    nothing_happened_counter=0;//why am I resetting to 1 rather than 0? because I've just verified that applications of the current rule are exhausted 
   } else { 
    nothing_happened_counter++; 
   } 
   if ( nothing_happened_counter === ruleGroup.length) 
    break; 
        } 
        if (propagated) { 
         loopPropagated=true; 
    
   if (verbose_logging){ 
    debugger_turnIndex++; 
    addToDebugTimeline(level,-2);//pre-movement-applied debug state 
   } 
        } 
    } 
 
    return loopPropagated; 
} 
 
function applyRules(rules, loopPoint, startRuleGroupindex, bannedGroup){ 
    //for each rule 
    //try to match it 
 
    playerPositions = getPlayerPositions(); 
 
    //when we're going back in, let's loop, to be sure to be sure 
    var loopPropagated = startRuleGroupindex>0; 
    var loopCount = 0; 
    for (var ruleGroupIndex=startRuleGroupindex;ruleGroupIndex<rules.length;) { 
      if (bannedGroup && bannedGroup[ruleGroupIndex]) { 
        //do nothing 
      } else { 
        var ruleGroup=rules[ruleGroupIndex]; 
      loopPropagated = applyRuleGroup(ruleGroup) || loopPropagated; 
      } 
        if (loopPropagated && loopPoint[ruleGroupIndex]!==undefined) { 
         ruleGroupIndex = loopPoint[ruleGroupIndex]; 
         loopPropagated=false; 
         loopCount++; 
   if (loopCount > 200) { 
       var ruleGroup=rules[ruleGroupIndex]; 
       logErrorCacheable("got caught in an endless startloop...endloop vortex, escaping!", ruleGroup[0].lineNumber,true); 
       break; 
   } 
    
   if (verbose_logging){ 
    debugger_turnIndex++; 
    addToDebugTimeline(level,-2);//pre-movement-applied debug state 
   } 
        } else { 
         ruleGroupIndex++; 
         if (ruleGroupIndex===rules.length) { 
          if (loopPropagated && loopPoint[ruleGroupIndex]!==undefined) { 
           ruleGroupIndex = loopPoint[ruleGroupIndex]; 
           loopPropagated=false; 
           loopCount++; 
     if (loopCount > 200) { 
         var ruleGroup=rules[ruleGroupIndex]; 
         logErrorCacheable("got caught in an endless startloop...endloop vortex, escaping!", ruleGroup[0].lineNumber,true); 
         break; 
     } 
          }  
         } 
    
   if (verbose_logging){ 
    debugger_turnIndex++; 
    addToDebugTimeline(level,-2);//pre-movement-applied debug state 
   } 
        } 
    } 
} 
 
 
//if this returns!=null, need to go back and reprocess 
function resolveMovements(level, bannedGroup){ 
    var moved=true; 
    while(moved){ 
        moved=false; 
        for (var i=0;i<level.n_tiles;i++) { 
          moved = repositionEntitiesAtCell(i) || moved; 
        } 
    } 
    var doUndo=false; 
 
 for (var i=0;i<level.n_tiles;i++) { 
  var cellMask = level.getCellInto(i,_o6); 
  var movementMask = level.getMovements(i); 
  if (!movementMask.iszero()) { 
   var rigidMovementAppliedMask = level.rigidMovementAppliedMask[i]; 
   if (rigidMovementAppliedMask !== 0) { 
    movementMask.iand(rigidMovementAppliedMask); 
    if (!movementMask.iszero()) { 
     //find what layer was restricted 
     for (var j=0;j<level.layerCount;j++) { 
      var layerSection = movementMask.getshiftor(0x1f, 5*j); 
      if (layerSection!==0) { 
       //this is our layer! 
       var rigidGroupIndexMask = level.rigidGroupIndexMask[i]; 
       var rigidGroupIndex = rigidGroupIndexMask.getshiftor(0x1f, 5*j); 
       rigidGroupIndex--;//group indices start at zero, but are incremented for storing in the bitfield 
       var groupIndex = state.rigidGroupIndex_to_GroupIndex[rigidGroupIndex]; 
       bannedGroup[groupIndex]=true; 
       //backtrackTarget = rigidBackups[rigidGroupIndex]; 
       doUndo=true; 
       break; 
      } 
     } 
    } 
   } 
   for (var j=0;j<state.sfx_MovementFailureMasks.length;j++) { 
    var o = state.sfx_MovementFailureMasks[j]; 
    var objectMask = o.objectMask; 
    if (objectMask.anyBitsInCommon(cellMask)) { 
     var directionMask = o.directionMask; 
     if (movementMask.anyBitsInCommon(directionMask) && seedsToPlay_CantMove.indexOf(o.seed)===-1) { 
      seedsToPlay_CantMove.push(o.seed); 
     } 
    } 
   } 
     } 
 
     for (var j=0;j<STRIDE_MOV;j++) { 
      level.movements[j+i*STRIDE_MOV]=0; 
     } 
     level.rigidGroupIndexMask[i]=0; 
     level.rigidMovementAppliedMask[i]=0; 
    } 
    return doUndo; 
} 
 
var sfxCreateMask=null; 
var sfxDestroyMask=null; 
 
function calculateRowColMasks() { 
 for(var i=0;i<level.mapCellContents.length;i++) { 
  level.mapCellContents[i]=0; 
  level.mapCellContents_Movements[i]=0;  
 } 
 
 for (var i=0;i<level.width;i++) { 
  var ccc = level.colCellContents[i]; 
  ccc.setZero(); 
  var ccc_Movements = level.colCellContents_Movements[i]; 
  ccc_Movements.setZero(); 
 } 
 
 for (var i=0;i<level.height;i++) { 
  var rcc = level.rowCellContents[i]; 
  rcc.setZero(); 
  var rcc_Movements = level.rowCellContents_Movements[i]; 
  rcc_Movements.setZero(); 
 } 
 
 for (var i=0;i<level.width;i++) { 
  for (var j=0;j<level.height;j++) { 
   var index = j+i*level.height; 
   var cellContents=level.getCellInto(index,_o9); 
   level.mapCellContents.ior(cellContents); 
   level.rowCellContents[j].ior(cellContents); 
   level.colCellContents[i].ior(cellContents); 
 
    
   var mapCellContents_Movements=level.getMovementsInto(index,_m1); 
   level.mapCellContents_Movements.ior(mapCellContents_Movements); 
   level.rowCellContents_Movements[j].ior(mapCellContents_Movements); 
   level.colCellContents_Movements[i].ior(mapCellContents_Movements); 
  } 
 } 
} 
 
var playerPositions; 
var playerPositionsAtTurnStart; 
 
/* returns a bool indicating if anything changed */ 
function processInput(dir,dontDoWin,dontModify,bak) { 
 againing = false; 
 
var bak = backupLevel(); 
var inputindex=dir;   
   
  playerPositions= []; 
playerPositionsAtTurnStart = getPlayerPositions(); 
 
    if (dir<=5) { 
 
      if (verbose_logging) {  
        debugger_turnIndex++; 
        addToDebugTimeline(level,-2);//pre-movement-applied debug state 
      } 
 
     if (dir>=0 && dir<=4) { 
         switch(dir){ 
             case 0://up 
             { 
                 dir=parseInt('00001', 2);; 
                 break; 
             } 
             case 1://left 
             { 
                 dir=parseInt('00100', 2);; 
                 break; 
             } 
             case 2://down 
             { 
                 dir=parseInt('00010', 2);; 
                 break; 
             } 
             case 3://right 
             { 
                 dir=parseInt('01000', 2);; 
                 break; 
             } 
             case 4://action 
             { 
                 dir=parseInt('10000', 2);; 
                 break; 
          } 
         } 
         playerPositions = startMovement(dir); 
  } 
    
   
  if (verbose_logging) {  
   consolePrint('Applying rules'); 
 
   var inspect_ID = addToDebugTimeline(level,-1); 
     
    if (dir===-1) { 
     consolePrint(`Turn starts with no input.`,false,null,inspect_ID) 
    } else { 
    //  consolePrint('======================='); 
    consolePrint(`Turn starts with input of ${['up','left','down','right','action','mouse'][inputindex]}.`,false,null,inspect_ID); 
    } 
  } 
 
   
        bannedGroup = []; 
        level.commandQueue=[]; 
        level.commandQueueSourceRules=[]; 
        var startRuleGroupIndex=0; 
        var rigidloop=false; 
  const startState = { 
   objects: new Int32Array(level.objects), 
   movements: new Int32Array(level.movements), 
   rigidGroupIndexMask: level.rigidGroupIndexMask.concat([]), 
   rigidMovementAppliedMask: level.rigidMovementAppliedMask.concat([]), 
   commandQueue: [], 
   commandQueueSourceRules: [] 
  } 
     sfxCreateMask.setZero(); 
     sfxDestroyMask.setZero(); 
 
    seedsToPlay_CanMove=[]; 
    seedsToPlay_CantMove=[]; 
 
    calculateRowColMasks(); 
 
        var i=0; 
        do { 
        //not particularly elegant, but it'll do for now - should copy the world state and check 
        //after each iteration 
         rigidloop=false; 
         i++; 
          
 
 
         applyRules(state.rules, state.loopPoint, startRuleGroupIndex, bannedGroup); 
         var shouldUndo = resolveMovements(level,bannedGroup); 
 
         if (shouldUndo) { 
          rigidloop=true; 
 
    { 
     // trackback 
     consolePrint("Rigid movement application failed. Rolling back...") 
     //don't need to concat or anythign here, once something is restored it won't be used again. 
     level.objects = new Int32Array(startState.objects) 
     level.movements = new Int32Array(startState.movements) 
     level.rigidGroupIndexMask = startState.rigidGroupIndexMask.concat([]) 
     level.rigidMovementAppliedMask = startState.rigidMovementAppliedMask.concat([]) 
     // TODO: shouldn't we also save/restore the level data computed by level.calculateRowColMasks() ? 
     level.commandQueue = startState.commandQueue.concat([]) 
     level.commandQueueSourceRules = startState.commandQueueSourceRules.concat([]) 
     sfxCreateMask.setZero() 
     sfxDestroyMask.setZero() 
     // TODO: should 
 
    } 
 
    if (verbose_logging && rigidloop && i>0){     
     consolePrint('Relooping through rules because of rigid.'); 
       
     debugger_turnIndex++; 
     addToDebugTimeline(level,-2);//pre-movement-applied debug state 
    } 
 
          startRuleGroupIndex=0;//rigidGroupUndoDat.ruleGroupIndex+1; 
         } else { 
          if (verbose_logging){ 
 
     var eof_idx = debug_visualisation_array[debugger_turnIndex].length+1;//just need some number greater than any rule group 
     var inspect_ID = addToDebugTimeline(level,eof_idx); 
 
     consolePrint(`Processed movements.`,false,null,inspect_ID); 
      
     if (state.lateRules.length>0){ 
            
      debugger_turnIndex++; 
      addToDebugTimeline(level,-2);//pre-movement-applied debug state 
      
      consolePrint('Applying late rules'); 
     } 
    } 
          applyRules(state.lateRules, state.lateLoopPoint, 0); 
          startRuleGroupIndex=0; 
         } 
        } while (i < 250 && rigidloop); 
 
        if (i>=250) { 
          consolePrint("looped through 250 times, gave up. Too many loops!"); 
           
          applyRules(state.lateRules, state.lateLoopPoint, 0); 
          startRuleGroupIndex=0; 
           
          backups.push(bak); 
          DoUndo(true,false); 
          return false; 
        } 
 
 
        if (playerPositionsAtTurnStart.length>0 && state.metadata.require_player_movement!==undefined && dir >= 0) { 
         var somemoved=false; 
         for (var i=0;i<playerPositionsAtTurnStart.length;i++) { 
          var pos = playerPositionsAtTurnStart[i]; 
          var val = level.getCell(pos); 
          if (state.playerMask.bitsClearInArray(val.data)) { 
           somemoved=true; 
           break; 
          } 
         } 
         if (somemoved===false) { 
          if (verbose_logging){ 
        consolePrint('require_player_movement set, but no player movement detected, so cancelling turn.'); 
        consoleCacheDump(); 
    } 
          addUndoState(bak); 
          DoUndo(true,false); 
          return false; 
         } 
         //play player cantmove sounds here 
        } 
   
  /// Taken from zarawesome, thank you :) 
     if (level.commandQueue.indexOf('undo')>=0) { 
      if (verbose_logging) { 
       consoleCacheDump(); 
       consolePrint('UNDO command executed, undoing turn.',true); 
   } 
   messagetext = ""; 
      DoUndo(true,false); 
      return true; 
  } 
 
 
 
     if (level.commandQueue.indexOf('cancel')>=0) { 
      if (verbose_logging) {  
       consoleCacheDump(); 
       var r = level.commandQueueSourceRules[level.commandQueue.indexOf('cancel')]; 
       consolePrintFromRule('CANCEL command executed, cancelling turn.',r,true); 
   } 
   processOutputCommands(level.commandQueue); 
      addUndoState(bak); 
      DoUndo(true,false); 
      tryPlayCancelSound(); 
      return false; 
     }  
 
     if (level.commandQueue.indexOf('restart')>=0) { 
      if (verbose_logging) {  
       var r = level.commandQueueSourceRules[level.commandQueue.indexOf('restart')]; 
       consolePrintFromRule('RESTART command executed, reverting to restart state.',r); 
       consoleCacheDump(); 
   } 
   processOutputCommands(level.commandQueue); 
      addUndoState(bak); 
      DoRestart(true); 
      return true; 
  } 
   
  if (level.commandQueue.indexOf('quit')>=0) { 
   if (verbose_logging) {  
    var r = level.commandQueueSourceRules[level.commandQueue.indexOf('restart')]; 
    consolePrintFromRule('QUIT command executed, exiting level.',r); 
    consoleCacheDump(); 
   } 
   if (state.metadata.level_select !== undefined) { 
    gotoLevelSelectScreen(); 
   } else { 
    goToTitleScreen(); 
   } 
   messagetext = ""; 
   canvasResize();  
   return true; 
  } 
 
     if (dontModify && level.commandQueue.indexOf('win')>=0) { 
      return true; 
  } 
   
  var save_backup = true; 
  if(!winning && level.commandQueue.indexOf('nosave')>=0) { 
   if (verbose_logging) {  
    var r = level.commandQueueSourceRules[level.commandQueue.indexOf('nosave')]; 
    consolePrintFromRule('NOSAVE command executed, not storing current state to undo queue.',r); 
   } 
   save_backup = false; 
  } 
      
        var modified=false; 
     for (var i=0;i<level.objects.length;i++) { 
      if (level.objects[i]!==bak.dat[i]) { 
    if (dontModify) { 
           if (verbose_logging) { 
            consoleCacheDump(); 
           } 
           addUndoState(bak); 
           DoUndo(true,false); 
     return true; 
    } else { 
     if (dir!==-1 && save_backup) { 
         addUndoState(bak); 
        } 
        modified=true; 
        updateCameraPositionTarget(); 
       } 
       break; 
      } 
     } 
 
  if (dontModify && level.commandQueue.indexOf('win')>=0) {  
      return true;  
  } 
   
  if (dontModify) {   
      if (verbose_logging) { 
       consoleCacheDump(); 
      } 
   return false; 
  } 
 
        for (var i=0;i<seedsToPlay_CantMove.length;i++) { 
            playSound(seedsToPlay_CantMove[i]); 
        } 
 
        for (var i=0;i<seedsToPlay_CanMove.length;i++) { 
            playSound(seedsToPlay_CanMove[i]); 
        } 
 
        for (var i=0;i<state.sfx_CreationMasks.length;i++) { 
          var entry = state.sfx_CreationMasks[i]; 
          if (sfxCreateMask.anyBitsInCommon(entry.objectMask)) { 
            playSound(entry.seed); 
          } 
        } 
 
        for (var i=0;i<state.sfx_DestructionMasks.length;i++) { 
          var entry = state.sfx_DestructionMasks[i]; 
          if (sfxDestroyMask.anyBitsInCommon(entry.objectMask)) { 
            playSound(entry.seed); 
          } 
        } 
 
     processOutputCommands(level.commandQueue); 
 
     if (textMode===false) { 
      if (verbose_logging) {  
       consolePrint('Checking win conditions.'); 
   } 
   if (dontDoWin===undefined){ 
    dontDoWin = false; 
   } 
      checkWin( dontDoWin ); 
     } 
 
     if (!winning) { 
   if (level.commandQueue.indexOf('checkpoint')>=0) { 
       if (verbose_logging) {  
        var r = level.commandQueueSourceRules[level.commandQueue.indexOf('checkpoint')]; 
        consolePrintFromRule('CHECKPOINT command executed, saving current state to the restart state.',r); 
    } 
    restartTarget=level4Serialization(); 
    hasUsedCheckpoint=true; 
    var backupStr = JSON.stringify(restartTarget); 
    storage_set(document.URL+'_checkpoint',backupStr); 
    storage_set(document.URL,curlevel);     
   }   
 
      if (level.commandQueue.indexOf('again')>=0 && modified) { 
 
       var r = level.commandQueueSourceRules[level.commandQueue.indexOf('again')]; 
 
       //first have to verify that something's changed 
       var old_verbose_logging=verbose_logging; 
       var oldmessagetext = messagetext; 
       verbose_logging=false; 
       if (processInput(-1,true,true)) { 
        verbose_logging=old_verbose_logging; 
 
        if (verbose_logging) {  
         consolePrintFromRule('AGAIN command executed, with changes detected - will execute another turn.',r); 
     } 
 
        againing=true; 
        timer=0; 
       } else {        
        verbose_logging=old_verbose_logging; 
     if (verbose_logging) {  
      consolePrintFromRule('AGAIN command not executed, it wouldn\'t make any changes.',r); 
     } 
       } 
       verbose_logging=old_verbose_logging; 
       messagetext = oldmessagetext; 
      }    
  } 
   
  if (verbose_logging) {  
   consolePrint(`Turn complete`);     
  } 
   
     level.commandQueue=[]; 
     level.commandQueueSourceRules=[]; 
 
    } 
 
  if (verbose_logging) { 
    consoleCacheDump(); 
  } 
 
  if (winning) { 
    againing=false; 
  } 
 
  return modified; 
} 
 
function checkWin(dontDoWin) { 
 
  if (levelEditorOpened) { 
    dontDoWin=true; 
  } 
 
 if (level.commandQueue.indexOf('win')>=0) { 
  if (runrulesonlevelstart_phase){ 
   consolePrint("Win Condition Satisfied (However this is in the run_rules_on_level_start rule pass, so I'm going to ignore it for you.  Why would you want to complete a level before it's already started?!)");   
  } else { 
   consolePrint("Win Condition Satisfied"); 
  } 
  if(!dontDoWin){ 
   DoWin(); 
  } 
  return; 
 } 
 
 var won= false; 
 if (state.winconditions.length>0)  { 
  var passed=true; 
  for (var wcIndex=0;wcIndex<state.winconditions.length;wcIndex++) { 
   var wincondition = state.winconditions[wcIndex]; 
   var filter1 = wincondition[1]; 
   var filter2 = wincondition[2]; 
   var rulePassed=true; 
   switch(wincondition[0]) { 
    case -1://NO 
    { 
     for (var i=0;i<level.n_tiles;i++) { 
      var cell = level.getCellInto(i,_o10); 
      if ( (!filter1.bitsClearInArray(cell.data)) &&   
        (!filter2.bitsClearInArray(cell.data)) ) { 
       rulePassed=false; 
       break; 
      } 
     } 
 
          break; 
        } 
        case 0://SOME 
        { 
          var passedTest=false; 
          for (var i=0;i<level.n_tiles;i++) { 
            var cell = level.getCellInto(i,_o10); 
            if ( (!filter1.bitsClearInArray(cell.data)) &&   
               (!filter2.bitsClearInArray(cell.data)) ) { 
              passedTest=true; 
              break; 
            } 
          } 
          if (passedTest===false) { 
            rulePassed=false; 
          } 
          break; 
        } 
        case 1://ALL 
        { 
          for (var i=0;i<level.n_tiles;i++) { 
            var cell = level.getCellInto(i,_o10); 
            if ( (!filter1.bitsClearInArray(cell.data)) &&   
               (filter2.bitsClearInArray(cell.data)) ) { 
              rulePassed=false; 
              break; 
            } 
          } 
          break; 
        } 
      } 
      if (rulePassed===false) { 
        passed=false; 
      } 
    } 
    won=passed; 
  } 
 
 if (won) { 
  if (runrulesonlevelstart_phase){ 
   consolePrint("Win Condition Satisfied (However this is in the run_rules_on_level_start rule pass, so I'm going to ignore it for you.  Why would you want to complete a level before it's already started?!)");   
  } else { 
   consolePrint("Win Condition Satisfied"); 
  } 
  if (!dontDoWin){ 
   DoWin(); 
  } 
 } 
} 
 
function DoWin() { 
  if (winning) { 
    return; 
  } 
  againing=false; 
  tryPlayEndLevelSound(); 
  if (unitTesting) { 
    nextLevel(); 
    return; 
  } 
 
  winning=true; 
  timer=0; 
} 
 
/* 
//this function isn't valid after refactoring, but also isn't used. 
function anyMovements() {  
    for (var i=0;i<level.movementMask.length;i++) { 
        if (level.movementMask[i]!==0) { 
          return true; 
        } 
    } 
    return false; 
}*/ 
 
function nextLevel() { 
    againing=false; 
 messagetext=""; 
 if (state && state.levels && (curlevel>state.levels.length) ){ 
  curlevel=state.levels.length-1; 
 } 
   
  ignoreNotJustPressedAction=true; 
 if (titleScreen && titleMode <= 1) { 
  if(isContinueOptionSelected()) { 
   // continue 
   loadLevelFromStateOrTarget(); 
  } else if(isNewGameOptionSelected()) { 
   // new game 
   curlevel=0; 
   curlevelTarget=null; 
 
   if (state.metadata.level_select === undefined) { 
    clearLocalStorage(); 
   } 
 
   loadLevelFromStateOrTarget(); 
  } else if(isLevelSelectOptionSelected()) { 
   // level select 
   titleSelection = 0; 
   gotoLevelSelectScreen(); 
  } else { 
   // settings 
   // TODO 
  } 
 } else { 
  if (hasUsedCheckpoint){ 
   curlevelTarget=null; 
   hasUsedCheckpoint=false; 
  } 
 
  if (curlevel<(state.levels.length-1)) { 
   var skip = false; 
   var curSection = state.levels[Number(curlevel)].section; 
   var nextSection = state.levels[Number(curlevel)+1].section; 
   if(nextSection != curSection) { 
    setSectionSolved(state.levels[Number(curlevel)].section); 
     
    if(solvedSections.length == state.sections.length && state.winSection != undefined) { 
     curlevel = state.winSection.firstLevel - 1; // it's gonna be increased to match few lines below 
    } else if (nextSection == "__WIN__") { 
     gotoLevelSelectScreen(); 
     skip = true; 
    }   
   } 
 
   if(!skip) { 
    curlevel++; 
    curlevelTarget=null; 
    textMode=false; 
    titleScreen=false; 
    quittingMessageScreen=false; 
    messageselected=false; 
  
    loadLevelFromStateOrTarget(); 
   } 
  } else { 
   if(solvedSections.length == state.sections.length) { 
    if(state.metadata["level_select"] === undefined) { 
     // solved all 
          try{ 
            storage_remove(document.URL); 
            storage_remove(document.URL+'_checkpoint');     
          } catch(ex){ 
          } 
      
     curlevel=0; 
     curlevelTarget=null; 
     goToTitleScreen(); 
    } else { 
     gotoLevelSelectScreen(); 
    } 
     
    tryPlayEndGameSound();  
   } else { 
    if(state.levels[Number(curlevel)].section != null) { 
     setSectionSolved(state.levels[Number(curlevel)].section); 
    } 
    gotoLevelSelectScreen(); 
   } 
  }   
  //continue existing game 
 } 
 
 updateLocalStorage(); 
 resetFlickDat(); 
 canvasResize();  
} 
 
function loadLevelFromStateOrTarget() { 
 if (curlevelTarget!==null){    
  loadLevelFromStateTarget(state,curlevel,curlevelTarget); 
 } else { 
  loadLevelFromState(state,curlevel); 
 } 
} 
 
function goToTitleScreen(){ 
    againing=false; 
 messagetext=""; 
 titleScreen=true; 
 textMode=true; 
 doSetupTitleScreenLevelContinue(); 
  //titleSelection=showContinueOptionOnTitleScreen()?1:0; 
   
  state.metadata = deepClone(state.default_metadata); 
  twiddleMetadataExtras(); 
 
  if (canvas!==null){//otherwise triggers error in cat bastard test 
  regenSpriteImages(); 
 } 
 
 generateTitleScreen(); 
} 
 
function resetFlickDat() { 
 if (state!==undefined && state.metadata.flickscreen!==undefined){ 
  oldflickscreendat=[0,0,Math.min(state.metadata.flickscreen[0],level.width),Math.min(state.metadata.flickscreen[1],level.height)]; 
 } 
} 
 
function updateLocalStorage() { 
 try { 
   
  storage_set(document.URL,curlevel); 
  if (curlevelTarget!==null){ 
   restartTarget=level4Serialization(); 
   var backupStr = JSON.stringify(restartTarget); 
   storage_set(document.URL+'_checkpoint',backupStr); 
  } else { 
   storage_remove(document.URL+"_checkpoint"); 
  }   
   
 } catch (ex) { 
  } 
} 
 
function setSectionSolved(section) { 
 if(section == null || section == undefined) { 
  return; 
 } 
 
 if(section.name == "__WIN__") { 
  return; 
 } 
 
 if(solvedSections.indexOf(section) >= 0) { 
  return; 
 } 
 
 try { 
  if(!!window.localStorage) { 
   solvedSections.push(section); 
   localStorage.setItem(document.URL + "_sections", JSON.stringify(solvedSections)); 
  } 
 } catch(ex) { } 
} 
 
function clearLocalStorage() { 
 curlevel = 0; 
 curlevelTarget = null; 
 solvedSections = []; 
 
 try { 
  if (!!window.localStorage) { 
   localStorage.removeItem(document.URL); 
   localStorage.removeItem(document.URL+'_checkpoint'); 
   localStorage.removeItem(document.URL+'_sections'); 
  } 
 } catch(ex){ } 
} 
 
var cameraPositionTarget = null; 
 
var cameraPosition = { 
  x: 0, 
  y: 0 
}; 
 
function initSmoothCamera() { 
    if (state===undefined || state.metadata.smoothscreen===undefined) { 
        return; 
    } 
 
    screenwidth=state.metadata.smoothscreen.screenSize.width; 
    screenheight=state.metadata.smoothscreen.screenSize.height; 
 
    var boundarySize = state.metadata.smoothscreen.boundarySize; 
    var flick = state.metadata.smoothscreen.flick; 
 
    var playerPositions = getPlayerPositions(); 
    if (playerPositions.length>0) { 
        var playerPosition = { 
            x: (playerPositions[0]/(level.height))|0, 
            y: (playerPositions[0]%level.height)|0 
        }; 
 
        cameraPositionTarget = { 
            x: flick 
              ? getFlickCameraPosition(playerPosition.x, level.width, screenwidth, boundarySize.width) 
              : getCameraPosition(playerPosition.x, level.width, screenwidth), 
            y: flick 
              ? getFlickCameraPosition(playerPosition.y, level.height, screenheight, boundarySize.height) 
              : getCameraPosition(playerPosition.y, level.height, screenheight) 
        }; 
 
        cameraPosition.x = cameraPositionTarget.x; 
        cameraPosition.y = cameraPositionTarget.y; 
    } 
} 
 
function getCameraPosition (targetPosition, levelDimension, screenDimension) { 
    return Math.min( 
        Math.max(targetPosition, Math.floor(screenDimension / 2)), 
        levelDimension - Math.ceil(screenDimension / 2) 
    ); 
} 
 
function getFlickCameraPosition (targetPosition, levelDimension, screenDimension, boundaryDimension) { 
    var flickGridOffset = (Math.floor(screenDimension / 2) - Math.floor(boundaryDimension / 2)); 
    var flickGridPlayerPosition = targetPosition - flickGridOffset; 
    var flickGridPlayerCell = Math.floor(flickGridPlayerPosition / boundaryDimension); 
    var maxFlickGridCell = Math.floor((levelDimension - Math.ceil(screenDimension / 2) - Math.floor(boundaryDimension / 2) - flickGridOffset) / boundaryDimension); 
 
    return Math.min(Math.max(flickGridPlayerCell, 0), maxFlickGridCell) * boundaryDimension + Math.floor(screenDimension / 2); 
} 
 
function updateCameraPositionTarget() { 
    var smoothscreenConfig = state.metadata.smoothscreen; 
    var playerPositions = getPlayerPositions(); 
 
    if (!smoothscreenConfig || playerPositions.length === 0) { 
        return 
    } 
 
    var playerPosition = { 
        x: (playerPositions[0]/(level.height))|0, 
        y: (playerPositions[0]%level.height)|0 
    }; 
 
    ['x', 'y'].forEach(function (coord) { 
        var screenDimension = coord === 'x' ? screenwidth : screenheight; 
 
        var dimensionName = coord === 'x' ? 'width' : 'height'; 
        var levelDimension = level[dimensionName]; 
        var boundaryDimension = smoothscreenConfig.boundarySize[dimensionName]; 
 
        var playerVector = playerPosition[coord] - cameraPositionTarget[coord]; 
        var direction = Math.sign(playerVector); 
        var boundaryVector = direction > 0 
          ? Math.ceil(boundaryDimension / 2) 
          : -(Math.floor(boundaryDimension / 2) + 1); 
 
        if (Math.abs(playerVector) - Math.abs(boundaryVector) >= 0) { 
            cameraPositionTarget[coord] = smoothscreenConfig.flick 
              ? getFlickCameraPosition(playerPosition[coord], levelDimension, screenDimension, boundaryDimension) 
              : getCameraPosition(playerPosition[coord] - boundaryVector + direction, levelDimension, screenDimension); 
        } 
    }) 
}</script> <script>function logErrorCacheable(e,n,r){if(compiling||r){if(void 0===n)return logErrorNoLine(e,r);var t='<a onclick="jumpToLine('+n.toString()+');"  href="javascript:void(0);"><span class="errorTextLineNumber"> line '+n.toString()+'</span></a> : <span class="errorText">'+e+"</span>";errorStrings.indexOf(t)>=0&&!r||(consolePrint(t),errorStrings.push(t),errorCount++)}}function logError(e,n,r){if(compiling||r){if(void 0===n)return logErrorNoLine(e,r);var t='<a onclick="jumpToLine('+n.toString()+');"  href="javascript:void(0);"><span class="errorTextLineNumber"> line '+n.toString()+'</span></a> : <span class="errorText">'+e+"</span>";errorStrings.indexOf(t)>=0&&!r||(consolePrint(t,!0),errorStrings.push(t),errorCount++)}}function logWarning(e,n,r){if(compiling||r){if(void 0===n)return logWarningNoLine(e,r);var t='<a onclick="jumpToLine('+n.toString()+');"  href="javascript:void(0);"><span class="errorTextLineNumber"> line '+n.toString()+'</span></a> : <span class="warningText">'+e+"</span>";errorStrings.indexOf(t)>=0&&!r||(consolePrint(t,!0),errorStrings.push(t))}}function logWarningNoLine(e,n){if(compiling||n){var r='<span class="warningText">'+e+"</span>";errorStrings.indexOf(r)>=0&&!n||(consolePrint(r,!0),errorStrings.push(r)),errorCount++}}function logErrorNoLine(e,n){if(compiling||n){var r='<span class="errorText">'+e+"</span>";errorStrings.indexOf(r)>=0&&!n||(consolePrint(r,!0),errorStrings.push(r)),errorCount++}}function logBetaMessage(e,n){if(compiling||n){var r='<span class="betaText">'+e+"</span>";errorStrings.indexOf(r)>=0&&!n||(consoleError(r),errorStrings.push(r))}}function blankLineHandle(e){"levels"===e.section?e.levels[e.levels.length-1].length>0&&e.levels.push([]):"objects"===e.section&&(e.objects_section=0)}var compiling=!1,errorStrings=[],errorCount=0,twiddleable_params=["background_color","text_color","key_repeat_interval","realtime_interval","again_interval","flickscreen","zoomscreen","smoothscreen","noundo","norestart"];"function"!=typeof Object.assign&&function(){Object.assign=function(e){"use strict";if(void 0===e||null===e)throw new TypeError("Cannot convert undefined or null to object");for(var n=Object(e),r=1;r<arguments.length;r++){var t=arguments[r];if(void 0!==t&&null!==t)for(var o in t)t.hasOwnProperty(o)&&(n[o]=t[o])}return n}}();var codeMirrorFn=function(){"use strict";function e(e,n){if(void 0!==e.objects[n])return logError('Object "'+n+'" defined multiple times.',e.lineNumber),"ERROR";for(var r=0;r<e.legend_synonyms.length;r++){var t=e.legend_synonyms[r];t[0]==n&&logError('Name "'+n+'" already in use.',e.lineNumber)}for(var r=0;r<e.legend_aggregates.length;r++){var t=e.legend_aggregates[r];t[0]==n&&logError('Name "'+n+'" already in use.',e.lineNumber)}for(var r=0;r<e.legend_properties.length;r++){var t=e.legend_properties[r];t[0]==n&&logError('Name "'+n+'" already in use.',e.lineNumber)}}var n=["objects","legend","sounds","collisionlayers","rules","winconditions","levels"],r=["sfx0","sfx1","sfx2","sfx3","sfx4","sfx5","sfx6","sfx7","sfx8","sfx9","sfx10","cancel","checkpoint","restart","win","message","again","undo","nosave","quit","zoomscreen","flickscreen","smoothscreen","again_interval","realtime_interval","key_repeat_interval","noundo","norestart","background_color","text_color","goto"],t=/[\p{L}\p{N}_]+[\p{Z}\s]*/u,o=/\d+\b/,s=/(objects|collisionlayers|legend|sounds|rules|winconditions|levels)(?![\p{L}\p{N}_])[\p{Z}\s]*/u,i=/[\=]+/,a=/[^\(]+/,l=/[ \,]*/,c=/(move|action|create|destroy|cantmove|undo|restart|titlescreen|startgame|cancel|endgame|startlevel|endlevel|showmessage|closemessage|sfx0|sfx1|sfx2|sfx3|sfx4|sfx5|sfx6|sfx7|sfx8|sfx9|sfx10)\b[\p{Z}\s]*/u,u=/^(action|up|down|left|right|\^|v|\<|\>|moving|stationary|parallel|perpendicular|horizontal|orthogonal|vertical|no|randomdir|random)$/,g=/^(startloop|endloop)$/,d=/^(up|down|left|right|horizontal|vertical|orthogonal|late|rigid)$/,m=/[\p{Z}\s]*(up|down|left|right|horizontal|vertical|orthogonal)(?![\p{L}\p{N}_])[\p{Z}\s]*/u,p=/^(all|any|no|some)$/,f=["title","author","homepage","background_color","text_color","key_repeat_interval","realtime_interval","again_interval","flickscreen","zoomscreen","smoothscreen","color_palette","youtube","sprite_size","level_select_unlocked_ahead","level_select_solve_symbol","custom_font","mouse_left","mouse_drag","mouse_right","mouse_rdrag","mouse_up","mouse_rup","local_radius","font_size"],h=["run_rules_on_level_start","norepeat_action","require_player_movement","debug","verbose_logging","throttle_movement","noundo","noaction","norestart","scanline","case_sensitive","level_select","continue_is_level_select","level_select_lock","settings","runtime_metadata_twiddling","runtime_metadata_twiddling_debug","smoothscreen_debug"],v=["checkpoint","objects","collisionlayers","legend","sounds","rules","...","winconditions","levels","|","[","]","up","down","left","right","late","rigid","^","v",">","<","no","randomdir","random","horizontal","vertical","any","all","no","some","moving","stationary","parallel","perpendicular","action","nosave","message","global","zoomscreen","flickscreen","smoothscreen","noundo","norestart","background_color","text_color","goto"];return{copyState:function(e){var n={};for(var r in e.objects)if(e.objects.hasOwnProperty(r)){var t=e.objects[r];n[r]={colors:t.colors.concat([]),lineNumber:t.lineNumber,spritematrix:t.spritematrix.concat([])}}for(var o=[],r=0;r<e.collisionLayers.length;r++)o.push(e.collisionLayers[r].concat([]));for(var s=[],i=[],a=[],l=[],c=[],u=[],r=0;r<e.legend_synonyms.length;r++)s.push(e.legend_synonyms[r].concat([]));for(var r=0;r<e.legend_aggregates.length;r++)i.push(e.legend_aggregates[r].concat([]));for(var r=0;r<e.legend_properties.length;r++)a.push(e.legend_properties[r].concat([]));for(var r=0;r<e.sounds.length;r++)l.push(e.sounds[r].concat([]));for(var r=0;r<e.levels.length;r++)c.push(e.levels[r].concat([]));for(var r=0;r<e.winconditions.length;r++)u.push(e.winconditions[r].concat([]));var g=Object.assign({},e.original_case_names);return{lineNumber:e.lineNumber,objects:n,collisionLayers:o,commentLevel:e.commentLevel,section:e.section,visitedSections:e.visitedSections.concat([]),objects_candname:e.objects_candname,objects_section:e.objects_section,objects_spritematrix:e.objects_spritematrix.concat([]),tokenIndex:e.tokenIndex,currentSection:e.currentSection,legend_synonyms:s,legend_aggregates:i,legend_properties:a,sounds:l,rules:e.rules.concat([]),names:e.names.concat([]),winconditions:u,original_case_names:g,abbrevNames:e.abbrevNames.concat([]),metadata:e.metadata.concat([]),sprite_size:e.sprite_size,case_sensitive:e.case_sensitive,levels:c,STRIDE_OBJ:e.STRIDE_OBJ,STRIDE_MOV:e.STRIDE_MOV}},blankLine:function(e){"levels"===e.section&&e.levels[e.levels.length-1].length>0&&e.levels.push([])},token:function(b,_){function x(e){function n(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}var r;r=_.case_sensitive?new RegExp("\\b"+n(e)+"\\b"):new RegExp("\\b"+n(e)+"\\b","i");var t=E.match(r);null!=t&&(_.original_case_names[e]=t[0])}var E=b.string,y=b.sol();y&&(_.case_sensitive||(b.string=b.string.toLowerCase()),_.tokenIndex=0),b.eatWhile(/[ \t]/);var N=b.peek();if("("===N&&-4!==_.tokenIndex)b.next(),_.commentLevel++;else if(")"===N&&(b.next(),_.commentLevel>0&&0===--_.commentLevel))return"comment";if(_.commentLevel>0){for(;;){if(b.eatWhile(/[^\(\)]+/),b.eol())break;if(N=b.peek(),"("===N?_.commentLevel++:")"===N&&_.commentLevel--,b.next(),0===_.commentLevel)break}return"comment"}if(b.eatWhile(/[ \t]/),y&&b.eol())return blankLineHandle(_);if(y&&b.match(i,!0))return"EQUALSBIT";if(y&&b.match(s,!0)){_.section=b.string.slice(0,b.pos).trim().toLowerCase(),_.visitedSections.indexOf(_.section)>=0&&logError('cannot duplicate sections (you tried to duplicate "'+_.section+'").',_.lineNumber),_.visitedSections.push(_.section);var R=n.indexOf(_.section);if(0==R?(_.objects_section=0,_.visitedSections.length>1&&logError('section "'+_.section+'" must be the first section',_.lineNumber)):-1==_.visitedSections.indexOf(n[R-1])&&(-1===R?logError('no such section as "'+_.section+'".',_.lineNumber):logError('section "'+_.section+'" is out of order, must follow  "'+n[R-1]+'".',_.lineNumber)),"sounds"===_.section){for(var O in _.objects)_.objects.hasOwnProperty(O)&&_.names.push(O);for(var k=0;k<_.legend_synonyms.length;k++){var O=_.legend_synonyms[k][0];_.names.push(O)}for(var k=0;k<_.legend_aggregates.length;k++){var O=_.legend_aggregates[k][0];_.names.push(O)}for(var k=0;k<_.legend_properties.length;k++){var O=_.legend_properties[k][0];_.names.push(O)}}else if("levels"===_.section){for(var O in _.objects)_.objects.hasOwnProperty(O)&&1==O.length&&_.abbrevNames.push(O);for(var k=0;k<_.legend_synonyms.length;k++)1==_.legend_synonyms[k][0].length&&_.abbrevNames.push(_.legend_synonyms[k][0]);for(var k=0;k<_.legend_aggregates.length;k++)1==_.legend_aggregates[k][0].length&&_.abbrevNames.push(_.legend_aggregates[k][0])}return"HEADER"}if(void 0===_.section&&logError('must start with section "OBJECTS"',_.lineNumber),b.eol())return null;switch(_.section){case"objects":var j=function(){var e=y?b.match(t,!0):b.match(/[^\p{Z}\s\()]+[\p{Z}\s]*/u,!0);if(null==e)return b.match(a,!0),b.pos>0&&logWarning('Unknown junk in object section (possibly: the main names for objects have to be words containing only the letters a-z0.9 - if you want to call them something like ",", do it in the legend section).',_.lineNumber),"ERROR";var n=e[0].trim();if(void 0!==_.objects[n])return logError('Object "'+n+'" defined multiple times.',_.lineNumber),"ERROR";for(var r=0;r<_.legend_synonyms.length;r++){_.legend_synonyms[r][0]==n&&logError('Name "'+n+'" already in use.',_.lineNumber)}if(v.indexOf(n)>=0&&logWarning('You named an object "'+n+"\", but this is a keyword. Don't do that!",_.lineNumber),y)_.objects_candname=n,x(n),_.objects[_.objects_candname]={lineNumber:_.lineNumber,colors:[],spritematrix:[],cloneSprite:""};else{if("copy:"==n.substring(0,5)&&n.length>5){var o=n.substring(5);return""!=_.objects[_.objects_candname].cloneSprite?(logError("You already assigned a sprite parent for "+o+", you can't have more than one!",_.lineNumber),"ERROR"):o==_.objects_candname?(logError("You attempted to set the sprite parent for "+o+" to "+o+"! Please don't, and keep the recursion in check.",_.lineNumber),"ERROR"):(_.objects[_.objects_candname].cloneSprite=o,"SPRITEPARENT")}x(n);var s=[n,_.objects_candname];s.lineNumber=_.lineNumber,_.legend_synonyms.push(s)}if(_.case_sensitive&&("player"==n.toLowerCase()&&"player"!=n||"background"==n.toLowerCase()&&"background"!=n)){var s=[n.toLowerCase(),_.objects_candname];s.lineNumber=_.lineNumber,_.legend_synonyms.push(s)}return _.objects_section=1,"NAME"};switch(y&&2==_.objects_section&&(_.objects_section=3),y&&1==_.objects_section&&(_.objects_section=2),_.objects_section){case 0:case 1:return _.objects_spritematrix=[],j();case 2:b.string=b.string.toLowerCase(),_.tokenIndex=0;var L=b.match(reg_color,!0);if(null==L){var I=b.match(t,!0)||b.match(a,!0);return logError("Was looking for color for object "+_.objects_candname+', got "'+I+'" instead.',_.lineNumber),null}void 0===_.objects[_.objects_candname].colors?_.objects[_.objects_candname].colors=[L[0].trim()]:_.objects[_.objects_candname].colors.push(L[0].trim());var w=L[0].trim().toLowerCase();return w in colorPalettes.arnecolors?"COLOR COLOR-"+w.toUpperCase():"transparent"===w?"COLOR FADECOLOR":"MULTICOLOR"+L[0];case 3:var N=b.eat(/[.\d]/),S=_.objects_spritematrix;if(void 0===N)return 0===S.length?j():(logError("Unknown junk in spritematrix for object "+_.objects_candname+".",_.lineNumber),b.match(a,!0),null);y&&S.push("");var C=_.objects[_.objects_candname];if(S[S.length-1]+=N,S[S.length-1].length>_.sprite_size)return logError("Sprites must be "+_.sprite_size+" wide and "+_.sprite_size+" high.",_.lineNumber),b.match(a,!0),null;if(C.spritematrix=_.objects_spritematrix,S.length===_.sprite_size&&S[S.length-1].length==_.sprite_size&&(_.objects_section=0),"."!==N){var O=parseInt(N);return O>=C.colors.length?(logError("Trying to access color number "+O+" from the color palette of sprite "+_.objects_candname+", but there are only "+C.colors.length+" defined in it.",_.lineNumber),"ERROR"):isNaN(O)?(logError('Invalid character "'+N+'" in sprite for '+_.objects_candname,_.lineNumber),"ERROR"):"COLOR BOLDCOLOR COLOR-"+C.colors[O].toUpperCase()}return"COLOR FADECOLOR";default:window.console.logError("EEK shouldn't get here.")}break;case"sounds":if(y){var T=!0,A=a.exec(b.string)[0].split(/[\p{Z}\s]/u).filter(function(e){return""!==e});A.push(_.lineNumber),_.sounds.push(A)}if(null!==(z=b.match(c,!0)))return"SOUNDVERB";if(null!==(z=b.match(m,!0)))return"DIRECTION";if(null!==(z=b.match(o,!0)))return _.tokenIndex++,"SOUND";if(null!==(z=b.match(/[^\[\|\]\p{Z}\s]*/u,!0))){var D=z[0].trim();if(_.names.indexOf(D)>=0)return"NAME"}else z=b.match(a,!0);return logError('unexpected sound token "'+z+'".',_.lineNumber),b.match(a,!0),"ERROR";case"collisionlayers":y&&(_.collisionLayers.push([]),_.tokenIndex=0);var M=b.match(t,!0);if(null===M){var Z=b.pos;return b.match(l,!0),b.pos==Z&&(logError("error detected - unexpected character "+b.peek(),_.lineNumber),b.next()),null}var z=M[0].trim(),B=function(e){if(_.case_sensitive||(e=e.toLowerCase()),e in _.objects)return[e];for(var n=0;n<_.legend_synonyms.length;n++){var r=_.legend_synonyms[n];if(r[0]===e)return B(r[1])}for(var n=0;n<_.legend_aggregates.length;n++){var r=_.legend_aggregates[n];if(r[0]===e)return logError('"'+e+'" is an aggregate (defined using "and"), and cannot be added to a single layer because its constituent objects must be able to coexist.',_.lineNumber),[]}for(var n=0;n<_.legend_properties.length;n++){var r=_.legend_properties[n];if(r[0]===e){return[].concat.apply([],r.slice(1).map(B))}}return logError('Cannot add "'+z+'" to a collision layer; it has not been declared.',_.lineNumber),[]};"background"===z.toLowerCase()?(_.collisionLayers.length>0&&_.collisionLayers[_.collisionLayers.length-1].length>0&&logError("Background must be in a layer by itself.",_.lineNumber),_.tokenIndex=1):0!==_.tokenIndex&&logError("Background must be in a layer by itself.",_.lineNumber);var W=B(z);if(0===_.collisionLayers.length)return logError("no layers found.",_.lineNumber),"ERROR";for(var P=[],k=0;k<W.length;k++)for(var z=W[k],G=0;G<=_.collisionLayers.length-1;G++){var U=_.collisionLayers[G];U.indexOf(z)>=0&&G!=_.collisionLayers.length-1&&P.push(G)}if(P.length>0){for(var V='Object "'+z+'" included in multiple collision layers ( layers ',k=0;k<P.length;k++)V+="#"+(P[k]+1)+", ";V+="#"+_.collisionLayers.length,logWarning(V+" ). You should fix this!",_.lineNumber)}return _.collisionLayers[_.collisionLayers.length-1]=_.collisionLayers[_.collisionLayers.length-1].concat(W),W.length>0?"NAME":"ERROR";case"legend":if(y){var Y=b.string.replace("="," = ");Y=a.exec(Y)[0];var A=Y.split(/[\p{Z}\s]/u).filter(function(e){return""!==e}),T=!0;if(A.length>0){var z=A[0];_.case_sensitive||(z=z.toLowerCase()),v.indexOf(z)>=0&&logWarning('You named an object "'+z+"\", but this is a keyword. Don't do that!",_.lineNumber),A.indexOf(z,2)>=2&&logError("You can't define object "+z.toUpperCase()+" in terms of itself!",_.lineNumber),e(_,z)}if(T)if(A.length<3)T=!1;else if("="!==A[1])T=!1;else if(3===A.length){var $=[A[0],A[2]];_.case_sensitive||($[1]=$[1].toLowerCase()),$.lineNumber=_.lineNumber,x(A[0]),_.legend_synonyms.push($)}else if(A.length%2==0)T=!1;else{var F=A[3].toLowerCase();if("and"===F){for(var B=(function(e){if(_.case_sensitive||(e=e.toLowerCase()),e in _.objects)return[e];for(var n=0;n<_.legend_synonyms.length;n++){var r=_.legend_synonyms[n];if(r[0]===e)return B(r[1])}for(var n=0;n<_.legend_aggregates.length;n++){var r=_.legend_aggregates[n];if(r[0]===e)return[].concat.apply([],r.slice(1).map(B))}for(var n=0;n<_.legend_properties.length;n++){var r=_.legend_properties[n];if(r[0]===e)return logError("Cannot define an aggregate (using 'and') in terms of properties (something that uses 'or').",_.lineNumber),T=!1,[e]}return[e]}),k=5;k<A.length;k+=2)if("and"!==A[k].toLowerCase()){T=!1;break}if(T){for(var K=[A[0]].concat(B(A[2])).concat(B(A[4])),k=6;k<A.length;k+=2)K=K.concat(B(A[k]));K.lineNumber=_.lineNumber,x(K[0]),_.legend_aggregates.push(K)}}else if("or"===F){for(var B=(function(e){if(_.case_sensitive||(e=e.toLowerCase()),e in _.objects)return[e];for(var n=0;n<_.legend_synonyms.length;n++){var r=_.legend_synonyms[n];if(r[0]===e)return B(r[1])}for(var n=0;n<_.legend_aggregates.length;n++){var r=_.legend_aggregates[n];r[0]===e&&(logError("Cannot define a property (using 'or') in terms of aggregates (something that uses 'and').",_.lineNumber),T=!1)}for(var n=0;n<_.legend_properties.length;n++){var r=_.legend_properties[n];if(r[0]===e)return[].concat.apply([],r.slice(1).map(B))}return[e]}),k=5;k<A.length;k+=2)if("or"!==A[k].toLowerCase()){T=!1;break}if(T){for(var K=[A[0]].concat(B(A[2])).concat(B(A[4])),k=6;k<A.length;k+=2)_.case_sensitive?K.push(A[k]):K.push(A[k].toLowerCase());K.lineNumber=_.lineNumber,x(K[0]),_.legend_properties.push(K)}}else T=!1}else;if(!1===T)return logError("incorrect format of legend - should be one of A = B, A = B or C ( or D ...), A = B and C (and D ...)",_.lineNumber),b.match(a,!0),"ERROR";_.tokenIndex=0}if(0===_.tokenIndex)return b.match(/[^=]*/,!0),_.tokenIndex++,"NAME";if(1===_.tokenIndex)return b.next(),b.match(/[\p{Z}\s]*/u,!0),_.tokenIndex++,"ASSSIGNMENT";var M=b.match(t,!0);if(null===M)return logError("Something bad's happening in the LEGEND",_.lineNumber),b.match(a,!0),"ERROR";var z=M[0].trim();if(_.tokenIndex%2==0){return!1===function(e){if(_.case_sensitive||(e=e.toLowerCase()),e in _.objects)return!0;for(var n=0;n<_.legend_aggregates.length;n++){var r=_.legend_aggregates[n];if(r[0]===e)return!0}for(var n=0;n<_.legend_properties.length;n++){var r=_.legend_properties[n];if(r[0]===e)return!0}for(var n=0;n<_.legend_synonyms.length;n++){var r=_.legend_synonyms[n];if(r[0]===e)return!0}return!1}(z)?(logError('Cannot reference "'+z+'" in the LEGEND section; it has not been defined yet.',_.lineNumber),_.tokenIndex++,"ERROR"):(_.tokenIndex++,"NAME")}return _.tokenIndex++,"LOGICWORD";case"rules":if(y){var q=a.exec(b.string)[0];_.rules.push([q,_.lineNumber,E]),_.tokenIndex=0}if(-4===_.tokenIndex)return b.skipToEnd(),"MESSAGE";if(b.match(/[\p{Z}\s]*->[\p{Z}\s]*/u,!0))return"ARROW";if("["===N||"|"===N||"]"===N||"+"===N)return"+"!==N&&(_.tokenIndex=1),b.next(),b.match(/[\p{Z}\s]*/u,!0),"BRACKET";var D=b.match(/[^\[\|\]\p{Z}\s]*/u,!0)[0].trim();return 0===_.tokenIndex&&g.exec(D)?"BRACKET":0===_.tokenIndex&&d.exec(D)?(b.match(/[\p{Z}\s]*/u,!0),"DIRECTION"):1===_.tokenIndex&&u.exec(D)?(b.match(/[\p{Z}\s]*/u,!0),"DIRECTION"):_.names.indexOf(D)>=0?y?(logError("Identifiers cannot appear outside of square brackets in rules, only directions can.",_.lineNumber),"ERROR"):(b.match(/[\p{Z}\s]*/u,!0),"NAME"):(D=D.toLowerCase(),"..."===D?"DIRECTION":"rigid"===D?"DIRECTION":"random"===D?"DIRECTION":"global"===D?"DIRECTION":r.indexOf(D)>=0?(("message"===D||"goto"===D||twiddleable_params.includes(D))&&(_.tokenIndex=-4),"COMMAND"):(logError('Name "'+D+'", referred to in a rule, does not exist.',_.lineNumber),"ERROR"));case"winconditions":if(y){var H=a.exec(b.string),J=H[0].split(/[\p{Z}\s]/u),Q=J.filter(function(e){return""!==e});Q.push(_.lineNumber),_.winconditions.push(Q),_.tokenIndex=-1}_.tokenIndex++;var X=b.match(/[\p{Z}\s]*[\p{L}\p{N}_]+[\p{Z}\s]*/u);if(null===X)return logError("incorrect format of win condition.",_.lineNumber),b.match(a,!0),"ERROR";var ee=X[0].trim();if(0===_.tokenIndex)return p.exec(ee)?"LOGICWORD":"ERROR";if(2===_.tokenIndex)return"on"!=ee?(logError('Expecting the word "ON" but got "'+ee.toUpperCase()+"'.",_.lineNumber),"ERROR"):"LOGICWORD";if(1===_.tokenIndex||3===_.tokenIndex)return-1===_.names.indexOf(ee)?(logError('Error in win condition: "'+ee+'" is not a valid object name.',_.lineNumber),"ERROR"):"NAME";break;case"levels":if(y){if(b.match(/[\p{Z}\s]*message\b[\p{Z}\s]*/u,!0)){_.tokenIndex=1;var ne=["message",E.slice(b.pos).trim(),_.lineNumber,_.currentSection];return 0==_.levels[_.levels.length-1].length?_.levels.splice(_.levels.length-1,0,ne):_.levels.push(ne),"MESSAGE_VERB"}if(b.match(/[\p{Z}\s]*message[\p{Z}\s]*/u,!0)){logWarning("You probably meant to put a space after 'message' innit.  That's ok, I'll still interpret it as a message, but you probably want to put a space there.",_.lineNumber),_.tokenIndex=1;var ne=["message",E.slice(b.pos).trim(),_.lineNumber,_.currentSection];return 0==_.levels[_.levels.length-1].length?_.levels.splice(_.levels.length-1,0,ne):_.levels.push(ne),"MESSAGE_VERB"}if(b.match(/\s*section\s*/i,!0))return _.tokenIndex=3,_.currentSection=E.slice(b.pos).trim(),"SECTION_VERB";if(b.match(/\s*goto\s*/i,!0)){_.tokenIndex=4;var ne=["goto",E.slice(b.pos).trim(),_.lineNumber,_.currentSection];return 0==_.levels[_.levels.length-1].length?_.levels.splice(_.levels.length-1,0,ne):_.levels.push(ne),"GOTO_VERB"}var re=b.match(a,!1)[0].trim();_.tokenIndex=2;var te=_.levels[_.levels.length-1];"\n"==te[0]?_.levels.push([_.lineNumber,_.currentSection,re]):(0==te.length&&(te.push(_.lineNumber),te.push(_.currentSection)),te.push(re),te.length>2&&re.length!=te[2].length&&logWarning("Maps must be rectangular, yo (In a level, the length of each row must be the same).",_.lineNumber))}else{if(1==_.tokenIndex)return b.skipToEnd(),"MESSAGE";if(3==_.tokenIndex)return b.skipToEnd(),"SECTION";if(4==_.tokenIndex)return b.skipToEnd(),"GOTO"}if(2===_.tokenIndex&&!b.eol()){var N=b.peek();return b.next(),_.abbrevNames.indexOf(N)>=0?"LEVEL":(logError('Key "'+N+'" not found. Do you need to add it to the legend, or define a new object?',_.lineNumber),"ERROR")}break;default:if(y&&(_.tokenIndex=0),0!=_.tokenIndex)return b.match(a,!0),"METADATATEXT";var X=b.match(/[\p{Z}\s]*[\p{L}\p{N}_]+[\p{Z}\s]*/u);if(null!==X){var oe=X[0].trim();if(y){if(f.indexOf(oe)>=0){"youtube"!==oe&&"author"!==oe&&"homepage"!==oe&&"title"!==oe&&"custom_font"!==oe||(b.string=E);var se=b.match(a,!1);return null!=se?"sprite_size"==oe?_.sprite_size=parseInt(se[0].trim(),10):(_.metadata.push(oe),_.metadata.push(se[0].trim())):logError('MetaData "'+oe+'" needs a value.',_.lineNumber),_.tokenIndex=1,"METADATA"}return h.indexOf(oe)>=0?("case_sensitive"==oe&&(_.case_sensitive=!0),_.metadata.push(oe),_.metadata.push("true"),_.tokenIndex=-1,"METADATA"):(logError("Unrecognised stuff in the prelude.",_.lineNumber),"ERROR")}return-1==_.tokenIndex?(logError('MetaData "'+oe+'" has no parameters.',_.lineNumber),"ERROR"):"METADATA"}}return b.eol()?null:b.eol()?void 0:(b.next(),null)},startState:function(){return{objects:{},lineNumber:0,commentLevel:0,section:"",visitedSections:[],objects_candname:"",objects_section:0,objects_spritematrix:[],collisionLayers:[],tokenIndex:0,currentSection:null,legend_synonyms:[],legend_aggregates:[],legend_properties:[],sounds:[],rules:[],names:[],winconditions:[],metadata:[],sprite_size:5,case_sensitive:!1,original_case_names:{},abbrevNames:[],levels:[[]],subsection:""}}}};window.CodeMirror.defineMode("puzzle",codeMirrorFn);</script> <script>'use strict'; 
 
function isColor(str) { 
 str = str.trim(); 
 if (str in colorPalettes.arnecolors) 
  return true; 
 if (/^#([0-9A-F]{2}){3,4}$/i.test(str)) 
  return true; 
 if (/^#([0-9A-F]{3})$/i.test(str)) 
  return true; 
 if (str === "transparent") 
  return true; 
 return false; 
} 
 
function colorToHex(palette, str) { 
    str = str.trim(); 
    if (str in palette) { 
        return palette[str]; 
    } 
 
    return str; 
} 
 
 
function generateSpriteMatrix(dat) { 
 
    var result = []; 
    for (var i = 0; i < dat.length; i++) { 
        var row = []; 
        for (var j = 0; j < dat.length; j++) { 
            var ch = dat[i].charAt(j); 
            if (ch == '.') { 
                row.push(-1); 
            } else { 
                row.push(ch); 
            } 
        } 
        result.push(row); 
    } 
    return result; 
} 
 
var debugMode; 
var colorPalette; 
 
function generateExtraMembers(state) { 
 
    if (state.collisionLayers.length === 0) { 
        logError("No collision layers defined.  All objects need to be in collision layers."); 
    } 
 
    //annotate objects with layers 
    //assign ids at the same time 
    state.idDict = []; 
    var idcount = 0; 
    for (var layerIndex = 0; layerIndex < state.collisionLayers.length; layerIndex++) { 
        for (var j = 0; j < state.collisionLayers[layerIndex].length; j++) { 
            var n = state.collisionLayers[layerIndex][j]; 
            if (n in state.objects) { 
                var o = state.objects[n]; 
                o.layer = layerIndex; 
                o.id = idcount; 
                state.idDict[idcount] = n; 
                idcount++; 
            } 
        } 
    } 
 
    //set object count 
    state.objectCount = idcount; 
 
    //calculate blank mask template 
    var layerCount = state.collisionLayers.length; 
    var blankMask = []; 
    for (var i = 0; i < layerCount; i++) { 
        blankMask.push(-1); 
    } 
 
    // how many words do our bitvecs need to hold? 
    STRIDE_OBJ = Math.ceil(state.objectCount / 32) | 0; 
    STRIDE_MOV = Math.ceil(layerCount / 5) | 0; 
    state.STRIDE_OBJ = STRIDE_OBJ; 
    state.STRIDE_MOV = STRIDE_MOV; 
 
    //get colorpalette name 
    debugMode = false; 
    verbose_logging = false; 
    throttle_movement = false; 
    colorPalette = colorPalettes.arnecolors; 
    for (var i = 0; i < state.metadata.length; i += 2) { 
        var key = state.metadata[i]; 
        var val = state.metadata[i + 1]; 
        if (key === 'color_palette') { 
            if (val in colorPalettesAliases) { 
                val = colorPalettesAliases[val]; 
            } 
            if (colorPalettes[val] === undefined) { 
                logError('Palette "' + val + '" not found, defaulting to arnecolors.', 0); 
            } else { 
                colorPalette = colorPalettes[val]; 
            } 
        } else if (key === 'debug') { 
            if (IDE && unitTesting===false){ 
                debugMode = true; 
                cache_console_messages = true; 
            } 
        } else if (key === 'verbose_logging') { 
            if (IDE && unitTesting===false){ 
                verbose_logging = true; 
                cache_console_messages = true; 
            } 
        } else if (key === 'throttle_movement') { 
            throttle_movement = true; 
        } 
    } 
 
    //convert colors to hex 
    for (var n in state.objects) { 
        if (state.objects.hasOwnProperty(n)) { 
            //convert color to hex 
            var o = state.objects[n]; 
            if (o.colors.length > 10) { 
                logError("a sprite cannot have more than 10 colors.  Why you would want more than 10 is beyond me.", o.lineNumber + 1); 
            } 
            for (var i = 0; i < o.colors.length; i++) { 
                var c = o.colors[i]; 
                if (isColor(c)) { 
                    c = colorToHex(colorPalette, c); 
                    o.colors[i] = c; 
                } else { 
                    logError('Invalid color specified for object "' + n + '", namely "' + o.colors[i] + '".', o.lineNumber + 1); 
                    o.colors[i] = '#ff00ff'; // magenta error color 
                } 
            } 
        } 
    } 
 
    //generate sprite matrix 
    for (var n in state.objects) { 
        if (state.objects.hasOwnProperty(n)) { 
            var o = state.objects[n]; 
            if (o.colors.length==0) { 
                logError('color not specified for object "' + n +'".',o.lineNumber); 
                o.colors=["#ff00ff"]; 
            } 
           if (o.cloneSprite !== "") { 
              //console.log("To clone: "+o.cloneSprite); 
              if (state.objects.hasOwnProperty(o.cloneSprite)) { 
                   
                  if (state.objects[o.cloneSprite].cloneSprite === "") { 
                      o.spritematrix = deepClone(state.objects[o.cloneSprite].spritematrix); 
                      //console.log(o.spritematrix); 
                  } else { 
                      logError("The sprite that "+n+" attempted to clone ("+o.cloneSprite+") clones a sprite itself ("+state.objects[o.cloneSprite].cloneSprite + "), so it can't clone the sprite! You'll need to set up the cloning differently.",o.lineNumber); 
                  } 
              } else { 
                  logError(n +" attempted to clone the sprite matrix of "+o.cloneSprite+", but that object doesn't exist?!",o.lineNumber); 
              } 
           }  
            /*else /*if (o.colors[o.colors.length-1][0] !== "#") { 
                var objectToClone = o.colors[o.colors.length-1]; 
                if (state.objects.hasOwnProperty(objectToClone)) { 
                  o.spritematrix = objectToClone.spritematrix; 
                  o.spritematrix = generateSpriteMatrix(o.spritematrix); 
                  logWarning("Cloned "+objectToClone, o.lineNumber); 
                  continue; 
              } else { 
                  logError("Attempted to clone the sprite matrix of "+objectToClone+", but that object doesn't exist?!") 
              } 
          }*/ 
          else if (o.spritematrix.length===0) { 
              o.spritematrix = new Array(state.sprite_size); 
              var zeros = new Array(state.sprite_size); 
              for(var i = 0; i < state.sprite_size; i++) { 
                  zeros[i] = 0; 
              } 
              for(var i = 0; i < state.sprite_size; i++) { 
                  o.spritematrix[i] = zeros; 
              } 
          } else { 
              if ( o.spritematrix.length!==state.sprite_size) { 
                  logWarning("Sprite graphics must be " + state.sprite_size + " wide and " + state.sprite_size + " high exactly.",o.lineNumber); 
              } else { 
                  for(var i = 0; i < state.sprite_size; i++) { 
                      if(o.spritematrix[i].length!==state.sprite_size) { 
                          logWarning("Sprite graphics must be " + state.sprite_size + " wide and " + state.sprite_size + " high exactly.",o.lineNumber); 
                          break; 
                      } 
                  } 
              } 
              o.spritematrix = generateSpriteMatrix(o.spritematrix); 
          } 
        } 
    } 
 
 
    //calculate glyph dictionary 
    var glyphDict = {}; 
    for (var n in state.objects) { 
        if (state.objects.hasOwnProperty(n)) { 
            var o = state.objects[n]; 
            var mask = blankMask.concat([]); 
            mask[o.layer] = o.id; 
            glyphDict[n] = mask; 
        } 
    } 
    var added = true; 
    while (added) { 
        added = false; 
 
        //then, synonyms 
        for (var i = 0; i < state.legend_synonyms.length; i++) { 
            var dat = state.legend_synonyms[i]; 
            var key = dat[0]; 
            var val = dat[1]; 
            if ((!(key in glyphDict) || (glyphDict[key] === undefined)) && (glyphDict[val] !== undefined)) { 
                added = true; 
                glyphDict[key] = glyphDict[val]; 
            } 
        } 
 
        //then, aggregates 
        for (var i = 0; i < state.legend_aggregates.length; i++) { 
            var dat = state.legend_aggregates[i]; 
            var key = dat[0]; 
            var vals = dat.slice(1); 
            var allVallsFound = true; 
            for (var j = 0; j < vals.length; j++) { 
                var v = vals[j]; 
                if (glyphDict[v] === undefined) { 
                    allVallsFound = false; 
                    break; 
                } 
            } 
            if ((!(key in glyphDict) || (glyphDict[key] === undefined)) && allVallsFound) { 
                var mask = blankMask.concat([]); 
 
                for (var j = 1; j < dat.length; j++) { 
                    var n = dat[j]; 
                    var o = state.objects[n]; 
                    if (o == undefined) { 
                        logError('Object not found with name ' + n, state.lineNumber); 
                    } 
                    if (mask[o.layer] == -1) { 
                        mask[o.layer] = o.id; 
                    } else { 
                        if (o.layer === undefined) { 
                            logError('Object "' + n.toUpperCase() + '" has been defined, but not assigned to a layer.', dat.lineNumber); 
                        } else { 
                            var n1 = n.toUpperCase(); 
                            var n2 = state.idDict[mask[o.layer]].toUpperCase(); 
                            if (n1 !== n2) { 
                                logError( 
                                    'Trying to create an aggregate object (defined in the legend) with both "' + 
                                    n1 + '" and "' + n2 + '", which are on the same layer and therefore can\'t coexist.', 
                                    dat.lineNumber 
                                ); 
                            } 
                        } 
                    } 
                } 
                added = true; 
                glyphDict[dat[0]] = mask; 
            } 
        } 
    } 
    state.glyphDict = glyphDict; 
 
    var aggregatesDict = {}; 
    for (var i = 0; i < state.legend_aggregates.length; i++) { 
        var entry = state.legend_aggregates[i]; 
        aggregatesDict[entry[0]] = entry.slice(1); 
    } 
    state.aggregatesDict = aggregatesDict; 
 
    var propertiesDict = {}; 
    for (var i = 0; i < state.legend_properties.length; i++) { 
        var entry = state.legend_properties[i]; 
        propertiesDict[entry[0]] = entry.slice(1); 
    } 
    state.propertiesDict = propertiesDict; 
 
    //calculate lookup dictionaries 
    var synonymsDict = {}; 
    for (var i = 0; i < state.legend_synonyms.length; i++) { 
        var entry = state.legend_synonyms[i]; 
        var key = entry[0]; 
        var value = entry[1]; 
        if (value in aggregatesDict) { 
            aggregatesDict[key] = aggregatesDict[value]; 
        } else if (value in propertiesDict) { 
            propertiesDict[key] = propertiesDict[value]; 
        } else if (key !== value) { 
            synonymsDict[key] = value; 
        } 
    } 
    state.synonymsDict = synonymsDict; 
 
    var modified = true; 
    while (modified) { 
        modified = false; 
        for (var n in synonymsDict) { 
            if (synonymsDict.hasOwnProperty(n)) { 
                var value = synonymsDict[n]; 
                if (value in propertiesDict) { 
                    delete synonymsDict[n]; 
                    propertiesDict[n] = propertiesDict[value]; 
                    modified = true; 
                } else if (value in aggregatesDict) { 
                    delete aggregatesDict[n]; 
                    aggregatesDict[n] = aggregatesDict[value]; 
                    modified = true; 
                } else if (value in synonymsDict) { 
                    synonymsDict[n] = synonymsDict[value]; 
                } 
            } 
        } 
 
        for (var n in propertiesDict) { 
            if (propertiesDict.hasOwnProperty(n)) { 
                var values = propertiesDict[n]; 
                for (var i = 0; i < values.length; i++) { 
                    var value = values[i]; 
                    if (value in synonymsDict) { 
                        values[i] = synonymsDict[value]; 
                        modified = true; 
                    } else if (value in propertiesDict) { 
                        values.splice(i, 1); 
                        var newvalues = propertiesDict[value]; 
                        for (var j = 0; j < newvalues.length; j++) { 
                            var newvalue = newvalues[j]; 
                            if (values.indexOf(newvalue) === -1) { 
                                values.push(newvalue); 
                            } 
                        } 
                        modified = true; 
                    } 
                    if (value in aggregatesDict) { 
                        logError('Trying to define property "' + n.toUpperCase() + '" in terms of aggregate "' + value.toUpperCase() + '".'); 
                    } 
                } 
            } 
        } 
 
 
        for (var n in aggregatesDict) { 
            if (aggregatesDict.hasOwnProperty(n)) { 
                var values = aggregatesDict[n]; 
                for (var i = 0; i < values.length; i++) { 
                    var value = values[i]; 
                    if (value in synonymsDict) { 
                        values[i] = synonymsDict[value]; 
                        modified = true; 
                    } else if (value in aggregatesDict) { 
                        values.splice(i, 1); 
                        var newvalues = aggregatesDict[value]; 
                        for (var j = 0; j < newvalues.length; j++) { 
                            var newvalue = newvalues[j]; 
                            if (values.indexOf(newvalue) === -1) { 
                                values.push(newvalue); 
                            } 
                        } 
                        modified = true; 
                    } 
                    if (value in propertiesDict) { 
                        logError('Trying to define aggregate "' + n.toUpperCase() + '" in terms of property "' + value.toUpperCase() + '".'); 
                    } 
                } 
            } 
        } 
    } 
 
    /* determine which properties specify objects all on one layer */ 
    state.propertiesSingleLayer = {}; 
    for (var key in propertiesDict) { 
        if (propertiesDict.hasOwnProperty(key)) { 
            var values = propertiesDict[key]; 
            var sameLayer = true; 
            for (var i = 1; i < values.length; i++) { 
                if ((state.objects[values[i - 1]].layer !== state.objects[values[i]].layer)) { 
                    sameLayer = false; 
                    break; 
                } 
            } 
            if (sameLayer) { 
                state.propertiesSingleLayer[key] = state.objects[values[0]].layer; 
            } 
        } 
    } 
 
    if (state.idDict[0] === undefined && state.collisionLayers.length > 0) { 
        logError('You need to have some objects defined'); 
    } 
 
    //set default background object 
    var backgroundid; 
    var backgroundlayer; 
    if (state.objects.background === undefined) { 
        if ('background' in state.synonymsDict) { 
            var n = state.synonymsDict['background']; 
            var o = state.objects[n]; 
            backgroundid = o.id; 
            backgroundlayer = o.layer; 
        } else if ('background' in state.propertiesDict) { 
            var n = state.propertiesDict['background'][0]; 
            var o = state.objects[n]; 
            backgroundid = o.id; 
            backgroundlayer = o.layer; 
        } else if ('background' in state.aggregatesDict) { 
            var o = state.objects[state.idDict[0]]; 
            backgroundid = o.id; 
            backgroundlayer = o.layer; 
            logError("background cannot be an aggregate (declared with 'and'), it has to be a simple type, or property (declared in terms of others using 'or')."); 
        } else { 
            var o = state.objects[state.idDict[0]]; 
            if (o!=null){ 
                backgroundid = o.id; 
                backgroundlayer = o.layer; 
            } 
            logError("you have to define something to be the background"); 
        } 
    } else { 
        backgroundid = state.objects.background.id; 
        backgroundlayer = state.objects.background.layer; 
    } 
    state.backgroundid = backgroundid; 
    state.backgroundlayer = backgroundlayer; 
} 
 
function generateExtraMembersPart2(state) { 
 function assignMouseObject(preludeTerm, defaultName) { 
  if (preludeTerm in state.metadata) { 
   var name = state.metadata[preludeTerm] || defaultName; 
   var id = null; 
   if (state.objects[name]) { 
    id = state.objects[name].id; 
   } else { 
    if (name in state.synonymsDict) { 
     var n = state.synonymsDict[name]; 
     var o = state.objects[n]; 
     id = o.id; 
    } else { 
     var o=state.objects[state.idDict[1]]; 
     id=o.id; 
     logError(name + " object/alias has to be defined"); 
    } 
   } 
   return id; 
  } 
 } 
  
 state.lmbID = assignMouseObject("mouse_left", "lmb"); 
 state.rmbID = assignMouseObject("mouse_right", "rmb"); 
 state.dragID = assignMouseObject("mouse_drag", "drag"); 
 state.rdragID = assignMouseObject("mouse_rdrag", "rdrag"); 
 state.lmbupID = assignMouseObject("mouse_up", "lmbup"); 
 state.rmbupID = assignMouseObject("mouse_rup", "rmbup"); 
  
 if ("mouse_obstacle" in state.metadata) { 
  var name = state.metadata["mouse_obstacle"]; 
   
  if (name) { 
   state.obstacleMask = state.objectMasks[name]; 
   if (!state.obstacleMask) { 
    logError(name + " object/alias has to be defined."); 
    state.obstacleMask = state.objectMasks["background"]; 
   } 
  } 
 } 
} 
 
Level.prototype.calcBackgroundMask = function(state) { 
    if (state.backgroundlayer === undefined) { 
        logError("you have to have a background layer"); 
    } 
 
    var backgroundMask = state.layerMasks[state.backgroundlayer]; 
    for (var i = 0; i < this.n_tiles; i++) { 
        var cell = this.getCell(i); 
        cell.iand(backgroundMask); 
        if (!cell.iszero()) { 
            return cell; 
        } 
    } 
    cell = new BitVec(STRIDE_OBJ); 
    cell.ibitset(state.backgroundid); 
    return cell; 
} 
 
function levelFromString(state,level) { 
 var backgroundlayer=state.backgroundlayer; 
 var backgroundid=state.backgroundid; 
 var backgroundLayerMask = state.layerMasks[backgroundlayer]; 
 var o = new Level(level[0], level[2].length, level.length-2, state.collisionLayers.length, null, level[1]); 
 o.objects = new Int32Array(o.width * o.height * STRIDE_OBJ); 
 
 for (var i = 0; i < o.width; i++) { 
  for (var j = 0; j < o.height; j++) { 
   var ch = level[j+2].charAt(i); 
   if (ch.length==0) { 
    ch=level[j+2].charAt(level[j+2].length-1); 
   } 
   var mask = state.glyphDict[ch]; 
 
   if (mask == undefined) { 
    if (state.propertiesDict[ch]===undefined) { 
     logError('Error, symbol "' + ch + '", used in map, not found.', level[0]+j); 
    } else { 
     logError('Error, symbol "' + ch + '" is defined using \'or\', and therefore ambiguous - it cannot be used in a map. Did you mean to define it in terms of \'and\'?', level[0]+j);        
    } 
 
   } 
 
   var maskint = new BitVec(STRIDE_OBJ); 
   mask = mask.concat([]);      
   for (var z = 0; z < o.layerCount; z++) { 
    if (mask[z]>=0) { 
     maskint.ibitset(mask[z]); 
    } 
   } 
   for (var w = 0; w < STRIDE_OBJ; ++w) { 
    o.objects[STRIDE_OBJ * (i * o.height + j) + w] = maskint.data[w]; 
   } 
  } 
 } 
 
 var levelBackgroundMask = o.calcBackgroundMask(state); 
 for (var i=0;i<o.n_tiles;i++) 
 { 
  var cell = o.getCell(i); 
  if (!backgroundLayerMask.anyBitsInCommon(cell)) { 
   cell.ior(levelBackgroundMask); 
   o.setCell(i, cell); 
  } 
 } 
 return o; 
} 
//also assigns glyphDict 
function levelsToArray(state) { 
 var levels = state.levels; 
 var processedLevels = []; 
 var sectionTerminated = false; 
 var previousSection = null; 
 
 for (var levelIndex = 0; levelIndex < levels.length; levelIndex++) { 
  var level = levels[levelIndex]; 
  if (level.length == 0) { 
   continue; 
  } 
   
  var o; 
  if (level[0] == 'message') { 
   o = { 
    message: level[1], 
    section: level[3] 
   }; 
   splitMessage = wordwrap(o.message,intro_template[0].length); 
   if (splitMessage.length>12){ 
    logWarning('Message too long to fit on screen.', level[2]); 
   } 
   if(o.section != previousSection) {sectionTerminated = false; previousSection = o.section;} 
   if(sectionTerminated) logWarning('Message unreachable due to previous GOTO.', level[2]); 
  } else if (level[0] == 'goto') { 
   o = { 
    target: level[1], 
    lineNumber: level[2], 
    section: level[3] 
   }; 
   if(o.section != previousSection) {sectionTerminated = false; previousSection = o.section;} 
   if(sectionTerminated) logWarning('GOTO unreachable due to previous GOTO.', o.lineNumber); 
   sectionTerminated = true; 
  } else { 
   o = levelFromString(state,level); 
   if(o.section != previousSection) {sectionTerminated = false; previousSection = o.section;} 
   if(sectionTerminated) logWarning('Level unreachable due to previous GOTO.', o.lineNumber); 
  } 
  processedLevels.push(o); 
 } 
 
 state.levels = processedLevels; 
} 
 
function extractSections(state) { 
 var sections = []; 
 
 var lastSection = null; 
 
 for(var i = 0; i < state.levels.length; i++) { 
  var level = state.levels[i]; 
   
  if(level.section != lastSection) { 
   var o = { 
    name: level.section, 
    firstLevel: i 
   }; 
   if(o.name == "__WIN__") { 
    state.winSection = o; 
   } else { 
    sections.push(o); 
   } 
    
   lastSection = level.section; 
  } 
 } 
 
 state.sections = sections; 
} 
 
function convertSectionNamesToIndices(state) { 
 var sectionMap = {}; 
 var duplicateSections = {}; 
 for (var s = 0; s < state.sections.length; s++) { 
  var sectionName = state.sections[s].name.toLowerCase(); 
  if(sectionMap[sectionName] === undefined){ 
   sectionMap[sectionName] = s; 
  }else{ 
   duplicateSections[sectionName] = true; 
  } 
 } 
  
 // GOTO commands in the RULES 
 for (var r = 0; r < state.rules.length; r++) { 
  var rule = state.rules[r]; 
  for (var c = 0; c < rule.commands.length; c++) { 
   var command = rule.commands[c]; 
   if (command[0] != 'goto') continue; 
   var sectionName = command[1].toLowerCase(); 
   var sectionIndex = sectionMap[sectionName]; 
   if (sectionIndex === undefined){ 
    logError('Invalid GOTO command - There is no section named "'+command[1]+'". Either it does not exist, or it has zero levels.', rule.lineNumber); 
    sectionIndex = -1; 
   }else if (duplicateSections[sectionName] !== undefined){ 
    logError('Invalid GOTO command - There are multiple sections named "'+command[1]+'". Section names must be unique for GOTO to work.', rule.lineNumber); 
    sectionIndex = -1; 
   } 
   command[1] = sectionIndex; 
  } 
 } 
  
 // GOTO commands in the LEVELS 
 for (var i = 0; i < state.levels.length; i++) { 
  var level = state.levels[i]; 
  if (level.target === undefined) continue; // This was a level or a message, but not a goto. 
  var targetName = level.target.toLowerCase(); 
  var targetIndex = sectionMap[targetName]; 
  if (targetIndex === undefined){ 
   logError('Invalid GOTO command - There is no section named "'+command[1]+'".', level.lineNumber); 
   targetIndex = 0; 
  }else if (duplicateSections[targetName] !== undefined){ 
   logError('Invalid GOTO command - There are multiple sections named "'+command[1]+'".', level.lineNumber); 
   targetIndex = 0; 
  } 
  level.target = targetIndex; 
 } 
} 
 
var directionaggregates = { 
    'horizontal': ['left', 'right'], 
    'horizontal_par': ['left', 'right'], 
    'horizontal_perp': ['left', 'right'], 
    'vertical': ['up', 'down'], 
    'vertical_par': ['up', 'down'], 
    'vertical_perp': ['up', 'down'], 
    'moving': ['up', 'down', 'left', 'right', 'action'], 
    'orthogonal': ['up', 'down', 'left', 'right'], 
    'perpendicular': ['^', 'v'], 
    'parallel': ['<', '>'] 
}; 
 
var relativeDirections = ['^', 'v', '<', '>', 'perpendicular', 'parallel']; 
var simpleAbsoluteDirections = ['up', 'down', 'left', 'right']; 
var simpleRelativeDirections = ['^', 'v', '<', '>']; 
var reg_directions_only = /^(\>|\<|\^|v|up|down|left|right|moving|stationary|no|randomdir|random|horizontal|vertical|orthogonal|perpendicular|parallel|action)$/i; 
//redeclaring here, i don't know why 
var commandwords = ["sfx0","sfx1","sfx2","sfx3","sfx4","sfx5","sfx6","sfx7","sfx8","sfx9","sfx10","cancel","checkpoint","restart","win","message","again","undo", 
  "nosave","quit","zoomscreen","flickscreen","smoothscreen","again_interval","realtime_interval","key_repeat_interval",'noundo','norestart','background_color','text_color','goto']; 
function directionalRule(rule) { 
    for (var i = 0; i < rule.lhs.length; i++) { 
        var cellRow = rule.lhs[i]; 
        if (cellRow.length > 1) { 
            return true; 
        } 
        for (var j = 0; j < cellRow.length; j++) { 
            var cell = cellRow[j]; 
            for (var k = 0; k < cell.length; k += 2) { 
                if (relativeDirections.indexOf(cell[k]) >= 0) { 
                    return true; 
                } 
            } 
        } 
    } 
    for (var i = 0; i < rule.rhs.length; i++) { 
        var cellRow = rule.rhs[i]; 
        if (cellRow.length > 1) { 
            return true; 
        } 
        for (var j = 0; j < cellRow.length; j++) { 
            var cell = cellRow[j]; 
            for (var k = 0; k < cell.length; k += 2) { 
                if (relativeDirections.indexOf(cell[k]) >= 0) { 
                    return true; 
                } 
            } 
        } 
    } 
    return false; 
} 
 
function findIndexAfterToken(str, tokens, tokenIndex) { 
    str = str.toLowerCase(); 
    var curIndex = 0; 
    for (var i = 0; i <= tokenIndex; i++) { 
        var token = tokens[i]; 
        curIndex = str.indexOf(token, curIndex) + token.length; 
    } 
    return curIndex; 
} 
function rightBracketToRightOf(tokens,i){ 
    for(;i<tokens.length;i++){ 
        if (tokens[i]==="]"){ 
            return true; 
        } 
    } 
    return false; 
} 
 
function processRuleString(rule, state, curRules) { 
    /* 
 
     intermediate structure 
      dirs: Directions[] 
      pre : CellMask[] 
      post : CellMask[] 
 
      //pre/post pairs must have same lengths 
     final rule structure 
      dir: Direction 
      pre : CellMask[] 
      post : CellMask[] 
    */ 
    var line = rule[0]; 
    var lineNumber = rule[1]; 
    var origLine = rule[2]; 
 
    // STEP ONE, TOKENIZE 
    line = line.replace(/\[/g, ' [ ').replace(/\]/g, ' ] ').replace(/\|/g, ' | ').replace(/\-\>/g, ' -> '); 
    line = line.trim(); 
    if (line[0] === '+') { 
        line = line.substring(0, 1) + " " + line.substring(1, line.length); 
    } 
    var tokens = line.split(/\s/).filter(function(v) { return v !== '' }); 
 
    if (tokens.length == 0) { 
        logError('Spooky error!  Empty line passed to rule function.', lineNumber); 
    } 
 
 
// STEP TWO, READ DIRECTIONS 
/* 
 STATE 
 0 - scanning for initial directions 
 LHS 
 1 - reading cell contents LHS 
 2 - reading cell contents RHS 
*/ 
var parsestate = 0; 
var directions = []; 
 
var curcell = null; // [up, cat, down mouse] 
var curcellrow = []; // [  [up, cat]  [ down, mouse ] ] 
 
var incellrow = false; 
 
    var appendGroup = false; 
    var rhs = false; 
    var lhs_cells = []; 
    var rhs_cells = []; 
    var late = false; 
    var rigid = false; 
    var groupNumber = lineNumber; 
    var commands = []; 
    var randomRule = false; 
    var has_plus = false; 
    var globalRule=false; 
 
if (tokens.length===1) { 
    if (tokens[0]==="startloop" ) { 
        rule_line = { 
            bracket: 1 
        } 
        return rule_line; 
    } else if (tokens[0]==="endloop" ) { 
        rule_line = { 
            bracket: -1 
        } 
        return rule_line; 
    } 
} 
 
if (tokens.indexOf('->') == -1) { 
    logError("A rule has to have an arrow in it.  There's no arrow here! Consider reading up about rules - you're clearly doing something weird", lineNumber); 
} 
 
    var curcell = []; 
    var bracketbalance = 0; 
    for (var i = 0; i < tokens.length; i++) { 
        var token = tokens[i]; 
        switch (parsestate) { 
            case 0: 
                { 
                    //read initial directions 
                    if (token === '+') { 
                        has_plus=true; 
                        if (groupNumber === lineNumber) { 
                            if (curRules.length == 0) { 
                                logError('The "+" symbol, for joining a rule with the group of the previous rule, needs a previous rule to be applied to.'); 
                            } 
                            if (i !== 0) { 
                                logError('The "+" symbol, for joining a rule with the group of the previous rule, must be the first symbol on the line '); 
                            } 
                            groupNumber = curRules[curRules.length - 1].groupNumber; 
                        } else { 
                            logError('Two "+"s ("append to previous rule group" symbol) applied to the same rule.', lineNumber); 
                        } 
                    } else if (token in directionaggregates) { 
                        directions = directions.concat(directionaggregates[token]); 
                    } else if (token === 'late') { 
                        late = true; 
                    } else if (token === 'rigid') { 
                        rigid = true; 
                    } else if (token === 'random') { 
                        randomRule = true; 
                        if (has_plus) 
                        { 
                            logError(`A rule-group can only be marked random by the opening rule in the group (aka, a '+' and 'random' can't appear as rule modifiers on the same line).  Why? Well, you see "random" isn't a property of individual rules, but of whole rule groups.  It indicates that a single possible application of some rule from the whole group should be applied at random.`, lineNumber)  
                        } 
 
                    }else if (token==='global') { 
                        globalRule=true; 
                    } else if (simpleAbsoluteDirections.indexOf(token) >= 0) { 
                        directions.push(token); 
                    } else if (simpleRelativeDirections.indexOf(token) >= 0) { 
                        logError('You cannot use relative directions (\"^v<>\") to indicate in which direction(s) a rule applies.  Use absolute directions indicators (Up, Down, Left, Right, Horizontal, or Vertical, for instance), or, if you want the rule to apply in all four directions, do not specify directions', lineNumber); 
                    } else if (token == '[') { 
                        if (directions.length == 0) { 
                            directions = directions.concat(directionaggregates['orthogonal']); 
                        } 
                        parsestate = 1; 
                        i--; 
                    } else { 
                        logError("The start of a rule must consist of some number of directions (possibly 0), before the first bracket, specifying in what directions to look (with no direction specified, it applies in all four directions).  It seems you've just entered \"" + token.toUpperCase() + '\".', lineNumber); 
            } 
            break; 
        } 
        case 1: { 
            if (token == '[') { 
                bracketbalance++; 
                if(bracketbalance>1){ 
                    logWarning("Multiple opening brackets without closing brackets.  Something fishy here.  Every '[' has to be closed by a ']', and you can't nest them.", lineNumber); 
                } 
                if (curcell.length > 0) { 
                    logError('Error, malformed cell rule - encountered a "["" before previous bracket was closed', lineNumber); 
                } 
                incellrow = true; 
                curcell = []; 
            } else if (reg_directions_only.exec(token)) { 
                if (curcell.length % 2 == 1) { 
                    logError("Error, an item can only have one direction/action at a time, but you're looking for several at once!", lineNumber); 
                } else if (!incellrow) { 
                    logWarning("Invalid syntax. Directions should be placed at the start of a rule.", lineNumber); 
                } else if (late && token!=='no' && token!=='random' && token!=='randomdir') { 
                    logError("Movements cannot appear in late rules.", lineNumber); 
                  } else { 
                    curcell.push(token); 
                } 
            } else if (token == '|') { 
                if (!incellrow) { 
                    logWarning('Janky syntax.  "|" should only be used inside cell rows (the square brackety bits).',lineNumber); 
                } else if (curcell.length % 2 == 1) { 
                    logError('In a rule, if you specify a force, it has to act on an object.', lineNumber); 
                } else { 
                    curcellrow.push(curcell); 
                    curcell = []; 
                } 
            } else if (token === ']') { 
                 
                bracketbalance--; 
                if(bracketbalance<0){ 
                    logWarning("Multiple closing brackets without corresponding opening brackets.  Something fishy here.  Every '[' has to be closed by a ']', and you can't nest them.", lineNumber); 
                } 
 
                if (curcell.length % 2 == 1) { 
                    if (curcell[0]==='...') { 
                        logError('Cannot end a rule with ellipses.', lineNumber); 
                    } else { 
                        logError('In a rule, if you specify a force, it has to act on an object.', lineNumber); 
                    } 
                } else { 
                    curcellrow.push(curcell); 
                    curcell = []; 
                } 
 
                if (rhs) { 
                    rhs_cells.push(curcellrow); 
                } else { 
                    lhs_cells.push(curcellrow); 
                } 
                curcellrow = []; 
                incellrow = false; 
            } else if (token === '->') { 
                if (incellrow) { 
                    logError('Encountered an unexpected "->" inside square brackets.  It\'s used to separate states, it has no place inside them >:| .', lineNumber); 
                } else if (rhs) { 
                    logError('Error, you can only use "->" once in a rule; it\'s used to separate before and after states.', lineNumber); 
                }  else { 
                    rhs = true; 
                } 
            } else if (state.names.indexOf(token) >= 0) { 
                if (!incellrow) { 
                     logWarning("Invalid token "+token.toUpperCase() +". Object names should only be used within cells (square brackets).", lineNumber); 
                 } 
                 else if (curcell.length % 2 == 0) { 
                    curcell.push(''); 
                    curcell.push(token); 
                } else if (curcell.length % 2 == 1) { 
                    curcell.push(token); 
                } 
            } else if (token==='...') { 
                if (!incellrow) { 
                     logWarning("Invalid syntax, ellipses should only be used within cells (square brackets).", lineNumber); 
                 } else { 
                     curcell.push(token); 
                     curcell.push(token); 
                 } 
            } else if (commandwords.indexOf(token.toLowerCase())>=0) { 
                if (rhs===false) { 
                    logError("Commands should only appear at the end of rules, not in or before the pattern-detection/-replacement sections.", lineNumber); 
                } else if (incellrow || rightBracketToRightOf(tokens,i)){//only a warning for legacy support reasons. 
                    logWarning("Commands should only appear at the end of rules, not in or before the pattern-detection/-replacement sections.", lineNumber); 
                } 
                if (token.toLowerCase()==='message') { 
                    var messageIndex = findIndexAfterToken(origLine,tokens,i); 
                    var messageStr = origLine.substring(messageIndex).trim(); 
                    if (messageStr===""){ 
                        messageStr=" "; 
                        //needs to be nonempty or the system gets confused and thinks it's a whole level message rather than an interstitial. 
                    } 
                    commands.push([token.toLowerCase(), messageStr]); 
                    i=tokens.length; 
                }else if (token.toLowerCase()==='goto') { 
                    var messageIndex = findIndexAfterToken(origLine,tokens,i); 
                    var messageStr = origLine.substring(messageIndex).trim(); 
                    if (messageStr===""){ 
                        messageStr=" "; 
                        //needs to be nonempty or the system gets confused and thinks it's a whole level message rather than an interstitial. 
                    } 
                    commands.push([token.toLowerCase(), messageStr]); 
                    i=tokens.length; 
                } else if (twiddleable_params.includes(token.toLowerCase())) { 
                    if (!state.metadata.includes("runtime_metadata_twiddling")) { 
                        logError("You can only change a flag at runtime if you have the 'runtime_metadata_twiddling' prelude flag defined!",lineNumber) 
                    } else { 
                        var messageIndex = findIndexAfterToken(origLine,tokens,i); 
                        var messageStr = origLine.substring(messageIndex).trim(); 
                        if (messageStr===""){ 
                            messageStr=" "; 
                            //needs to be nonempty or the system gets confused and thinks it's a whole level message rather than an interstitial. 
                        } 
                        commands.push([token.toLowerCase(), messageStr]); 
                    } 
                    i=tokens.length; 
                }  else { 
                    commands.push([token.toLowerCase()]); 
                } 
            } else { 
                logError('Error, malformed cell rule - was looking for cell contents, but found "' + token + '".  What am I supposed to do with this, eh, please tell me that.', lineNumber); 
            } 
        } 
 
    } 
} 
 
if (lhs_cells.length != rhs_cells.length) { 
    if (commands.length>0&&rhs_cells.length==0) { 
        //ok 
    } else { 
        logError('Error, when specifying a rule, the number of matches (square bracketed bits) on the left hand side of the arrow must equal the number on the right', lineNumber); 
    } 
} else { 
    for (var i = 0; i < lhs_cells.length; i++) { 
        if (lhs_cells[i].length != rhs_cells[i].length) { 
            logError('In a rule, each pattern to match on the left must have a corresponding pattern on the right of equal length (number of cells).', lineNumber); 
            state.invalid=true; 
        } 
        if (lhs_cells[i].length == 0) { 
            logError("You have an totally empty pattern on the left-hand side.  This will match *everything*.  You certainly don't want this."); 
        } 
    } 
} 
 
if (lhs_cells.length == 0) { 
    logError('This rule refers to nothing.  What the heck? :O', lineNumber); 
} 
 
var rule_line = { 
    directions: directions, 
    lhs: lhs_cells, 
    rhs: rhs_cells, 
    lineNumber: lineNumber, 
    late: late, 
    rigid: rigid, 
    groupNumber: groupNumber, 
    commands: commands, 
    randomRule: randomRule, 
    globalRule: globalRule 
}; 
 
    if (directionalRule(rule_line) === false && rule_line.directions.length>1) { 
        rule_line.directions.splice(1); 
    } 
 
//next up - replace relative directions with absolute direction 
 
return rule_line; 
} 
 
function deepCloneHS(HS) { 
    var cloneHS = HS.map(function(arr) { return arr.map(function(deepArr) { return deepArr.slice(); }); }); 
    return cloneHS; 
} 
 
function deepCloneRule(rule) { 
 var clonedRule = { 
  direction: rule.direction, 
  lhs: deepCloneHS(rule.lhs), 
  rhs: deepCloneHS(rule.rhs), 
  lineNumber: rule.lineNumber, 
  late: rule.late, 
  rigid: rule.rigid, 
  groupNumber: rule.groupNumber, 
  commands:rule.commands, 
  randomRule:rule.randomRule, 
  globalRule:rule.globalRule 
 }; 
 return clonedRule; 
} 
 
function rulesToArray(state) { 
    var oldrules = state.rules; 
    var rules = []; 
    var loops = []; 
    for (var i = 0; i < oldrules.length; i++) { 
        var lineNumber = oldrules[i][1]; 
        var newrule = processRuleString(oldrules[i], state, rules); 
        if (newrule.bracket !== undefined) { 
            loops.push([lineNumber, newrule.bracket]); 
            continue; 
        } 
        rules.push(newrule); 
    } 
    state.loops = loops; 
 
    //now expand out rules with multiple directions 
    var rules2 = []; 
    for (var i = 0; i < rules.length; i++) { 
        var rule = rules[i]; 
        var ruledirs = rule.directions; 
        for (var j = 0; j < ruledirs.length; j++) { 
            var dir = ruledirs[j]; 
            if (dir in directionaggregates && directionalRule(rule)) { 
                var dirs = directionaggregates[dir]; 
                for (var k = 0; k < dirs.length; k++) { 
                    var modifiedrule = deepCloneRule(rule); 
                    modifiedrule.direction = dirs[k]; 
                    rules2.push(modifiedrule); 
                } 
            } else { 
                var modifiedrule = deepCloneRule(rule); 
                modifiedrule.direction = dir; 
                rules2.push(modifiedrule); 
            } 
        } 
    } 
 
    for (var i = 0; i < rules2.length; i++) { 
        var rule = rules2[i]; 
        //remove relative directions 
        convertRelativeDirsToAbsolute(rule); 
        //optional: replace up/left rules with their down/right equivalents 
        rewriteUpLeftRules(rule); 
        //replace aggregates with what they mean 
        atomizeAggregates(state, rule); 
 
        if (state.invalid){ 
            return; 
        } 
         
        //replace synonyms with what they mean 
        rephraseSynonyms(state, rule); 
    } 
 
    var rules3 = []; 
    //expand property rules 
    for (var i = 0; i < rules2.length; i++) { 
        var rule = rules2[i]; 
        rules3 = rules3.concat(concretizeMovingRule(state, rule, rule.lineNumber)); 
    } 
 
    var rules4 = []; 
    for (var i = 0; i < rules3.length; i++) { 
        var rule = rules3[i]; 
        rules4 = rules4.concat(concretizePropertyRule(state, rule, rule.lineNumber)); 
 
    } 
 
    for (i=0;i<rules4.length;i++){ 
        makeSpawnedObjectsStationary(state,rules4[i],rule.lineNumber); 
    } 
    state.rules = rules4; 
} 
 
function containsEllipsis(rule) { 
    for (var i = 0; i < rule.lhs.length; i++) { 
        for (var j = 0; j < rule.lhs[i].length; j++) { 
            if (rule.lhs[i][j][1] === '...') 
                return true; 
        } 
    } 
    return false; 
} 
 
function rewriteUpLeftRules(rule) { 
    if (containsEllipsis(rule)) { 
        return; 
    } 
 
    if (rule.direction == 'up') { 
        rule.direction = 'down'; 
    } else if (rule.direction == 'left') { 
        rule.direction = 'right'; 
    } else { 
        return; 
    } 
 
    for (var i = 0; i < rule.lhs.length; i++) { 
        rule.lhs[i].reverse(); 
        if (rule.rhs.length > 0) { 
            rule.rhs[i].reverse(); 
        } 
    } 
} 
 
//expands all properties to list of all things it could be, filterio 
function getPossibleObjectsFromCell(state, cell) { 
    var result = []; 
    for (var j = 0; j < cell.length; j += 2) { 
        var dir = cell[j]; 
        var name = cell[j + 1]; 
        if (name in state.objects){ 
            result.push(name); 
        } 
        else if (name in state.propertiesDict) { 
            var aliases = state.propertiesDict[name]; 
            for (var k = 0; k < aliases.length; k++) { 
                var alias = aliases[k]; 
                result.push(alias); 
            }         
        } 
    } 
    return result; 
} 
 
function getPropertiesFromCell(state, cell) { 
    var result = []; 
    for (var j = 0; j < cell.length; j += 2) { 
        var dir = cell[j]; 
        var name = cell[j + 1]; 
        if (dir == "random") { 
            continue; 
        } 
        if (name in state.propertiesDict) { 
            result.push(name); 
        } 
    } 
    return result; 
} 
 
//returns you a list of object names in that cell that're moving 
function getMovings(state, cell) { 
    var result = []; 
    for (var j = 0; j < cell.length; j += 2) { 
        var dir = cell[j]; 
        var name = cell[j + 1]; 
        if (dir in directionaggregates) { 
            result.push([name, dir]); 
        } 
    } 
    return result; 
} 
 
function concretizePropertyInCell(cell, property, concreteType) { 
    for (var j = 0; j < cell.length; j += 2) { 
        if (cell[j + 1] === property && cell[j] !== "random") { 
            cell[j + 1] = concreteType; 
        } 
    } 
} 
 
function concretizeMovingInCell(cell, ambiguousMovement, nameToMove, concreteDirection) { 
    for (var j = 0; j < cell.length; j += 2) { 
        if (cell[j] === ambiguousMovement && cell[j + 1] === nameToMove) { 
            cell[j] = concreteDirection; 
        } 
    } 
} 
 
function concretizeMovingInCellByAmbiguousMovementName(cell, ambiguousMovement, concreteDirection) { 
    for (var j = 0; j < cell.length; j += 2) { 
        if (cell[j] === ambiguousMovement) { 
            cell[j] = concreteDirection; 
        } 
    } 
} 
 
function expandNoPrefixedProperties(state, cell) { 
    var expanded = []; 
    for (var i = 0; i < cell.length; i += 2) { 
        var dir = cell[i]; 
        var name = cell[i + 1]; 
 
        if (dir === 'no' && (name in state.propertiesDict)) { 
            var aliases = state.propertiesDict[name]; 
            for (var j = 0; j < aliases.length; j++) { 
                var alias = aliases[j]; 
                expanded.push(dir); 
                expanded.push(alias); 
            } 
        } else { 
            expanded.push(dir); 
            expanded.push(name); 
        } 
    } 
    return expanded; 
} 
 
function concretizePropertyRule(state, rule, lineNumber) { 
 
    //step 1, rephrase rule to change "no flying" to "no cat no bat" 
    for (var i = 0; i < rule.lhs.length; i++) { 
        var cur_cellrow_l = rule.lhs[i]; 
        for (var j = 0; j < cur_cellrow_l.length; j++) { 
            cur_cellrow_l[j] = expandNoPrefixedProperties(state, cur_cellrow_l[j]); 
            if (rule.rhs.length > 0) 
                rule.rhs[i][j] = expandNoPrefixedProperties(state, rule.rhs[i][j]); 
        } 
    } 
 
    //are there any properties we could avoid processing? 
    // e.g. [> player | movable] -> [> player | > movable], 
    //   doesn't need to be split up (assuming single-layer player/block aggregates) 
 
    // we can't manage this if they're being used to disambiguate 
    var ambiguousProperties = {}; 
 
    for (var j = 0; j < rule.rhs.length; j++) { 
        var row_l = rule.lhs[j]; 
        var row_r = rule.rhs[j]; 
        for (var k = 0; k < row_r.length; k++) { 
            var properties_l = getPropertiesFromCell(state, row_l[k]); 
            var properties_r = getPropertiesFromCell(state, row_r[k]); 
            for (var prop_n = 0; prop_n < properties_r.length; prop_n++) { 
                var property = properties_r[prop_n]; 
                if (properties_l.indexOf(property) == -1) { 
                    ambiguousProperties[property] = true; 
                } 
            } 
        } 
    } 
 
    var shouldremove; 
    var result = [rule]; 
    var modified = true; 
    while (modified) { 
        modified = false; 
        for (var i = 0; i < result.length; i++) { 
            //only need to iterate through lhs 
            var cur_rule = result[i]; 
            shouldremove = false; 
            for (var j = 0; j < cur_rule.lhs.length && !shouldremove; j++) { 
                var cur_rulerow = cur_rule.lhs[j]; 
                for (var k = 0; k < cur_rulerow.length && !shouldremove; k++) { 
                    var cur_cell = cur_rulerow[k]; 
                    var properties = getPropertiesFromCell(state, cur_cell); 
                    for (var prop_n = 0; prop_n < properties.length; ++prop_n) { 
                        var property = properties[prop_n]; 
 
                        if (state.propertiesSingleLayer.hasOwnProperty(property) && 
                            ambiguousProperties[property] !== true) { 
                            // we don't need to explode this property 
                            continue; 
                        } 
 
                        var aliases = state.propertiesDict[property]; 
 
                        shouldremove = true; 
                        modified = true; 
 
                        //just do the base property, let future iterations take care of the others 
 
                        for (var l = 0; l < aliases.length; l++) { 
                            var concreteType = aliases[l]; 
                            var newrule = deepCloneRule(cur_rule); 
                            newrule.propertyReplacement = {}; 
                            for (var prop in cur_rule.propertyReplacement) { 
                                if (cur_rule.propertyReplacement.hasOwnProperty(prop)) { 
                                    var propDat = cur_rule.propertyReplacement[prop]; 
                                    newrule.propertyReplacement[prop] = [propDat[0], propDat[1]]; 
                                } 
                            } 
 
                            concretizePropertyInCell(newrule.lhs[j][k], property, concreteType); 
                            if (newrule.rhs.length > 0) { 
                                concretizePropertyInCell(newrule.rhs[j][k], property, concreteType); //do for the corresponding rhs cell as well 
                            } 
 
                            if (newrule.propertyReplacement[property] === undefined) { 
                                newrule.propertyReplacement[property] = [concreteType, 1]; 
                            } else { 
                                newrule.propertyReplacement[property][1] = newrule.propertyReplacement[property][1] + 1; 
                            } 
 
                            result.push(newrule); 
                        } 
 
                        break; 
                    } 
                } 
            } 
            if (shouldremove) { 
                result.splice(i, 1); 
                i--; 
            } 
        } 
    } 
 
 
    for (var i = 0; i < result.length; i++) { 
        //for each rule 
        var cur_rule = result[i]; 
        if (cur_rule.propertyReplacement === undefined) { 
            continue; 
        } 
 
        //for each property replacement in that rule 
        for (var property in cur_rule.propertyReplacement) { 
            if (cur_rule.propertyReplacement.hasOwnProperty(property)) { 
                var replacementInfo = cur_rule.propertyReplacement[property]; 
                var concreteType = replacementInfo[0]; 
                var occurrenceCount = replacementInfo[1]; 
                if (occurrenceCount === 1) { 
                    //do the replacement 
                    for (var j = 0; j < cur_rule.rhs.length; j++) { 
                        var cellRow_rhs = cur_rule.rhs[j]; 
                        for (var k = 0; k < cellRow_rhs.length; k++) { 
                            var cell = cellRow_rhs[k]; 
                            concretizePropertyInCell(cell, property, concreteType); 
                        } 
                    } 
                } 
            } 
        } 
    } 
 
    //if any properties remain on the RHSes, bleep loudly 
    var rhsPropertyRemains = ''; 
    for (var i = 0; i < result.length; i++) { 
        var cur_rule = result[i]; 
        delete result.propertyReplacement; 
        for (var j = 0; j < cur_rule.rhs.length; j++) { 
            var cur_rulerow = cur_rule.rhs[j]; 
            for (var k = 0; k < cur_rulerow.length; k++) { 
                var cur_cell = cur_rulerow[k]; 
                var properties = getPropertiesFromCell(state, cur_cell); 
                for (var prop_n = 0; prop_n < properties.length; prop_n++) { 
                    if (ambiguousProperties.hasOwnProperty(properties[prop_n])) { 
                        rhsPropertyRemains = properties[prop_n]; 
                    } 
                } 
            } 
        } 
    } 
 
 
    if (rhsPropertyRemains.length > 0) { 
        logError('This rule has a property on the right-hand side, \"' + rhsPropertyRemains.toUpperCase() + "\", that can't be inferred from the left-hand side.  (either for every property on the right there has to be a corresponding one on the left in the same cell, OR, if there's a single occurrence of a particular property name on the left, all properties of the same name on the right are assumed to be the same).", lineNumber); 
    } 
 
    return result; 
} 
 
function makeSpawnedObjectsStationary(state,rule,lineNumber){ 
    //movement not getting correctly cleared from tile #492 
    //[ > Player | ] -> [ Crate | Player ] if there was a player already in the second cell, it's not replaced with a stationary player. 
    //if there are properties remaining by this stage, just ignore them ( c.f. "[ >  Moveable | Moveable ] -> [ > Moveable | > Moveable ]" in block faker, what's left in this form) - this only happens IIRC when the properties span a single layer so it's) 
    //if am object without moving-annotations appears on the RHS, and that object is not present on the lhs (either explicitly as an object, or implicitly in a property), add a 'stationary' 
    if (rule.late){ 
        return; 
    } 
 
    for (var j = 0; j < rule.rhs.length; j++) { 
        var row_l = rule.lhs[j]; 
        var row_r = rule.rhs[j]; 
        for (var k = 0; k < row_r.length; k++) { 
            var cell=row_r[k]; 
 
            //this is super intricate. uff.  
            var objects_l = getPossibleObjectsFromCell(state, row_l[k]); 
            var layers = objects_l.map(n=>state.objects[n].layer); 
            for (var l = 0; l < cell.length; l += 2) { 
                var dir = cell[l]; 
                if (dir!==""){ 
                    continue; 
                } 
                var name = cell[l + 1]; 
                if (name in state.propertiesDict || objects_l.indexOf(name)>=0){ 
                    continue; 
                } 
                var r_layer = state.objects[name].layer; 
                if (layers.indexOf(r_layer)===-1){ 
                    cell[l]='stationary'; 
                } 
            } 
        } 
    } 
 
} 
 
function concretizeMovingRule(state, rule, lineNumber) { 
 
    var shouldremove; 
    var result = [rule]; 
    var modified = true; 
    while (modified) { 
        modified = false; 
        for (var i = 0; i < result.length; i++) { 
            //only need to iterate through lhs 
            var cur_rule = result[i]; 
            shouldremove = false; 
            for (var j = 0; j < cur_rule.lhs.length; j++) { 
                var cur_rulerow = cur_rule.lhs[j]; 
                for (var k = 0; k < cur_rulerow.length; k++) { 
                    var cur_cell = cur_rulerow[k]; 
                    var movings = getMovings(state, cur_cell); //finds aggregate directions 
                    if (movings.length > 0) { 
                        shouldremove = true; 
                        modified = true; 
 
                        //just do the base property, let future iterations take care of the others 
                        var cand_name = movings[0][0]; 
                        var ambiguous_dir = movings[0][1]; 
                        var concreteDirs = directionaggregates[ambiguous_dir]; 
                        for (var l = 0; l < concreteDirs.length; l++) { 
                            var concreteDirection = concreteDirs[l]; 
                            var newrule = deepCloneRule(cur_rule); 
 
                            //deep copy replacements 
                            newrule.movingReplacement = {}; 
                            for (var moveTerm in cur_rule.movingReplacement) { 
                                if (cur_rule.movingReplacement.hasOwnProperty(moveTerm)) { 
                                    var moveDat = cur_rule.movingReplacement[moveTerm]; 
                                    newrule.movingReplacement[moveTerm] = [moveDat[0], moveDat[1], moveDat[2],moveDat[3],moveDat[4],moveDat[5]]; 
                                } 
                            } 
                            newrule.aggregateDirReplacement = {}; 
                            for (var moveTerm in cur_rule.aggregateDirReplacement) { 
                                if (cur_rule.aggregateDirReplacement.hasOwnProperty(moveTerm)) { 
                                    var moveDat = cur_rule.aggregateDirReplacement[moveTerm]; 
                                    newrule.aggregateDirReplacement[moveTerm] = [moveDat[0], moveDat[1], moveDat[2]]; 
                                }                                 
                            } 
 
                            concretizeMovingInCell(newrule.lhs[j][k], ambiguous_dir, cand_name, concreteDirection); 
                            if (newrule.rhs.length > 0) { 
                                concretizeMovingInCell(newrule.rhs[j][k], ambiguous_dir, cand_name, concreteDirection); //do for the corresponding rhs cell as well 
                            } 
 
                            if (newrule.movingReplacement[cand_name+ambiguous_dir] === undefined) { 
                                newrule.movingReplacement[cand_name+ambiguous_dir] = [concreteDirection, 1, ambiguous_dir,cand_name,j,k]; 
                            } else { 
                                var mr = newrule.movingReplacement[cand_name+ambiguous_dir]; 
                                if (j!==mr[4] || k!==mr[5]){ 
                                    mr[1] = mr[1] + 1; 
                                } 
                            } 
                            if (newrule.aggregateDirReplacement[ambiguous_dir] === undefined) { 
                                newrule.aggregateDirReplacement[ambiguous_dir] = [concreteDirection, 1, ambiguous_dir]; 
                            } else { 
                                newrule.aggregateDirReplacement[ambiguous_dir][1] = newrule.aggregateDirReplacement[ambiguous_dir][1] + 1; 
                            } 
 
                            result.push(newrule); 
                        } 
                    } 
                } 
            } 
            if (shouldremove) { 
                result.splice(i, 1); 
                i--; 
            } 
        } 
    } 
 
 
    for (var i = 0; i < result.length; i++) { 
        //for each rule 
        var cur_rule = result[i]; 
        if (cur_rule.movingReplacement === undefined) { 
            continue; 
        } 
        var ambiguous_movement_dict = {}; 
        //strict first - matches movement direction to objects 
        //for each property replacement in that rule 
        for (var cand_name in cur_rule.movingReplacement) { 
            if (cur_rule.movingReplacement.hasOwnProperty(cand_name)) { 
                var replacementInfo = cur_rule.movingReplacement[cand_name]; 
                var concreteMovement = replacementInfo[0]; 
                var occurrenceCount = replacementInfo[1]; 
                var ambiguousMovement = replacementInfo[2]; 
                var ambiguousMovement_attachedObject = replacementInfo[3]; 
 
                if (occurrenceCount === 1) { 
                    //do the replacement 
                    for (var j = 0; j < cur_rule.rhs.length; j++) { 
                        var cellRow_rhs = cur_rule.rhs[j]; 
                        for (var k = 0; k < cellRow_rhs.length; k++) { 
                            var cell = cellRow_rhs[k]; 
                            concretizeMovingInCell(cell, ambiguousMovement, ambiguousMovement_attachedObject, concreteMovement); 
                        } 
                    } 
                } 
 
            } 
        } 
 
        //I don't fully understand why the following part is needed (and I wrote this yesterday), but it's not obviously malicious. 
        var ambiguous_movement_names_dict = {}; 
        for (var cand_name in cur_rule.aggregateDirReplacement) { 
            if (cur_rule.aggregateDirReplacement.hasOwnProperty(cand_name)) { 
                var replacementInfo = cur_rule.aggregateDirReplacement[cand_name]; 
                var concreteMovement = replacementInfo[0]; 
                var occurrenceCount = replacementInfo[1]; 
                var ambiguousMovement = replacementInfo[2]; 
                //are both the following boolean bits necessary, or just the latter? ah well, no harm it seems. 
                if ((ambiguousMovement in ambiguous_movement_names_dict) || (occurrenceCount !== 1)) { 
                    ambiguous_movement_names_dict[ambiguousMovement] = "INVALID"; 
                } else { 
                    ambiguous_movement_names_dict[ambiguousMovement] = concreteMovement 
                } 
            } 
        }         
 
        //for each ambiguous word, if there's a single ambiguous movement specified in the whole lhs, then replace that wholesale 
        for (var ambiguousMovement in ambiguous_movement_dict) { 
            if (ambiguous_movement_dict.hasOwnProperty(ambiguousMovement) && ambiguousMovement !== "INVALID") { 
                concreteMovement = ambiguous_movement_dict[ambiguousMovement]; 
                if (concreteMovement === "INVALID") { 
                    continue; 
                } 
                for (var j = 0; j < cur_rule.rhs.length; j++) { 
                    var cellRow_rhs = cur_rule.rhs[j]; 
                    for (var k = 0; k < cellRow_rhs.length; k++) { 
                        var cell = cellRow_rhs[k]; 
                        concretizeMovingInCellByAmbiguousMovementName(cell, ambiguousMovement, concreteMovement); 
                    } 
                } 
            } 
        } 
 
         
        //further replacements - if a movement word appears once on the left, can use to disambiguate remaining ones on the right 
        for (var ambiguousMovement in ambiguous_movement_names_dict) { 
            if (ambiguous_movement_names_dict.hasOwnProperty(ambiguousMovement) && ambiguousMovement !== "INVALID") { 
                concreteMovement = ambiguous_movement_names_dict[ambiguousMovement]; 
                if (concreteMovement === "INVALID") { 
                    continue; 
                } 
                for (var j = 0; j < cur_rule.rhs.length; j++) { 
                    var cellRow_rhs = cur_rule.rhs[j]; 
                    for (var k = 0; k < cellRow_rhs.length; k++) { 
                        var cell = cellRow_rhs[k]; 
                        concretizeMovingInCellByAmbiguousMovementName(cell, ambiguousMovement, concreteMovement); 
                    } 
                } 
            } 
        } 
    } 
 
    //if any properties remain on the RHSes, bleep loudly 
    var rhsAmbiguousMovementsRemain = ''; 
    for (var i = 0; i < result.length; i++) { 
        var cur_rule = result[i]; 
        delete result.movingReplacement; 
        for (var j = 0; j < cur_rule.rhs.length; j++) { 
            var cur_rulerow = cur_rule.rhs[j]; 
            for (var k = 0; k < cur_rulerow.length; k++) { 
                var cur_cell = cur_rulerow[k]; 
                var movings = getMovings(state, cur_cell); 
                if (movings.length > 0) { 
                    rhsAmbiguousMovementsRemain = movings[0][1]; 
                } 
            } 
        } 
    } 
 
 
    if (rhsAmbiguousMovementsRemain.length > 0) { 
        logError('This rule has an ambiguous movement on the right-hand side, \"' + rhsAmbiguousMovementsRemain + "\", that can't be inferred from the left-hand side.  (either for every ambiguous movement associated to an entity on the right there has to be a corresponding one on the left attached to the same entity, OR, if there's a single occurrence of a particular ambiguous movement on the left, all properties of the same movement attached to the same object on the right are assumed to be the same (or something like that)).", lineNumber); 
        state.invalid=true; 
    } 
 
    return result; 
} 
 
function rephraseSynonyms(state, rule) { 
    for (var i = 0; i < rule.lhs.length; i++) { 
        var cellrow_l = rule.lhs[i]; 
        var cellrow_r = rule.rhs[i]; 
        for (var j = 0; j < cellrow_l.length; j++) { 
            var cell_l = cellrow_l[j]; 
            for (var k = 1; k < cell_l.length; k += 2) { 
                var name = cell_l[k]; 
                if (name in state.synonymsDict) { 
                    cell_l[k] = state.synonymsDict[cell_l[k]]; 
                } 
            } 
            if (rule.rhs.length > 0) { 
                var cell_r = cellrow_r[j]; 
                for (var k = 1; k < cell_r.length; k += 2) { 
                    var name = cell_r[k]; 
                    if (name in state.synonymsDict) { 
                        cell_r[k] = state.synonymsDict[cell_r[k]]; 
                    } 
                } 
            } 
        } 
    } 
} 
 
function atomizeAggregates(state, rule) { 
    for (var i = 0; i < rule.lhs.length; i++) { 
        var cellrow = rule.lhs[i]; 
        for (var j = 0; j < cellrow.length; j++) { 
            var cell = cellrow[j]; 
            atomizeCellAggregates(state, cell, rule.lineNumber); 
        } 
    } 
    for (var i = 0; i < rule.rhs.length; i++) { 
        var cellrow = rule.rhs[i]; 
        for (var j = 0; j < cellrow.length; j++) { 
            var cell = cellrow[j]; 
            atomizeCellAggregates(state, cell, rule.lineNumber); 
        } 
    } 
} 
 
function atomizeCellAggregates(state, cell, lineNumber) { 
    for (var i = 0; i < cell.length; i += 2) { 
        var dir = cell[i]; 
        var c = cell[i + 1]; 
        if (c in state.aggregatesDict) { 
            if (dir === 'no') { 
                logError("You cannot use 'no' to exclude the aggregate object " + c.toUpperCase() + " (defined using 'AND'), only regular objects, or properties (objects defined using 'OR').  If you want to do this, you'll have to write it out yourself the long way.", lineNumber); 
            } 
            var equivs = state.aggregatesDict[c]; 
            cell[i + 1] = equivs[0]; 
            for (var j = 1; j < equivs.length; j++) { 
                cell.push(cell[i]); //push the direction 
                cell.push(equivs[j]); 
            } 
        } 
    } 
} 
 
function convertRelativeDirsToAbsolute(rule) { 
    var forward = rule.direction; 
    for (var i = 0; i < rule.lhs.length; i++) { 
        var cellrow = rule.lhs[i]; 
        for (var j = 0; j < cellrow.length; j++) { 
            var cell = cellrow[j]; 
            absolutifyRuleCell(forward, cell); 
        } 
    } 
    for (var i = 0; i < rule.rhs.length; i++) { 
        var cellrow = rule.rhs[i]; 
        for (var j = 0; j < cellrow.length; j++) { 
            var cell = cellrow[j]; 
            absolutifyRuleCell(forward, cell); 
        } 
    } 
} 
 
var relativeDirs = ['^', 'v', '<', '>', 'parallel', 'perpendicular']; //used to index the following 
//I use _par/_perp just to keep track of providence for replacement purposes later. 
var relativeDict = { 
    'right': ['up', 'down', 'left', 'right', 'horizontal_par', 'vertical_perp'], 
    'up': ['left', 'right', 'down', 'up', 'vertical_par', 'horizontal_perp'], 
    'down': ['right', 'left', 'up', 'down', 'vertical_par', 'horizontal_perp'], 
    'left': ['down', 'up', 'right', 'left', 'horizontal_par', 'vertical_perp'] 
}; 
 
function absolutifyRuleCell(forward, cell) { 
    for (var i = 0; i < cell.length; i += 2) { 
        var c = cell[i]; 
        var index = relativeDirs.indexOf(c); 
        if (index >= 0) { 
            cell[i] = relativeDict[forward][index]; 
        } 
    } 
} 
/* 
 direction mask 
 UP parseInt('%1', 2); 
 DOWN parseInt('0', 2); 
 LEFT parseInt('0', 2); 
 RIGHT parseInt('0', 2); 
 ?  parseInt('', 2); 
*/ 
 
var dirMasks = { 
    'up': parseInt('00001', 2), 
    'down': parseInt('00010', 2), 
    'left': parseInt('00100', 2), 
    'right': parseInt('01000', 2), 
    'moving': parseInt('01111', 2), 
    'no': parseInt('00011', 2), 
    'randomdir': parseInt('00101', 2), 
    'random': parseInt('10010', 2), 
    'action': parseInt('10000', 2), 
    '': parseInt('00000', 2) 
}; 
 
function rulesToMask(state) { 
    /* 
 
    */ 
    var layerCount = state.collisionLayers.length; 
    var layerTemplate = []; 
    for (var i = 0; i < layerCount; i++) { 
        layerTemplate.push(null); 
    } 
 
    for (var i = 0; i < state.rules.length; i++) { 
        var rule = state.rules[i]; 
        for (var j = 0; j < rule.lhs.length; j++) { 
            var cellrow_l = rule.lhs[j]; 
            var cellrow_r = rule.rhs[j]; 
            for (var k = 0; k < cellrow_l.length; k++) { 
                var cell_l = cellrow_l[k]; 
                var layersUsed_l = layerTemplate.concat([]); 
                var objectsPresent = new BitVec(STRIDE_OBJ); 
                var objectsMissing = new BitVec(STRIDE_OBJ); 
                var anyObjectsPresent = []; 
                var movementsPresent = new BitVec(STRIDE_MOV); 
                var movementsMissing = new BitVec(STRIDE_MOV); 
 
                var objectlayers_l = new BitVec(STRIDE_MOV); 
                for (var l = 0; l < cell_l.length; l += 2) { 
                    var object_dir = cell_l[l]; 
                    if (object_dir === '...') { 
                        objectsPresent = ellipsisPattern; 
                        if (cell_l.length !== 2) { 
                            logError("You can't have anything in with an ellipsis. Sorry.", rule.lineNumber); 
                        } else if ((k === 0) || (k === cellrow_l.length - 1)) { 
                            logError("There's no point in putting an ellipsis at the very start or the end of a rule", rule.lineNumber); 
                        } else if (rule.rhs.length > 0) { 
                            var rhscell = cellrow_r[k]; 
                            if (rhscell.length !== 2 || rhscell[0] !== '...') { 
                                logError("An ellipsis on the left must be matched by one in the corresponding place on the right.", rule.lineNumber); 
                            } 
                        } 
                        break; 
                    } else if (object_dir === 'random') { 
                        logError("'random' cannot be matched on the left-hand side, it can only appear on the right", rule.lineNumber); 
                        continue; 
                    } 
 
                    var object_name = cell_l[l + 1]; 
                    var object = state.objects[object_name]; 
                    var objectMask = state.objectMasks[object_name]; 
                    if (object) { 
                        var layerIndex = object.layer | 0; 
                    } else { 
                        var layerIndex = state.propertiesSingleLayer[object_name]; 
                    } 
 
                    if (typeof(layerIndex) === "undefined") { 
                        logError("Oops!  " + object_name.toUpperCase() + " not assigned to a layer.", rule.lineNumber); 
                    } 
 
                    if (object_dir === 'no') { 
                        objectsMissing.ior(objectMask); 
                    } else { 
                        var existingname = layersUsed_l[layerIndex]; 
                        if (existingname !== null) { 
                            rule.discard = [object_name.toUpperCase(), existingname.toUpperCase()]; 
                        } 
 
                        layersUsed_l[layerIndex] = object_name; 
 
                        if (object) { 
                            objectsPresent.ior(objectMask); 
                            objectlayers_l.ishiftor(0x1f, 5 * layerIndex); 
                        } else { 
                            anyObjectsPresent.push(objectMask); 
                        } 
 
                        if (object_dir === 'stationary') { 
                            movementsMissing.ishiftor(0x1f, 5 * layerIndex); 
                        } else { 
                            movementsPresent.ishiftor(dirMasks[object_dir], 5 * layerIndex); 
                        } 
                    } 
                } 
 
                if (rule.rhs.length > 0) { 
                    var rhscell = cellrow_r[k]; 
                    var lhscell = cellrow_l[k]; 
                    if (rhscell[0] === '...' && lhscell[0] !== '...') { 
                        logError("An ellipsis on the right must be matched by one in the corresponding place on the left.", rule.lineNumber); 
                    } 
                    for (var l = 0; l < rhscell.length; l += 2) { 
                        var content = rhscell[l]; 
                        if (content === '...') { 
                            if (rhscell.length !== 2) { 
                                logError("You can't have anything in with an ellipsis. Sorry.", rule.lineNumber); 
                            } 
                        } 
                    } 
                } 
 
                if (objectsPresent === ellipsisPattern) { 
                    cellrow_l[k] = ellipsisPattern; 
                    continue; 
                } else { 
                    cellrow_l[k] = new CellPattern([objectsPresent, objectsMissing, anyObjectsPresent, movementsPresent, movementsMissing, null]); 
                } 
 
                //if X no X, then cancel 
                if (objectsPresent.anyBitsInCommon(objectsMissing)){ 
                    //if I'm about the remove the last representative of this line number, throw an error 
                    var ln = rule.lineNumber; 
                    if ( (i>0 && state.rules[i-1].lineNumber===ln) || ( (i+1<state.rules.length) && state.rules[i+1].lineNumber===ln)){ 
                        //all good 
                    } else { 
                        logError('This rule has some content of the form "X no X" which can never match and so the rule is getting removed during compilation.', rule.lineNumber); 
                    } 
                    state.rules.splice(i,1); 
                    i--; 
                    continue; 
                } 
                 
                if (rule.rhs.length === 0) { 
                    continue; 
                } 
 
                var cell_r = cellrow_r[k]; 
                var layersUsed_r = layerTemplate.concat([]); 
                var layersUsedRand_r = layerTemplate.concat([]); 
 
                var objectsClear = new BitVec(STRIDE_OBJ); 
                var objectsSet = new BitVec(STRIDE_OBJ); 
                var movementsClear = new BitVec(STRIDE_MOV); 
                var movementsSet = new BitVec(STRIDE_MOV); 
 
                var objectlayers_r = new BitVec(STRIDE_MOV); 
                var randomMask_r = new BitVec(STRIDE_OBJ); 
                var postMovementsLayerMask_r = new BitVec(STRIDE_MOV); 
                var randomDirMask_r = new BitVec(STRIDE_MOV); 
                for (var l = 0; l < cell_r.length; l += 2) { 
                    var object_dir = cell_r[l]; 
                    var object_name = cell_r[l + 1]; 
 
                    if (object_dir === '...') { 
                        //logError("spooky ellipsis found! (should never hit this)"); 
                        break; 
                    } else if (object_dir === 'random') { 
                        if (object_name in state.objectMasks) { 
                            var mask = state.objectMasks[object_name]; 
                            randomMask_r.ior(mask); 
                            var values; 
                            if (state.propertiesDict.hasOwnProperty(object_name)) { 
                                values = state.propertiesDict[object_name]; 
                            } else { 
                                values = [object_name]; 
                            } 
                            for (var m = 0; m < values.length; m++) { 
                                var subobject = values[m]; 
                                var layerIndex = state.objects[subobject].layer | 0; 
                                var existingname = layersUsed_r[layerIndex]; 
                                if (existingname !== null) { 
                                    var o1 = subobject.toUpperCase(); 
                                    var o2 = existingname.toUpperCase(); 
                                    if (o1 !== o2) { 
                                        logWarning("This rule may try to spawn a " + o1 + " with random, but also requires a " + o2 + " be here, which is on the same layer - they shouldn't be able to coexist!", rule.lineNumber); 
                                    } 
                                } 
 
                                layersUsedRand_r[layerIndex] = subobject; 
                            } 
 
                        } else { 
                            logError('You want to spawn a random "' + object_name.toUpperCase() + '", but I don\'t know how to do that', rule.lineNumber); 
                        } 
                        continue; 
                    } 
 
                    var object = state.objects[object_name]; 
                    var objectMask = state.objectMasks[object_name]; 
                    if (object) { 
                        var layerIndex = object.layer | 0; 
                    } else { 
                        var layerIndex = state.propertiesSingleLayer[object_name]; 
                    } 
 
 
                    if (object_dir == 'no') { 
                        objectsClear.ior(objectMask); 
                    } else { 
                        var existingname = layersUsed_r[layerIndex]; 
                        if (existingname === null) { 
                            existingname = layersUsedRand_r[layerIndex]; 
                        } 
 
                        if (existingname !== null) { 
                            if (rule.hasOwnProperty('discard')) { 
 
                            } else { 
                                logError('Rule matches object types that can\'t overlap: "' + object_name.toUpperCase() + '" and "' + existingname.toUpperCase() + '".', rule.lineNumber); 
                            } 
                        } 
 
                        layersUsed_r[layerIndex] = object_name; 
 
                        if (object_dir.length > 0) { 
                            postMovementsLayerMask_r.ishiftor(0x1f, 5 * layerIndex); 
                        } 
 
                        var layerMask = state.layerMasks[layerIndex]; 
 
                        if (object) { 
                            objectsSet.ibitset(object.id); 
                            objectsClear.ior(layerMask); 
                            objectlayers_r.ishiftor(0x1f, 5 * layerIndex); 
                        } else { 
                            // shouldn't need to do anything here... 
                        } 
                        //possibility - if object not present on lhs in same position, clear movement 
                        if (object_dir === 'stationary') { 
                            movementsClear.ishiftor(0x1f, 5 * layerIndex); 
                        }                 
                        if (object_dir === 'randomdir') { 
                            randomDirMask_r.ishiftor(dirMasks[object_dir], 5 * layerIndex); 
                        } else { 
                            movementsSet.ishiftor(dirMasks[object_dir], 5 * layerIndex); 
                        }; 
                    } 
                } 
 
                //I don't know why these two ifs here are needed. 
                if (!(objectsPresent.bitsSetInArray(objectsSet.data))) { 
                    objectsClear.ior(objectsPresent); // clear out old objects 
                } 
                if (!(movementsPresent.bitsSetInArray(movementsSet.data))) { 
                    movementsClear.ior(movementsPresent); // ... and movements 
                } 
 
                /* 
                for rules like this I want to clear movements on newly-spawned entities 
                    [ >  Player | Crate ] -> [  >  Player | > Crate  ] 
                    [ > Player | ] -> [ Crate | Player ] 
 
                WITHOUT havin this rule remove movements 
                    [ > Player | ] -> [ Crate | Player ] 
                (bug #492) 
                */ 
                
                for (var l = 0; l < layerCount; l++) { 
                    if (layersUsed_l[l] !== null && layersUsed_r[l] === null) { 
                        // a layer matched on the lhs, but not on the rhs 
                        objectsClear.ior(state.layerMasks[l]); 
                        postMovementsLayerMask_r.ishiftor(0x1f, 5 * l); 
                    } 
                } 
 
                objectlayers_l.iclear(objectlayers_r); 
 
                postMovementsLayerMask_r.ior(objectlayers_l); 
                if (!objectsClear.iszero() || !objectsSet.iszero() || !movementsClear.iszero() || !movementsSet.iszero() || !postMovementsLayerMask_r.iszero() || !randomMask_r.iszero() || !randomDirMask_r.iszero()) { 
                    // only set a replacement if something would change 
                    cellrow_l[k].replacement = new CellReplacement([objectsClear, objectsSet, movementsClear, movementsSet, postMovementsLayerMask_r, randomMask_r, randomDirMask_r]); 
                }  
            } 
        } 
    } 
} 
 
function cellRowMasks(rule) { 
    var ruleMasks = []; 
    var lhs = rule[1]; 
    for (var i = 0; i < lhs.length; i++) { 
        var cellRow = lhs[i]; 
        var rowMask = new BitVec(STRIDE_OBJ); 
        for (var j = 0; j < cellRow.length; j++) { 
            if (cellRow[j] === ellipsisPattern) 
                continue; 
            rowMask.ior(cellRow[j].objectsPresent); 
        } 
        ruleMasks.push(rowMask); 
    } 
    return ruleMasks; 
} 
 
function cellRowMasks_Movements(rule){ 
    var ruleMasks_mov = []; 
    var lhs = rule[1]; 
    for (var i = 0; i < lhs.length; i++) { 
        var cellRow = lhs[i]; 
        var rowMask = new BitVec(STRIDE_MOV); 
        for (var j = 0; j < cellRow.length; j++) { 
            if (cellRow[j] === ellipsisPattern) 
                continue; 
            rowMask.ior(cellRow[j].movementsPresent); 
        } 
        ruleMasks_mov.push(rowMask); 
    } 
    return ruleMasks_mov; 
} 
 
function collapseRules(groups) { 
    for (var gn = 0; gn < groups.length; gn++) { 
        var rules = groups[gn]; 
        for (var i = 0; i < rules.length; i++) { 
            var oldrule = rules[i]; 
            var newrule = [0, [], oldrule.rhs.length > 0, oldrule.lineNumber /*ellipses,group number,rigid,commands,randomrule,[cellrowmasks]*/ ]; 
            var ellipses = []; 
            for (var j = 0; j < oldrule.lhs.length; j++) { 
                ellipses.push(false); 
            } 
 
            newrule[0] = dirMasks[oldrule.direction]; 
            for (var j = 0; j < oldrule.lhs.length; j++) { 
                var cellrow_l = oldrule.lhs[j]; 
                for (var k = 0; k < cellrow_l.length; k++) { 
                    if (cellrow_l[k] === ellipsisPattern) { 
                        if (ellipses[j]) { 
                            logError("You can't use two ellipses in a single cell match pattern.  If you really want to, please implement it yourself and send me a patch :) ", oldrule.lineNumber); 
                        } 
                        ellipses[j] = true; 
                    } 
                } 
                newrule[1][j] = cellrow_l; 
            } 
            newrule.push(ellipses); 
            newrule.push(oldrule.groupNumber); 
            newrule.push(oldrule.rigid); 
            newrule.push(oldrule.commands); 
            newrule.push(oldrule.randomRule); 
            newrule.push(cellRowMasks(newrule)); 
            newrule.push(cellRowMasks_Movements(newrule)); 
            newrule.push(oldrule.globalRule); 
            rules[i] = new Rule(newrule); 
        } 
    } 
    matchCache = {}; // clear match cache so we don't slowly leak memory 
} 
 
 
 
function ruleGroupDiscardOverlappingTest(ruleGroup) { 
    if (ruleGroup.length === 0) 
        return; 
 
    for (var i = 0; i < ruleGroup.length; i++) { 
        var rule = ruleGroup[i]; 
        if (rule.hasOwnProperty('discard')) { 
            ruleGroup.splice(i, 1); 
 
            //if rule before isn't of same linenumber, and rule after isn't of same linenumber,  
            //then a rule has been totally erased and you should throw an error! 
            if ( (i===0 || ruleGroup[i-1].lineNumber !==  rule.lineNumber )  
                && (i<ruleGroup.length-1 && ruleGroup[i+1].lineNumber !==  rule.lineNumber) || ruleGroup.length===0) { 
                var example = rule['discard']; 
                 
                logError(example[0] + ' and ' + example[1] + ' can never overlap, but this rule requires that to happen.', rule.lineNumber); 
            } 
            i--; 
        } 
    } 
} 
 
function arrangeRulesByGroupNumber(state) { 
    var aggregates = {}; 
    var aggregates_late = {}; 
    for (var i = 0; i < state.rules.length; i++) { 
        var rule = state.rules[i]; 
        var targetArray = aggregates; 
        if (rule.late) { 
            targetArray = aggregates_late; 
        } 
 
        if (targetArray[rule.groupNumber] == undefined) { 
            targetArray[rule.groupNumber] = []; 
        } 
        targetArray[rule.groupNumber].push(rule); 
    } 
 
    var result = []; 
    for (var groupNumber in aggregates) { 
        if (aggregates.hasOwnProperty(groupNumber)) { 
            var ruleGroup = aggregates[groupNumber]; 
            ruleGroupDiscardOverlappingTest(ruleGroup); 
            if (ruleGroup.length > 0) { 
                result.push(ruleGroup); 
            } 
        } 
    } 
    var result_late = []; 
    for (var groupNumber in aggregates_late) { 
        if (aggregates_late.hasOwnProperty(groupNumber)) { 
            var ruleGroup = aggregates_late[groupNumber]; 
            ruleGroupDiscardOverlappingTest(ruleGroup); 
            if (ruleGroup.length > 0) { 
                result_late.push(ruleGroup); 
            } 
        } 
    } 
    state.rules = result; 
 
    //check that there're no late movements with direction requirements on the lhs 
    state.lateRules = result_late; 
} 
 
function generateRigidGroupList(state) { 
 var rigidGroupIndex_to_GroupIndex=[]; 
 var groupIndex_to_RigidGroupIndex=[]; 
 var groupNumber_to_GroupIndex=[]; 
 var groupNumber_to_RigidGroupIndex=[]; 
 var rigidGroups=[]; 
 for (var i=0;i<state.rules.length;i++) { 
  var ruleset=state.rules[i]; 
  var rigidFound=false; 
  for (var j=0;j<ruleset.length;j++) { 
   var rule=ruleset[j]; 
   if (rule.isRigid) { 
    rigidFound=true; 
   } 
  } 
  rigidGroups[i]=rigidFound; 
  if (rigidFound) { 
   var groupNumber=ruleset[0].groupNumber; 
   groupNumber_to_GroupIndex[groupNumber]=i; 
   var rigid_group_index = rigidGroupIndex_to_GroupIndex.length; 
   groupIndex_to_RigidGroupIndex[i]=rigid_group_index; 
   groupNumber_to_RigidGroupIndex[groupNumber]=rigid_group_index; 
   rigidGroupIndex_to_GroupIndex.push(i); 
  } 
 } 
 if (rigidGroupIndex_to_GroupIndex.length>60) { 
  logError("There can't be more than 60 rigid groups (rule groups containing rigid members).",rules[0][0][3]); 
 } 
 
 state.rigidGroups=rigidGroups; 
 state.rigidGroupIndex_to_GroupIndex=rigidGroupIndex_to_GroupIndex; 
 state.groupNumber_to_RigidGroupIndex=groupNumber_to_RigidGroupIndex; 
 state.groupIndex_to_RigidGroupIndex=groupIndex_to_RigidGroupIndex; 
} 
 
function getMaskFromName(state,name) { 
 var objectMask=new BitVec(STRIDE_OBJ); 
 if (name in state.objects) { 
  var o=state.objects[name]; 
  objectMask.ibitset(o.id); 
 } 
 
 if (name in state.aggregatesDict) { 
  var objectnames = state.aggregatesDict[name]; 
  for(var i=0;i<objectnames.length;i++) { 
   var n=objectnames[i]; 
   var o = state.objects[n]; 
   objectMask.ibitset(o.id); 
  } 
 } 
 
 if (name in state.propertiesDict) { 
  var objectnames = state.propertiesDict[name]; 
  for(var i=0;i<objectnames.length;i++) { 
   var n = objectnames[i]; 
   var o = state.objects[n]; 
   objectMask.ibitset(o.id); 
  } 
 } 
 
 if (name in state.synonymsDict) { 
  var n = state.synonymsDict[name]; 
  var o = state.objects[n]; 
  objectMask.ibitset(o.id); 
 } 
 
 if (!state.metadata.includes("nokeyboard") && objectMask.iszero()) { 
  logErrorNoLine("error, didn't find any object called player, either in the objects section, or the legends section. there must be a player!"); 
 } 
 return objectMask; 
} 
 
function generateMasks(state) { 
    state.playerMask = getMaskFromName(state, 'player'); 
 
    var layerMasks = []; 
    var layerCount = state.collisionLayers.length; 
    for (var layer = 0; layer < layerCount; layer++) { 
        var layerMask = new BitVec(STRIDE_OBJ); 
        for (var j = 0; j < state.objectCount; j++) { 
            var n = state.idDict[j]; 
            var o = state.objects[n]; 
            if (o.layer == layer) { 
                layerMask.ibitset(o.id); 
            } 
        } 
        layerMasks.push(layerMask); 
    } 
    state.layerMasks = layerMasks; 
 
    var objectMask = {}; 
    for (var n in state.objects) { 
        if (state.objects.hasOwnProperty(n)) { 
            var o = state.objects[n]; 
            objectMask[n] = new BitVec(STRIDE_OBJ); 
            objectMask[n].ibitset(o.id); 
        } 
    } 
 
    // Synonyms can depend on properties, and properties can depend on synonyms. 
    // Process them in order by combining & sorting by linenumber. 
 
    var synonyms_and_properties = state.legend_synonyms.concat(state.legend_properties); 
    synonyms_and_properties.sort(function(a, b) { 
        return a.lineNumber - b.lineNumber; 
    }); 
 
    for (var i = 0; i < synonyms_and_properties.length; i++) { 
        var synprop = synonyms_and_properties[i]; 
        if (synprop.length == 2) { 
            // synonym (a = b) 
            objectMask[synprop[0]] = objectMask[synprop[1]]; 
        } else { 
            // property (a = b or c) 
            var val = new BitVec(STRIDE_OBJ); 
            for (var j = 1; j < synprop.length; j++) { 
                var n = synprop[j]; 
                val.ior(objectMask[n]); 
            } 
            objectMask[synprop[0]] = val; 
        } 
    } 
 
    //use \n as a delimeter for internal-only objects 
    var all_obj = new BitVec(STRIDE_OBJ); 
    all_obj.inot(); 
    objectMask["\nall\n"] = all_obj; 
 
    state.objectMasks = objectMask; 
} 
 
function checkObjectsAreLayered(state) { 
    for (var n in state.objects) { 
        if (state.objects.hasOwnProperty(n)) { 
            var found = false; 
            for (var i = 0; i < state.collisionLayers.length; i++) { 
                var layer = state.collisionLayers[i]; 
                for (var j = 0; j < layer.length; j++) { 
                    if (layer[j] === n) { 
                        found = true; 
                        break; 
                    } 
                } 
                if (found) { 
                    break; 
                } 
            } 
            if (found === false) { 
                var o = state.objects[n]; 
                logError('Object "' + n.toUpperCase() + '" has been defined, but not assigned to a layer.', o.lineNumber); 
            } 
        } 
    } 
} 
 
function twiddleMetaData(state, update = false) { 
 var newmetadata; 
 
 if (!update) { 
  newmetadata = {}; 
  for (var i=0;i<state.metadata.length;i+=2) { 
   var key = state.metadata[i]; 
   var val = state.metadata[i+1]; 
   newmetadata[key]=val; 
  } 
 } else { 
  newmetadata = state.metadata; 
 } 
 
 if (newmetadata.flickscreen!==undefined) { 
  var val = newmetadata.flickscreen; 
  var coords = val.split('x'); 
  var intcoords = [parseInt(coords[0]),parseInt(coords[1])]; 
  newmetadata.flickscreen=intcoords; 
 } 
 if (newmetadata.zoomscreen!==undefined) { 
  var val = newmetadata.zoomscreen; 
  var coords = val.split('x'); 
  var intcoords = [parseInt(coords[0]),parseInt(coords[1])]; 
  newmetadata.zoomscreen=intcoords; 
 } 
 if (newmetadata.smoothscreen!==undefined) { 
  var val = newmetadata.smoothscreen; 
  var args = val.split(/\s+/); 
 
  var validArguments = true 
 
  if (args.length < 1) { 
   logErrorNoLine('smoothscreen given no arguments but expects at least 1: smoothscreen [flick] WxH [IxJ] [S]') 
   validArguments = false 
  } else if (args.length > 4) { 
   logErrorNoLine('smoothscreen given ' + args.length + ' arguments but expects at most 4: smoothscreen [flick] WxH [IxJ] [S]') 
   validArguments = false 
  } 
 
  const smoothscreen = { 
   screenSize: { width: 0, height: 0 }, 
   boundarySize: { width: 1, height: 1 }, 
   cameraSpeed: 0.125, 
   flick: false, 
   debug: !!newmetadata.smoothscreen_debug 
  } 
 
  if (args[0] === 'flick') { 
   smoothscreen.flick = true 
   args.shift() 
  } 
 
  const screenSizeMatch = args[0].match(/^(?<width>\d+)x(?<height>\d+)$/) 
  if (screenSizeMatch) { 
   smoothscreen.screenSize.width = parseInt(screenSizeMatch.groups.width) 
   smoothscreen.screenSize.height = parseInt(screenSizeMatch.groups.height) 
 
   if (smoothscreen.flick) { 
    smoothscreen.boundarySize.width = smoothscreen.screenSize.width 
    smoothscreen.boundarySize.height = smoothscreen.screenSize.height 
   } 
  } else { 
   logErrorNoLine('smoothscreen given first argument ' + args[0] + ' but must be formatted WxH where W and H are integers') 
   validArguments = false 
  } 
 
  if (args.length > 1) { 
   const boundarySizeMatch = args[1].match(/^(?<width>\d+)x(?<height>\d+)$/) 
   if (boundarySizeMatch) { 
    smoothscreen.boundarySize.width = parseInt(boundarySizeMatch.groups.width) 
    smoothscreen.boundarySize.height = parseInt(boundarySizeMatch.groups.height) 
   } else { 
    logErrorNoLine('smoothscreen given second argument ' + args[1] + ' but must be formatted IxJ where I and J are integers') 
    validArguments = false 
   } 
  } 
 
  if (args.length > 2) { 
   const cameraSpeedMatch = args[2].match(/^(?<speed>\d+(\.\d+)?)$/) 
   if (cameraSpeedMatch) { 
    smoothscreen.cameraSpeed = parseFloat(cameraSpeedMatch.groups.speed) 
   } else { 
    logErrorNoLine('smoothscreen given third argument ' + args[2] + ' but must be a number') 
    validArguments = false 
   } 
  } 
 
  if (validArguments) { 
   newmetadata.smoothscreen = smoothscreen; 
  } else { 
   delete newmetadata.smoothscreen 
  } 
 } 
 
 state.metadata=newmetadata; 
 
 if (!update) { 
  state.default_metadata = deepClone(newmetadata); 
 } 
} 
 
function processWinConditions(state) { 
    // [-1/0/1 (no,some,all),ob1,ob2] (ob2 is background by default) 
    var newconditions = []; 
    for (var i = 0; i < state.winconditions.length; i++) { 
        var wincondition = state.winconditions[i]; 
        if (wincondition.length == 0) { 
            return; 
        } 
        var num = 0; 
        switch (wincondition[0]) { 
            case 'no': 
                { num = -1; break; } 
            case 'all': 
                { num = 1; break; } 
        } 
 
        var lineNumber = wincondition[wincondition.length - 1]; 
 
        var n1 = wincondition[1]; 
        var n2; 
        if (wincondition.length == 5) { 
            n2 = wincondition[3]; 
        } else { 
            n2 = '\nall\n'; 
        } 
 
        var mask1 = 0; 
        var mask2 = 0; 
        if (n1 in state.objectMasks) { 
            mask1 = state.objectMasks[n1]; 
        } else { 
            logError('Unwelcome term "' + n1 + '" found in win condition. Win conditions objects have to be objects or properties (defined using "or", in terms of other properties)', lineNumber); 
        } 
        if (n2 in state.objectMasks) { 
            mask2 = state.objectMasks[n2]; 
        } else { 
            logError('Unwelcome term "' + n2 + '" found in win condition. Win conditions objects have to be objects or properties (defined using "or", in terms of other properties)', lineNumber); 
        } 
        var newcondition = [num, mask1, mask2, lineNumber]; 
        newconditions.push(newcondition); 
    } 
    state.winconditions = newconditions; 
} 
 
function printCellRow(cellRow) { 
    var result = "[ "; 
    for (var i = 0; i < cellRow.length; i++) { 
        if (i > 0) { 
            result += "| "; 
        } 
        var cell = cellRow[i]; 
        for (var j = 0; j < cell.length; j += 2) { 
            var direction = cell[j]; 
            var object = cell[j + 1] 
            if (direction === "...") { 
                result += direction + " "; 
            } else { 
                result += direction + " " + object + " "; 
            } 
        } 
    } 
    result += "] "; 
    return result; 
} 
 
function cacheRuleStringRep(rule) { 
 var result="(<a onclick=\"jumpToLine('"+ rule.lineNumber.toString() + "');\"  href=\"javascript:void(0);\">"+rule.lineNumber+"</a>) "+ rule.direction.toString().toUpperCase()+" "; 
 if (rule.rigid) { 
  result = "RIGID "+result+" "; 
 } 
 if (rule.randomRule) { 
  result = "RANDOM "+result+" "; 
 } 
 if (rule.globalRule) { 
  result = "GLOBAL "+result+" "; 
 } 
 if (rule.late) { 
  result = "LATE "+result+" "; 
 } 
 for (var i=0;i<rule.lhs.length;i++) { 
  var cellRow = rule.lhs[i]; 
  result = result + printCellRow(cellRow); 
 } 
 result = result + "-> "; 
 for (var i=0;i<rule.rhs.length;i++) { 
  var cellRow = rule.rhs[i]; 
  result = result + printCellRow(cellRow); 
 } 
 for (var i=0;i<rule.commands.length;i++) { 
  var command = rule.commands[i]; 
  if (command.length===1) { 
   result = result +command[0].toString(); 
  } else { 
   result = result + '('+command[0].toString()+", "+command[1].toString()+') ';    
  } 
 } 
 //print commands next 
 rule.stringRep=result; 
} 
 
function cacheAllRuleNames(state) { 
 
    for (var i = 0; i < state.rules.length; i++) { 
        var rule = state.rules[i]; 
        cacheRuleStringRep(rule); 
    } 
} 
 
function printRules(state) { 
    var output = ""; 
    var loopIndex = 0; 
    var loopEnd = -1; 
    var discardcount = 0; 
    for (var i = 0; i < state.rules.length; i++) { 
        var rule = state.rules[i]; 
        if (loopIndex < state.loops.length) { 
            if (state.loops[loopIndex][0] < rule.lineNumber) { 
                output += "STARTLOOP<br>"; 
                loopIndex++; 
                if (loopIndex < state.loops.length) { // don't die with mismatched loops 
                    loopEnd = state.loops[loopIndex][0]; 
                    loopIndex++; 
                } 
            } 
        } 
        if (loopEnd !== -1 && loopEnd < rule.lineNumber) { 
            output += "ENDLOOP<br>"; 
            loopEnd = -1; 
        } 
        if (rule.hasOwnProperty('discard')) { 
            discardcount++; 
        } else { 
            output += rule.stringRep + "<br>"; 
        } 
    } 
    if (loopEnd !== -1) { // no more rules after loop end 
        output += "ENDLOOP<br>"; 
    } 
    output += "===========<br>"; 
    output = "<br>Rule Assembly : (" + (state.rules.length - discardcount) + " rules)<br>===========<br>" + output; 
    consolePrint(output); 
} 
 
function removeDuplicateRules(state) { 
    var record = {}; 
    var newrules = []; 
    var lastgroupnumber = -1; 
    for (var i = state.rules.length - 1; i >= 0; i--) { 
        var r = state.rules[i]; 
        var groupnumber = r.groupNumber; 
        if (groupnumber !== lastgroupnumber) { 
            record = {}; 
        } 
        var r_string = r.stringRep; 
        if (record.hasOwnProperty(r_string)) { 
            state.rules.splice(i, 1); 
        } else { 
            record[r_string] = true; 
        } 
        lastgroupnumber = groupnumber; 
    } 
} 
 
function generateLoopPoints(state) { 
    var loopPoint = {}; 
    var loopPointIndex = 0; 
    var outside = true; 
    var source = 0; 
    var target = 0; 
    if (state.loops.length % 2 === 1) { 
        logErrorNoLine("have to have matching number of  'startLoop' and 'endLoop' loop points."); 
    } 
 
    for (var j = 0; j < state.loops.length; j++) { 
        var loop = state.loops[j]; 
        for (var i = 0; i < state.rules.length; i++) { 
            var ruleGroup = state.rules[i]; 
 
            var firstRule = ruleGroup[0]; 
            var lastRule = ruleGroup[ruleGroup.length - 1]; 
 
            var firstRuleLine = firstRule.lineNumber; 
            var lastRuleLine = lastRule.lineNumber; 
 
            if (outside) { 
                if (firstRuleLine >= loop[0]) { 
                    target = i; 
                    outside = false; 
                    if (loop[1] === -1) { 
                        logErrorNoLine("Need have to have matching number of  'startLoop' and 'endLoop' loop points."); 
                    } 
                    break; 
                } 
            } else { 
                if (firstRuleLine >= loop[0]) { 
                    source = i - 1; 
                    loopPoint[source] = target; 
                    outside = true; 
                    if (loop[1] === 1) { 
                        logErrorNoLine("Need have to have matching number of  'startLoop' and 'endLoop' loop points."); 
                    } 
                    break; 
                } 
            } 
        } 
    } 
    if (outside === false) { 
        var source = state.rules.length; 
        loopPoint[source] = target; 
    } else {} 
    state.loopPoint = loopPoint; 
 
    loopPoint = {}; 
    outside = true; 
    for (var j = 0; j < state.loops.length; j++) { 
        var loop = state.loops[j]; 
        for (var i = 0; i < state.lateRules.length; i++) { 
            var ruleGroup = state.lateRules[i]; 
 
            var firstRule = ruleGroup[0]; 
            var lastRule = ruleGroup[ruleGroup.length - 1]; 
 
            var firstRuleLine = firstRule.lineNumber; 
            var lastRuleLine = lastRule.lineNumber; 
 
            if (outside) { 
                if (firstRuleLine >= loop[0]) { 
                    target = i; 
                    outside = false; 
                    if (loop[1] === -1) { 
                        logErrorNoLine("Need have to have matching number of  'startLoop' and 'endLoop' loop points."); 
                    } 
                    break; 
                } 
            } else { 
                if (firstRuleLine >= loop[0]) { 
                    source = i - 1; 
                    loopPoint[source] = target; 
                    outside = true; 
                    if (loop[1] === 1) { 
                        logErrorNoLine("Need have to have matching number of  'startLoop' and 'endLoop' loop points."); 
                    } 
                    break; 
                } 
            } 
        } 
    } 
    if (outside === false) { 
        var source = state.lateRules.length; 
        loopPoint[source] = target; 
    } else {} 
    state.lateLoopPoint = loopPoint; 
} 
 
var soundEvents = ["titlescreen", "startgame", "cancel", "endgame", "startlevel", "undo", "restart", "endlevel", "showmessage", "closemessage", "sfx0", "sfx1", "sfx2", "sfx3", "sfx4", "sfx5", "sfx6", "sfx7", "sfx8", "sfx9", "sfx10"]; 
var soundMaskedEvents = ["create", "destroy", "move", "cantmove", "action"]; 
var soundVerbs = soundEvents.concat(soundMaskedEvents); 
 
 
function validSeed(seed) { 
    return /^\s*\d+\s*$/.exec(seed) !== null; 
} 
 
 
var soundDirectionIndicatorMasks = { 
    'up': parseInt('00001', 2), 
    'down': parseInt('00010', 2), 
    'left': parseInt('00100', 2), 
    'right': parseInt('01000', 2), 
    'horizontal': parseInt('01100', 2), 
    'vertical': parseInt('00011', 2), 
    'orthogonal': parseInt('01111', 2), 
    '___action____': parseInt('10000', 2) 
}; 
 
var soundDirectionIndicators = ["up", "down", "left", "right", "horizontal", "vertical", "orthogonal", "___action____"]; 
 
 
function generateSoundData(state) { 
    var sfx_Events = {}; 
    var sfx_CreationMasks = []; 
    var sfx_DestructionMasks = []; 
    var sfx_MovementMasks = []; 
    var sfx_MovementFailureMasks = []; 
 
    for (var i = 0; i < state.sounds.length; i++) { 
        var sound = state.sounds[i]; 
        if (sound.length <= 1) { 
            continue; 
        } 
        var lineNumber = sound[sound.length - 1]; 
 
        if (sound.length === 2) { 
            logError('incorrect sound declaration.', lineNumber); 
            continue; 
        } 
 
        if (soundEvents.indexOf(sound[0]) >= 0) { 
            if (sound.length > 4) { 
                logError("too much stuff to define a sound event.", lineNumber); 
            } 
            var seed = sound[1]; 
            if (validSeed(seed)) { 
                if (sfx_Events[sound[0]] !== undefined) { 
                    logWarning(sound[0].toUpperCase() + " already declared.", lineNumber); 
                } 
                sfx_Events[sound[0]] = sound[1]; 
            } else { 
                logError("Expecting sfx data, instead found \"" + sound[1] + "\".", lineNumber); 
            } 
        } else { 
            var target = sound[0].trim(); 
            var verb = sound[1].trim(); 
            var directions = sound.slice(2, sound.length - 2); 
            if (directions.length > 0 && (verb !== 'move' && verb !== 'cantmove')) { 
                logError('incorrect sound declaration.', lineNumber); 
            } 
 
            if (verb === 'action') { 
                verb = 'move'; 
                directions = ['___action____']; 
            } 
 
            if (directions.length == 0) { 
                directions = ["orthogonal"]; 
            } 
            var seed = sound[sound.length - 2]; 
 
            if (target in state.aggregatesDict) { 
                logError('cannot assign sound events to aggregate objects (declared with "and"), only to regular objects, or properties, things defined in terms of "or" ("' + target + '").', lineNumber); 
            } else if (target in state.objectMasks) { 
 
            } else { 
                logError('Object "' + target + '" not found.', lineNumber); 
            } 
 
            var objectMask = state.objectMasks[target]; 
 
            var directionMask = 0; 
            for (var j = 0; j < directions.length; j++) { 
                directions[j] = directions[j].trim(); 
                var direction = directions[j]; 
                if (soundDirectionIndicators.indexOf(direction) === -1) { 
                    logError('Was expecting a direction, instead found "' + direction + '".', lineNumber); 
                } else { 
                    var soundDirectionMask = soundDirectionIndicatorMasks[direction]; 
                    directionMask |= soundDirectionMask; 
                } 
            } 
 
 
            var targets = [target]; 
            var modified = true; 
            while (modified) { 
                modified = false; 
                for (var k = 0; k < targets.length; k++) { 
                    var t = targets[k]; 
                    if (t in state.synonymsDict) { 
                        targets[k] = state.synonymsDict[t]; 
                        modified = true; 
                    } else if (t in state.propertiesDict) { 
                        modified = true; 
                        var props = state.propertiesDict[t]; 
                        targets.splice(k, 1); 
                        k--; 
                        for (var l = 0; l < props.length; l++) { 
                            targets.push(props[l]); 
                        } 
                    } 
                } 
            } 
 
            if (verb === 'move' || verb === 'cantmove') { 
                for (var j = 0; j < targets.length; j++) { 
                    var targetName = targets[j]; 
                    var targetDat = state.objects[targetName]; 
                    var targetLayer = targetDat.layer; 
                    var shiftedDirectionMask = new BitVec(STRIDE_MOV); 
                    shiftedDirectionMask.ishiftor(directionMask, 5 * targetLayer); 
 
                    var o = { 
                        objectMask: objectMask, 
                        directionMask: shiftedDirectionMask, 
                        seed: seed 
                    }; 
 
                    if (verb === 'move') { 
                        sfx_MovementMasks.push(o); 
                    } else { 
                        sfx_MovementFailureMasks.push(o); 
                    } 
                } 
            } 
 
 
            if (!validSeed(seed)) { 
                logError("Expecting sfx data, instead found \"" + seed + "\".", lineNumber); 
            } 
 
            var targetArray; 
            switch (verb) { 
                case "create": 
                    { 
                        var o = { 
                            objectMask: objectMask, 
                            seed: seed 
                        } 
                        sfx_CreationMasks.push(o); 
                        break; 
                    } 
                case "destroy": 
                    { 
                        var o = { 
                            objectMask: objectMask, 
                            seed: seed 
                        } 
                        sfx_DestructionMasks.push(o); 
                        break; 
                    } 
            } 
        } 
    } 
 
    state.sfx_Events = sfx_Events; 
    state.sfx_CreationMasks = sfx_CreationMasks; 
    state.sfx_DestructionMasks = sfx_DestructionMasks; 
    state.sfx_MovementMasks = sfx_MovementMasks; 
    state.sfx_MovementFailureMasks = sfx_MovementFailureMasks; 
} 
 
 
function formatHomePage(state) { 
    if ('background_color' in state.metadata) { 
        state.bgcolor = colorToHex(colorPalette, state.metadata.background_color); 
    } else { 
        state.bgcolor = "#000000"; 
    } 
    if ('text_color' in state.metadata) { 
        state.fgcolor = colorToHex(colorPalette, state.metadata.text_color); 
    } else { 
        state.fgcolor = "#FFFFFF"; 
    } 
 
    if (isColor(state.fgcolor) === false) { 
        logError("text_color in incorrect format - found " + state.fgcolor + ", but I expect a color name (like 'pink') or hex-formatted color (like '#1412FA').  Defaulting to white.") 
        state.fgcolor = "#FFFFFF"; 
    } 
    if (isColor(state.bgcolor) === false) { 
        logError("background_color in incorrect format - found " + state.bgcolor + ", but I expect a color name (like 'pink') or hex-formatted color (like '#1412FA').  Defaulting to black.") 
        state.bgcolor = "#000000"; 
    } 
 
    if (canSetHTMLColors) { 
 
        if ('background_color' in state.metadata) { 
            document.body.style.backgroundColor = state.bgcolor; 
        } 
 
        if ('text_color' in state.metadata) { 
            var separator = document.getElementById("separator"); 
            if (separator != null) { 
                separator.style.color = state.fgcolor; 
            } 
 
            var h1Elements = document.getElementsByTagName("a"); 
            for (var i = 0; i < h1Elements.length; i++) { 
                h1Elements[i].style.color = state.fgcolor; 
            } 
 
            var h1Elements = document.getElementsByTagName("h1"); 
            for (var i = 0; i < h1Elements.length; i++) { 
                h1Elements[i].style.color = state.fgcolor; 
            } 
        } 
    } 
 
    if ('homepage' in state.metadata) { 
        var url = state.metadata['homepage']; 
        url = url.replace("http://", ""); 
        url = url.replace("https://", ""); 
        state.metadata['homepage'] = url; 
    } 
} 
 
var MAX_ERRORS = 5; 
 
function loadFile(str) { 
 var processor = new codeMirrorFn(); 
 var state = processor.startState(); 
 
 var lines = str.split('\n'); 
 for (var i = 0; i < lines.length; i++) { 
  var line = lines[i]; 
  state.lineNumber = i + 1; 
  var ss = new CodeMirror.StringStream(line, 4); 
  do { 
   processor.token(ss, state); 
 
   if (errorCount>MAX_ERRORS) { 
    consolePrint("too many errors, aborting compilation"); 
    return; 
   } 
  }   
  while (ss.eol() === false); 
 } 
 
    // delete state.lineNumber; 
 
 generateExtraMembers(state); 
 generateMasks(state); 
 levelsToArray(state); 
 extractSections(state); 
    rulesToArray(state); 
     
    if (state.invalid>0){ 
        return null; 
    } 
 
 cacheAllRuleNames(state); 
 
    removeDuplicateRules(state); 
     
    if (state.invalid>0){ 
        return null; 
    } 
 
 convertSectionNamesToIndices(state); 
 
 rulesToMask(state); 
 
  
 if (debugMode) { 
  printRules(state); 
 } 
 
 arrangeRulesByGroupNumber(state); 
 collapseRules(state.rules); 
 collapseRules(state.lateRules); 
 
    generateRigidGroupList(state); 
 
 processWinConditions(state); 
 checkObjectsAreLayered(state); 
 
 twiddleMetaData(state); 
  
 generateExtraMembersPart2(state); 
 
 generateLoopPoints(state); 
 
 generateSoundData(state); 
 
 formatHomePage(state); 
 
 delete state.commentLevel; 
 delete state.names; 
 delete state.abbrevNames; 
 delete state.objects_candname; 
 delete state.objects_section; 
 delete state.objects_spritematrix; 
 delete state.section; 
 delete state.subsection; 
 delete state.tokenIndex; 
 delete state.visitedSections; 
 delete state.loops; 
 /* 
 var lines = stripComments(str); 
 window.console.log(lines); 
 var sections = generateSections(lines); 
 window.console.log(sections); 
 var sss = generateSemiStructuredSections(sections);*/ 
 return state; 
} 
 
var ifrm; 
 
function compile(command, text, randomseed) { 
    matchCache = {}; 
    forceRegenImages = true; 
    if (command === undefined) { 
        command = ["restart"]; 
    } 
    if (randomseed === undefined) { 
        randomseed = null; 
    } 
    lastDownTarget = canvas; 
 
    if (text === undefined) { 
        var code = window.form1.code; 
 
        var editor = code.editorreference; 
 
        text = editor.getValue() + "\n"; 
    } 
    if (canDump === true) { 
        compiledText = text; 
    } 
 
    errorCount = 0; 
    compiling = true; 
    errorStrings = []; 
    consolePrint('================================='); 
    try { 
        var state = loadFile(text); 
        //  consolePrint(JSON.stringify(state)); 
    } finally { 
        compiling = false; 
    } 
 
    if (state && state.levels && state.levels.length === 0) { 
        logError('No levels found.  Add some levels!', undefined, true); 
    } 
 
    if (errorCount > MAX_ERRORS) { 
        return; 
    } 
    /*catch(err) 
    { 
     if (anyErrors===false) { 
      logErrorNoLine(err.toString()); 
     } 
    }*/ 
 
    if (errorCount > 0) { 
        if (IDE===false){ 
            consoleError('<span class="systemMessage">Errors detected during compilation; the game may not work correctly.  If this is an older game, and you think it just broke because of recent changes in the puzzlescript engine, please consider dropping an email to analytic@gmail.com with a link to the game and I\'ll try make sure it\'s back working ASAP.</span>'); 
        } else{ 
            consoleError('<span class="systemMessage">Errors detected during compilation; the game may not work correctly.</span>'); 
        } 
    } else { 
        var ruleCount = 0; 
        for (var i = 0; i < state.rules.length; i++) { 
            ruleCount += state.rules[i].length; 
        } 
        for (var i = 0; i < state.lateRules.length; i++) { 
            ruleCount += state.lateRules[i].length; 
        } 
        if (command[0] == "restart") { 
            consolePrint('<span class="systemMessage">Successful Compilation, generated ' + ruleCount + ' instructions.</span>'); 
        } else { 
            consolePrint('<span class="systemMessage">Successful live recompilation, generated ' + ruleCount + ' instructions.</span>'); 
 
        } 
 
 
         
        if (IDE){ 
            if (state.metadata.title!==undefined) { 
                document.title="PuzzleScript - " + state.metadata.title; 
            } 
        } 
    } 
 
    if (state!==null){//otherwise error 
        setGameState(state, command, randomseed); 
    } 
 
    clearInputHistory(); 
 
    consoleCacheDump(); 
 
} 
 
 
 
function qualifyURL(url) { 
    var a = document.createElement('a'); 
    a.href = url; 
    return a.href; 
}</script> <script>function selectText(e,t){t=t||window.event;var l=document.getElementById(e);if(t&&(t.ctrlKey||t.metaKey)){if(solving)return;var o=["console"].concat(l.innerHTML.split("<br>")),n=levelFromString(state,o);loadLevelFromLevelDat(state,n,null),canvasResize()}else if(document.selection){var i=document.body.createTextRange();i.moveToElementText(l),i.select()}else if(window.getSelection){var i=document.createRange();i.selectNode(l);var r=window.getSelection();r.removeAllRanges(),r.addRange(i)}}function recalcLevelBounds(){}function arrCopy(e,t,l,o,n){for(;n--;)l[o++]=e[t]++}function adjustLevel(e,t,l){backups.push(backupLevel());var o=e.clone();e.width+=t,e.height+=l,e.n_tiles=e.width*e.height,e.objects=new Int32Array(e.n_tiles*STRIDE_OBJ);var n=new BitVec(STRIDE_OBJ);n.ibitset(state.backgroundid);for(var i=0;i<e.n_tiles;++i)e.setCell(i,n);return e.movements=new Int32Array(e.objects.length),columnAdded=!0,RebuildLevelArrays(),o}function addLeftColumn(){for(var e=adjustLevel(level,1,0),t=1;t<level.width;++t)for(var l=0;l<level.height;++l){var o=t*level.height+l;level.setCell(o,e.getCell(o-level.height))}}function addRightColumn(){for(var e=adjustLevel(level,1,0),t=0;t<level.width-1;++t)for(var l=0;l<level.height;++l){var o=t*level.height+l;level.setCell(o,e.getCell(o))}}function addTopRow(){for(var e=adjustLevel(level,0,1),t=0;t<level.width;++t)for(var l=1;l<level.height;++l){var o=t*level.height+l;level.setCell(o,e.getCell(o-t-1))}}function addBottomRow(){for(var e=adjustLevel(level,0,1),t=0;t<level.width;++t)for(var l=0;l<level.height-1;++l){var o=t*level.height+l;level.setCell(o,e.getCell(o-t))}}function removeLeftColumn(){if(!(level.width<=1))for(var e=adjustLevel(level,-1,0),t=0;t<level.width;++t)for(var l=0;l<level.height;++l){var o=t*level.height+l;level.setCell(o,e.getCell(o+level.height))}}function removeRightColumn(){if(!(level.width<=1))for(var e=adjustLevel(level,-1,0),t=0;t<level.width;++t)for(var l=0;l<level.height;++l){var o=t*level.height+l;level.setCell(o,e.getCell(o))}}function removeTopRow(){if(!(level.height<=1))for(var e=adjustLevel(level,0,-1),t=0;t<level.width;++t)for(var l=0;l<level.height;++l){var o=t*level.height+l;level.setCell(o,e.getCell(o+t+1))}}function removeBottomRow(){if(!(level.height<=1))for(var e=adjustLevel(level,0,-1),t=0;t<level.width;++t)for(var l=0;l<level.height;++l){var o=t*level.height+l;level.setCell(o,e.getCell(o+t))}}function matchGlyph(e,t){for(var l,o=-1,n=0;n<t.length;++n){var i=t[n][0],r=t[n][1],a=t[n][2];if(r.bitsSetInArray(e.data)){for(var s=0,c=0;c<32*STRIDE_OBJ;++c)a.get(c)&&e.get(c)&&s++,r.get(c)&&e.get(c)&&s++;s>o&&(o=s,l=i)}}return o>0?l:(logErrorNoLine("Wasn't able to approximate a glyph value for some tiles, using '.' as a placeholder.",!0),".")}function printLevel(){var e=[];for(var t in state.glyphDict)if(state.glyphDict.hasOwnProperty(t)&&1===t.length){for(var l=state.glyphDict[t],o=new BitVec(STRIDE_OBJ),n=0;n<l.length;n++){var i=l[n];i>=0&&o.ibitset(i)}var r=o.clone(),a=state.layerMasks[state.backgroundlayer];o.iclear(a),e.push([t,o,r])}selectableint++;var s="selectable"+selectableint,c='Printing level contents:<br><br><span id="'+s+'" onclick="selectText(\''+s+"',event)\"><br>";cache_console_messages=!1;for(var d=0;d<level.height;d++){for(var n=0;n<level.width;n++){var u=d+n*level.height,v=level.getCell(u),l=matchGlyph(v,e);l in htmlEntityMap&&(l=htmlEntityMap[l]),c+=l}d<level.height-1&&(c+="<br>")}c+="</span><br><br>",consolePrint(c,!0)}function levelEditorClick(e,t){if(mouseCoordY<=-2){var l=editorRowCount-(-mouseCoordY-2)-1,o=mouseCoordX+(screenwidth-1)*l;-1===mouseCoordX?printLevel():mouseCoordX>=0&&o<glyphImages.length&&(glyphSelectedIndex=o,redraw())}else if(mouseCoordX>-1&&mouseCoordY>-1&&mouseCoordX<screenwidth-2&&mouseCoordY<screenheight-2-editorRowCount){for(var n=glyphImagesCorrespondance[glyphSelectedIndex],i=state.glyphDict[n],r=new BitVec(STRIDE_OBJ),a=0;a<i.length;a++){var s=i[a];s>=0&&r.ibitset(s)}var c=state.layerMasks[state.backgroundlayer];r.bitsClearInArray(c.data)&&r.ibitset(state.backgroundid);var d=mouseCoordY+mouseCoordX*level.height,u=level.getCell(d);if(u.equals(r))return;!1===anyEditsSinceMouseDown&&(anyEditsSinceMouseDown=!0,backups.push(backupLevel())),level.setCell(d,r),redraw()}else t&&(-1===mouseCoordX?(addLeftColumn(),canvasResize()):mouseCoordX===screenwidth-2&&(addRightColumn(),canvasResize()),-1===mouseCoordY?(addTopRow(),canvasResize()):mouseCoordY===screenheight-2-editorRowCount&&(addBottomRow(),canvasResize()))}function levelEditorRightClick(e,t){if(-2===mouseCoordY)mouseCoordX<=glyphImages.length&&(glyphSelectedIndex=mouseCoordX,redraw());else if(mouseCoordX>-1&&mouseCoordY>-1&&mouseCoordX<screenwidth-2&&mouseCoordY<screenheight-2-editorRowCount){var l=mouseCoordY+mouseCoordX*level.height,o=new BitVec(STRIDE_OBJ);o.ibitset(state.backgroundid),level.setCell(l,o),redraw()}else t&&(-1===mouseCoordX?(removeLeftColumn(),canvasResize()):mouseCoordX===screenwidth-2&&(removeRightColumn(),canvasResize()),-1===mouseCoordY?(removeTopRow(),canvasResize()):mouseCoordY===screenheight-2-editorRowCount&&(removeBottomRow(),canvasResize()))}function getTilesTraversingPoints(e,t,l,o){if(cellwidth!==cellheight)throw"Error: Cell is not square.";for(var n=l-e,i=o-t,r=t*n,a=e*i-r,s=cellwidth*(Math.abs(n)+Math.abs(i)),c=2*a-s,d=2*a+s,u=Math.floor(e/cellwidth),v=Math.floor(t/cellheight),g=Math.floor(l/cellwidth),m=Math.floor(o/cellheight),h=cellwidth-1,f=g-u>=0?1:-1,p=m-v>=0?1:-1,y=v,C=[],S=[],w=u;w!=g+f;w+=f)for(var k=!1,b=y;b!=m+p;b+=p){if(b>=level.height||b<0||w>=level.width||w<0)throw error="Mouse input loop failed; it:"+w+" "+b+" cell1:"+u+" "+v+" cell2:"+g+" "+m,console.log(error),error;if(function(e,t){return fOfPointTimesTwo=i*e-n*t,c<fOfPointTimesTwo==fOfPointTimesTwo<=d}(2*w*cellwidth+h,2*b*cellwidth+h))C.push(w),S.push(b),k=!0,y=b;else if(k)break}for(var M=("mouse_obstacle"in state.metadata),x=[u],T=[v];u!==g||v!==m;){if(v>=level.height||v<0||u>=level.width||u<0)throw error="Mouse input loop failed; cell1:"+u+" "+v+" cell2:"+g+" "+m,console.log(error),error;if(hitObstacle=function(){var e=screenOffsetY+v+(screenOffsetX+u)*level.height,t=level.getCell(e);return!!state.obstacleMask.anyBitsInCommon(t)},cellCornerXTimesTwo=2*u*cellwidth+h+f*cellwidth,cellCornerYTimesTwo=2*v*cellwidth+h+p*cellwidth,fOfPointTimesTwo=i*cellCornerXTimesTwo-n*cellCornerYTimesTwo,fOfPointTimesTwo>2*a==p>0!=f>0?(u+=f,M&&hitObstacle()&&(u-=f,v+=p)):(v+=p,M&&hitObstacle()&&(v-=p,u+=f)),M&&hitObstacle())break;x.push(u),T.push(v)}if(M)C=x,S=T;else for(var w=0;w<C.length;w++)if(C[w]!==x[w]||S[w]!==T[w]){try{displayError("line tile placement algorithm discrepancies detected")}catch(e){}throw consolePrint("line tile placement algorithm discrepancies detected",!0),"line tile placement algorithm discrepancies detected"}return{tileListX:C,tileListY:S}}function mouseAction(e,t,l){if(textMode){if(!t)return;if(!titleScreen||quittingTitleScreen||titleSelected)!1===messageselected&&state.levels[curlevel].message&&(messageselected=!0,timer=0,quittingMessageScreen=!0,tryPlayCloseMessageSound(),titleScreen=!1,drawMessageScreen());else if(0===titleMode)titleButtonSelected();else if(1===titleMode)5===mouseCoordY&&titleSelectOptions>=1?(titleSelection=0,titleButtonSelected()):6===mouseCoordY&&titleSelectOptions>=3?(titleSelection=1,titleButtonSelected()):7===mouseCoordY&&titleSelectOptions>=2?(2===titleSelectOptions?titleSelection=1:titleSelection=2,titleButtonSelected()):8===mouseCoordY&&titleSelectOptions>=4&&(titleSelection=3,titleButtonSelected());else if(2===titleMode)if(0===mouseCoordY)titleSelection=0,goToTitleScreen(),tryPlayTitleSound(),canvasResize();else if(2===mouseCoordY)0!=levelSelectScrollPos&&levelSelectScroll(-3);else if(12===mouseCoordY)titleSelectOptions-amountOfLevelsOnScreen>levelSelectScrollPos&&levelSelectScroll(3);else{var o=-1;switch(mouseCoordY){case 3:o=0;break;case 4:o=1;break;case 5:o=2;break;case 6:o=3;break;case 7:o=4;break;case 8:o=5;break;case 9:o=6;break;case 10:o=7;break;case 11:o=8}-1!=o&&(o+=levelSelectScrollPos)<titleSelectOptions&&(titleSelection=o,titleSelected=!0,messageselected=!1,timer=0,quittingTitleScreen=!0,generateLevelSelectScreen(),redraw())}}else if(x1=x2,y1=y2,x2=mousePixelX,y2=mousePixelY,x2=Math.max(0,Math.min(cellwidth*screenwidth-1,mousePixelX)),y2=Math.max(0,Math.min(cellheight*screenheight-1,mousePixelY)),t){if(mouseCoordX>=0&&mouseCoordY>=0&&mouseCoordX<screenwidth&&mouseCoordY<screenheight){var n=screenOffsetY+mouseCoordY+(screenOffsetX+mouseCoordX)*level.height;if(lastCoord=n,againing);else try{var i=backupLevel(),r=level.getCell(n);r.ibitset(l),level.setCell(n,r);var a=5;pushInput(a),processInput(a,!1,!1,i)&&redraw()}catch(e){console.log(e),consolePrint(e,!0)}}}else for(var s=getTilesTraversingPoints(x1,y1,x2,y2),c=s.tileListX,d=s.tileListY,u=0;u<c.length;u++){var n=screenOffsetY+d[u]+(screenOffsetX+c[u])*level.height;if(lastCoord!==n)if(lastCoord=n,againing);else try{var i=backupLevel(),r=level.getCell(n);r.ibitset(l),level.setCell(n,r);var a=5;pushInput(a),processInput(a,!1,!1,i)&&redraw()}catch(e){console.log(e),consolePrint(e,!0)}}}function onMouseDown(e){if(!e.handled){ULBS();var t=0===e.button;e.button;if("touchstart"==e.type&&(t=!0),t&&(e.ctrlKey||e.metaKey)&&(t=!1,!0),t){if(lastDownTarget=e.target,keybuffer=[],e.target===canvas||"tapFocusIndicator"===e.target.className){if(setMouseCoord(e),dragging=!0,rightdragging=!1,anyEditsSinceMouseDown=!1,levelEditorOpened)return levelEditorClick(e,!0);if("mouse_left"in state.metadata)return mouseAction(e,!0,state.lmbID)}dragging=!1,rightdragging=!1}else if(2===e.button||0===e.button&&(e.ctrlKey||e.metaKey))if("gameCanvas"===e.target.id){if(setMouseCoord(e),dragging=!1,rightdragging=!0,levelEditorOpened)return levelEditorRightClick(e,!0);if("mouse_right"in state.metadata)return mouseAction(e,!0,state.rmbID)}else dragging=!1,rightdragging=!1;else 1===e.button&&!1===textMode&&levelEditorOpened&&(pushInput("undo"),DoUndo(!1,!0),canvasResize());e.handled=!0}}function rightClickCanvas(e){return"mouse_right"in state.metadata?prevent(e):levelEditorOpened?prevent(e):void 0}function onMouseUp(e){if(!e.handled){if(dragging=!1,rightdragging=!1,0===e.button){if(e.target===canvas&&(setMouseCoord(e),"mouse_up"in state.metadata))return mouseAction(e,!0,state.lmbupID)}else if(2===e.button&&"gameCanvas"===e.target.id&&(setMouseCoord(e),"mouse_rup"in state.metadata))return mouseAction(e,!0,state.rmbupID);e.handled=!0}}function onKeyDown(e){ULBS(),e=e||window.event,!IDE&&[32,37,38,39,40].indexOf(e.keyCode)>-1&&(e&&(e.ctrlKey||e.metaKey)||prevent(e)),IDE||77!==e.keyCode||toggleMute(),keybuffer.indexOf(e.keyCode)>=0||((lastDownTarget===canvas||window.Mobile&&lastDownTarget===window.Mobile.focusIndicator)&&-1===keybuffer.indexOf(e.keyCode)&&(e&&(e.ctrlKey||e.metaKey)||(keybuffer.splice(keyRepeatIndex,0,e.keyCode),keyRepeatTimer=0,checkKey(e,!e.repeat))),!0===canDump&&(74===e.keyCode&&(e.ctrlKey||e.metaKey)?(dumpTestCase(),prevent(e)):75===e.keyCode&&(e.ctrlKey||e.metaKey)?(makeGIF(),prevent(e)):83===e.keyCode&&(e.ctrlKey||e.metaKey)?(saveClick(),prevent(e)):66===e.keyCode&&(e.ctrlKey||e.metaKey)?(rebuildClick(),prevent(e),e.target.blur(),canvas.focus()):82===e.keyCode&&(e.ctrlKey||e.metaKey)?(runClick(),prevent(e),e.target.blur(),canvas.focus()):120===e.keyCode?(prevent(e),solve()):119===e.keyCode?(prevent(e),stopSolving()):13===e.keyCode&&(e.ctrlKey||e.metaKey)&&(canvas.focus(),editor.display.input.blur(),e.shiftKey?runClick():rebuildClick(),prevent(e))))}function relMouseCoords(e){var t=0,l=0,o=0,n=0,i=this;do{t+=i.offsetLeft-i.scrollLeft,l+=i.offsetTop-i.scrollTop}while(i=i.offsetParent);return null==e.touches?(o=e.pageX-t,n=e.pageY-l):(o=e.touches[0].pageX-t,n=e.touches[0].pageY-l),{x:o,y:n}}function onKeyUp(e){e=e||window.event;var t=keybuffer.indexOf(e.keyCode);t>=0&&(keybuffer.splice(t,1),keyRepeatIndex>=t&&keyRepeatIndex--)}function onMyFocus(e){keybuffer=[],keyRepeatIndex=0,keyRepeatTimer=0}function onMyBlur(e){keybuffer=[],keyRepeatIndex=0,keyRepeatTimer=0}function setMouseCoord(e){var t=canvas.relMouseCoords(e);mousePixelX=t.x-xoffset,mousePixelY=t.y-yoffset,mouseCoordX=Math.floor(mousePixelX/cellwidth),mouseCoordY=Math.floor(mousePixelY/cellheight)}function mouseMove(e){e.handled||(levelEditorOpened?(setMouseCoord(e),dragging?levelEditorClick(e,!1):rightdragging&&levelEditorRightClick(e,!1),redraw()):dragging&&"mouse_drag"in state.metadata?(setMouseCoord(e),mouseAction(e,!1,state.dragID),redraw()):rightdragging&&"mouse_rdrag"in state.metadata&&(setMouseCoord(e),mouseAction(e,!1,state.rdragID),redraw()),e.handled=!0)}function onMouseIn(){mouseInCanvas=!0}function onMouseOut(){mouseInCanvas=!1}function onMouseWheel(e){mouseInCanvas&&!e.ctrlKey&&(normalizedDelta=Math.sign(e.deltaY),titleScreen&&2==titleMode&&(state.metadata.mouse_left||state.metadata.mouse_drag)&&(levelSelectScroll(normalizedDelta),redraw(),prevent(e)),levelEditorOpened&&(glyphSelectedIndex=clamp(glyphSelectedIndex+normalizedDelta,0,glyphCount()-1),redraw(),prevent(e)))}function levelSelectScroll(e){levelSelectScrollPos=clamp(levelSelectScrollPos+e,0,Math.max(state.sections.length-amountOfLevelsOnScreen,0)),titleSelection=clamp(titleSelection+e,0,state.sections.length-1),console.log(levelSelectScrollPos+" "+titleSelection),generateLevelSelectScreen(),redraw()}function clamp(e,t,l){return Math.min(Math.max(e,t),l)}function prevent(e){return e.preventDefault&&e.preventDefault(),e.stopImmediatePropagation&&e.stopImmediatePropagation(),e.stopPropagation&&e.stopPropagation(),e.returnValue=!1,!1}function titleButtonSelected(){!1===titleSelected&&(tryPlayStartGameSound(),titleSelected=!0,messageselected=!1,timer=0,quittingTitleScreen=!0,generateTitleScreen(),canvasResize())}function pollGamepads(){function e(e,t){return!(e.length<t)&&("object"==typeof e[t]?e[t].pressed:1==e[t])}function t(e,t,l){return!(e.length<t)&&e[t]*l>.5}function l(e){-1===keybuffer.indexOf(e)&&(keybuffer.splice(keyRepeatIndex,0,e),keyRepeatTimer=0,checkKey({keyCode:e},!0)),n.splice(0,0,e)}function o(){for(var e=0;e<gamepadKeys.length;e++)if(!(n.indexOf(gamepadKeys[e])>=0)){var t=keybuffer.indexOf(gamepadKeys[e]);t>=0&&(keybuffer.splice(t,1),keyRepeatIndex>=t&&keyRepeatIndex--)}gamepadKeys=n}var n=[],i=navigator.getGamepads?navigator.getGamepads():navigator.webkitGetGamepads?navigator.webkitGetGamepads():[];if(null==i||0==i.length)return void o();for(var r=0;r<i.length;r++){var a=i[r];null!=a&&a.connected&&((e(a.buttons,3)||e(a.buttons,4))&&l(82),(e(a.buttons,1)||t(a.axes,2,1))&&l(90),(e(a.buttons,2)||e(a.buttons,0)||e(a.buttons,5)||t(a.axes,1,1))&&l(88),e(a.buttons,7)&&l(27),e(a.buttons,6)&&l(69),(t(a.axes,0,1)||t(a.axes,6,1))&&l(39),(t(a.axes,0,-1)||t(a.axes,6,-1))&&l(37),(t(a.axes,1,-1)||t(a.axes,7,-1))&&l(38),(t(a.axes,1,1)||t(a.axes,7,1))&&l(40))}o()}function checkKey(e,t){if(ULBS(),!(winning||e&&(e.ctrlKey||e.metaKey||e.altKey))){var l=-1;switch(e.keyCode){case 65:case 37:l=1;break;case 38:case 87:l=0;break;case 68:case 39:l=3;break;case 83:case 40:l=2;break;case 80:printLevel();break;case 13:case 32:case 67:case 88:if(t&&ignoreNotJustPressedAction&&(ignoreNotJustPressedAction=!1),!1===t&&ignoreNotJustPressedAction)return;if(!1!==norepeat_action&&!t)return;l=4;break;case 85:case 90:if(!1===textMode)return pushInput("undo"),DoUndo(!1,!0),canvasResize(),prevent(e);break;case 82:if(!1===textMode&&t)return pushInput("restart"),DoRestart(),canvasResize(),prevent(e);break;case 27:if(solving){stopSolving();break}if(!1===titleScreen||titleMode>1)return(timer/1e3>.5||titleMode>1)&&(titleSelection=0,timer=0,!1===titleScreen&&void 0!==state.metadata.level_select?gotoLevelSelectScreen():goToTitleScreen(),tryPlayTitleSound(),canvasResize()),prevent(e);break;case 69:if(!solving&&canOpenEditor)return t&&(titleScreen&&("EMPTY GAME"===state.title?compile(["loadFirstNonMessageLevel"]):nextLevel()),levelEditorOpened=!levelEditorOpened,!1===levelEditorOpened&&printLevel(),restartTarget=backupLevel(),canvasResize()),prevent(e);break;case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:if(levelEditorOpened&&t){var o=9;return e.keyCode>=49&&(o=e.keyCode-49),o<glyphImages.length?glyphSelectedIndex=o:consolePrint("Trying to select tile outside of range in level editor.",!0),canvasResize(),prevent(e)}break;case 189:if(levelEditorOpened&&t&&glyphSelectedIndex>0)return glyphSelectedIndex--,canvasResize(),prevent(e);break;case 187:if(levelEditorOpened&&t&&glyphSelectedIndex+1<glyphImages.length)return glyphSelectedIndex++,canvasResize(),prevent(e)}if(throttle_movement&&l>=0&&l<=3){if(lastinput==l&&input_throttle_timer<repeatinterval)return;lastinput=l,input_throttle_timer=0}if(textMode){if(!throttle_movement){if(lastinput==l&&input_throttle_timer<repeatinterval)return;lastinput=l,input_throttle_timer=0}if(0===state.levels.length);else if(titleScreen)0===titleMode?4===l&&t&&titleButtonSelected():4==l&&t?!1===titleSelected&&(tryPlayStartGameSound(),titleSelected=!0,messageselected=!1,timer=0,quittingTitleScreen=!0,1==titleMode?generateTitleScreen():2==titleMode&&generateLevelSelectScreen(),redraw()):0!==l&&2!==l||(0===l?titleSelection--:titleSelection++,titleSelection>=titleSelectOptions?titleSelection-=titleSelectOptions:titleSelection<0&&(titleSelection+=titleSelectOptions),1==titleMode?generateTitleScreen():2==titleMode&&generateLevelSelectScreen(),redraw());else if(4==l&&t){if(unitTesting)return void nextLevel();!1===messageselected&&(messageselected=!0,timer=0,quittingMessageScreen=!0,tryPlayCloseMessageSound(),titleScreen=!1,drawMessageScreen())}}else if(!againing&&l>=0)return 4===l&&"noaction"in state.metadata||(pushInput(l),processInput(l)&&redraw()),prevent(e)}}function update(){var e=!1;if(timer+=deltatime,input_throttle_timer+=deltatime,quittingTitleScreen&&timer/1e3>.3&&(quittingTitleScreen=!1,titleMode<=1?nextLevel():2==titleMode&&gotoLevel(titleSelection)),againing&&timer>againinterval&&0==messagetext.length&&processInput(-1)&&(e=!0,keyRepeatTimer=0,autotick=0),quittingMessageScreen&&timer/1e3>.15&&(quittingMessageScreen=!1,""===messagetext?nextLevel():(messagetext="",textMode=!1,titleScreen=!1,titleMode=curlevel>0||null!==curlevelTarget?1:0,titleSelected=!1,ignoreNotJustPressedAction=!0,titleSelection=0,canvasResize(),checkWin())),winning&&timer/1e3>.5&&(winning=!1,nextLevel()),pollGamepads(),keybuffer.length>0){keyRepeatTimer+=deltatime;var t=throttle_movement?repeatinterval:repeatinterval/Math.sqrt(keybuffer.length);if(keyRepeatTimer>t){keyRepeatTimer=0,keyRepeatIndex=(keyRepeatIndex+1)%keybuffer.length;checkKey({keyCode:keybuffer[keyRepeatIndex]},!1)}}!(autotickinterval>0)||textMode||levelEditorOpened||againing||winning||(autotick+=deltatime,autotick>autotickinterval&&(autotick=0,pushInput("tick"),processInput(-1)&&(e=!0))),(e||"undefined"!=typeof state&&void 0!==state.metadata.smoothscreen)&&redraw()}var keyRepeatTimer=0,keyRepeatIndex=0,input_throttle_timer=0,lastinput=-100,dragging=!1,rightdragging=!1,columnAdded=!1,htmlEntityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;"},selectableint=0,lastCoord,x1=5,y1=5,x2=5,y2=5,anyEditsSinceMouseDown=!1;HTMLCanvasElement.prototype.relMouseCoords=relMouseCoords;var mouseCoordX=0,mouseCoordY=0,mousePixelX=0,mousePixelY=0;mouseInCanvas=!1,document.addEventListener("touchstart",onMouseDown,!1),document.addEventListener("touchmove",mouseMove,!1),document.addEventListener("touchend",onMouseUp,!1),document.addEventListener("mousedown",onMouseDown,!1),document.addEventListener("mouseup",onMouseUp,!1),document.addEventListener("mousemove",mouseMove,!1),document.addEventListener("contextmenu",rightClickCanvas,!1),document.addEventListener("keydown",onKeyDown,!1),document.addEventListener("keyup",onKeyUp,!1),document.addEventListener("wheel",onMouseWheel,{passive:!1}),window.addEventListener("focus",onMyFocus,!1),window.addEventListener("blur",onMyBlur,!1),canvas.addEventListener("mouseenter",onMouseIn,!1),canvas.addEventListener("mouseleave",onMouseOut,!1);var gamepadKeys=[],looping=!1,loop=function(){looping=!0;try{update()}catch(e){console.error(e)}if("hidden"===document.visibilityState)return void(looping=!1);setTimeout(loop,deltatime)};document.addEventListener("visibilitychange",function(){"visible"===document.visibilityState&&!1===looping&&loop()}),loop();</script> <script>/* 
 * Add gesture support for mobile devices. 
 */ 
 
window.Mobile = {}; 
 
//stolen from https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js 
Mobile.hasTouch = function() { 
    var bool; 
    if(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch)     { 
      bool = true; 
    } else { 
      /* 
      //don't know what's happening with this, so commented it out 
      var query = ['@media (',prefixes.join('touch-enabled),    ('),'heartz',')','{#modernizr{top:9px;position:absolute}}'].join(''); 
      testStyles(query, function( node ) { 
        bool = node.offsetTop === 9; 
      });*/ 
    } 
    return bool; 
} 
 
Mobile.enable = function (force) { 
    if (force || Mobile.hasTouch() && !Mobile._instance) { 
        Mobile._instance = new Mobile.GestureHandler(); 
        Mobile._instance.bindEvents(); 
        Mobile._instance.bootstrap(); 
    } 
    return Mobile._instance; 
}; 
 
window.Mobile.GestureHandler = function () { 
    this.initialize.apply(this, arguments); 
}; 
 
Mobile.log = function (message) { 
    var h1; 
    h1 = document.getElementsByTagName('h1')[0]; 
    h1.innerHTML = "" + Math.random().toString().substring(4, 1) + "-" + message; 
}; 
 
Mobile.debugDot = function (event) { 
    var dot, body, style 
 
    style = 'border-radius: 50px;' + 
        'width: 5px;' + 
        'height: 5px;' + 
        'background: red;' + 
        'position: absolute;' + 
        'left: ' + event.touches[0].clientX + 'px;' + 
        'top: ' + event.touches[0].clientY + 'px;'; 
    dot = document.createElement('div'); 
    dot.setAttribute('style', style); 
    body = document.getElementsByTagName('body')[0]; 
    body.appendChild(dot); 
}; 
 
(function (proto) { 
    'use strict'; 
 
    // Minimum range to begin looking at the swipe direction, in pixels 
    var SWIPE_THRESHOLD = 10; 
    // Distance in pixels required to complete a swipe gesture. 
    var SWIPE_DISTANCE = 50; 
    // Time in milliseconds to complete the gesture. 
    var SWIPE_TIMEOUT = 1000; 
    // Time in milliseconds to repeat a motion if still holding down, 
    // ... and not specified in state.metadata.key_repeat_interval. 
    var DEFAULT_REPEAT_INTERVAL = 150; 
 
    // Lookup table mapping action to keyCode. 
    var CODE = { 
        action:  88, // x 
        left:    37, // left arrow 
        right:   39, // right arrow 
        up:      38, // up arrow 
        down:    40, // down arrow 
        undo:    85, // u 
        restart: 82, // r 
        quit:    27 // escape 
    } 
 
    var TAB_STRING = [ 
        '<div class="tab">', 
        '  <div class="tab-affordance"></div>', 
        '  <div class="tab-icon">', 
        '    <div class="slice"></div>', 
        '    <div class="slice"></div>', 
        '  </div>', 
        '</div>' 
    ].join("\n"); 
 
    /** Bootstrap Methods **/ 
 
    proto.initialize = function () { 
        this.firstPos = { x: 0, y: 0 }; 
        this.setTabAnimationRatio = this.setTabAnimationRatio.bind(this); 
        this.setMenuAnimationRatio = this.setMenuAnimationRatio.bind(this); 
        this.repeatTick = this.repeatTick.bind(this); 
        this.isFocused = true; 
    }; 
 
    // assign the element that will allow tapping to toggle focus. 
    proto.setFocusElement = function (focusElement) { 
        this.focusElement = focusElement; 
        this.isFocused = false; 
        this.buildFocusIndicator(); 
    }; 
 
    proto.bindEvents = function () { 
        window.addEventListener('touchstart', this.onTouchStart.bind(this)); 
        window.addEventListener('touchend', this.onTouchEnd.bind(this)); 
        window.addEventListener('touchmove', this.onTouchMove.bind(this)); 
    }; 
 
    proto.bootstrap = function () { 
        this.showTab(); 
        this.disableScrolling(); 
        if (!this.isAudioSupported()) { 
            this.disableAudio(); 
        } 
        this.disableSelection(); 
    }; 
 
    /** Event Handlers **/ 
 
    proto.onTouchStart = function (event) { 
        if (this.isTouching) { 
            return; 
        } 
 
        // Handle focus changes used in editor. 
        this.handleFocusChange(event); 
        if (!this.isFocused) { 
            return; 
        } 
 
        if (event.target.tagName.toUpperCase() === 'A') { 
            return; 
        } 
        this.isTouching = true; 
 
        this.mayBeSwiping = true; 
        this.gestured = false; 
 
        this.swipeDirection = undefined; 
        this.swipeDistance = 0; 
        this.startTime = new Date().getTime(); 
 
        this.firstPos.x = event.touches[0].clientX; 
        this.firstPos.y = event.touches[0].clientY; 
    }; 
 
    proto.onTouchEnd = function (event) { 
        if (!this.isFocused) { 
            return; 
        } 
        if (!this.isTouching) { 
            // If we're here, the menu event handlers had probably 
            // canceled the touchstart event. 
            return; 
        } 
        if (!this.gestured) { 
            if (event.touches.length === 0 && event.target.id!=="unMuteButton" && event.target.id!=="muteButton" ) { 
                this.handleTap(); 
            } 
        } 
 
        // The last finger to be removed from the screen lets us know 
        // we aren't tracking anything. 
        if (event.touches.length === 0) { 
            this.isTouching = false; 
            this.endRepeatWatcher(); 
        } 
    }; 
 
    proto.onTouchMove = function (event) { 
        if (!this.isFocused) { 
            return; 
        } 
        if (levelEditorOpened){ 
            return; 
        } 
        if (this.isSuccessfulSwipe()) { 
            this.handleSwipe(this.swipeDirection, this.touchCount); 
            this.gestured = true; 
            this.mayBeSwiping = false; 
            this.beginRepeatWatcher(event); 
        } else if (this.mayBeSwiping) { 
            this.swipeStep(event); 
        } else if (this.isRepeating) { 
            this.repeatStep(event); 
        } 
 
        prevent(event); 
        return false; 
    }; 
 
    proto.handleFocusChange = function (event) { 
        if (!this.focusElement) { 
            return; 
        } 
 
        this.isFocused = this.isTouchInsideFocusElement(event); 
        this.setFocusIndicatorVisibility(this.isFocused); 
         
        canvas.focus(); 
        editor.display.input.blur(); 
    }; 
 
    proto.isTouchInsideFocusElement = function (event) { 
        var canvasPosition; 
 
        if (!event.touches || !event.touches[0]) { 
            return false; 
        } 
        canvasPosition = this.absoluteElementPosition(this.focusElement); 
 
        if (event.touches[0].clientX < canvasPosition.left || 
            event.touches[0].clientY < canvasPosition.top) { 
            return false; 
        } 
 
        if (event.touches[0].clientX > canvasPosition.left + this.focusElement.clientWidth || 
            event.touches[0].clientY > canvasPosition.top + this.focusElement.clientHeight) { 
            return false; 
        } 
 
        return true; 
    }; 
 
    proto.setFocusIndicatorVisibility = function (isVisible) { 
        var visibility; 
 
        visibility = 'visible'; 
        if (!isVisible) { 
            visibility = 'hidden'; 
        } 
        // this.focusIndicator.setAttribute('style', 'visibility: ' + visibility + ';'); 
    }; 
 
    proto.absoluteElementPosition = function (element) { 
        var position, body; 
 
        position = { 
            top: element.offsetTop || 0, 
            left: element.offsetLeft || 0 
        }; 
        body = document.getElementsByTagName('body')[0]; 
        position.top -= body.scrollTop || 0; 
 
        while (true) { 
            element = element.offsetParent; 
            if (!element) { 
                break; 
            } 
            position.top += element.offsetTop || 0; 
            position.left += element.offsetLeft || 0; 
        } 
 
        return position; 
    }; 
 
    proto.beginRepeatWatcher = function (event) { 
        var repeatIntervalMilliseconds; 
        if (this.repeatInterval) { 
            return; 
        } 
        this.isRepeating = true; 
        repeatIntervalMilliseconds = state.metadata.key_repeat_interval * 1000; 
        if (isNaN(repeatIntervalMilliseconds) || !repeatIntervalMilliseconds) { 
            repeatIntervalMilliseconds = DEFAULT_REPEAT_INTERVAL; 
        } 
        this.repeatInterval = setInterval(this.repeatTick, repeatIntervalMilliseconds); 
        this.recenter(event); 
    }; 
 
    proto.endRepeatWatcher = function () { 
        if (this.repeatInterval) { 
            clearInterval(this.repeatInterval); 
            delete this.repeatInterval; 
            this.isRepeating = false; 
        } 
    }; 
 
    proto.repeatTick = function () { 
        if (this.isTouching) { 
            this.handleSwipe(this.direction, this.touchCount); 
        } 
    }; 
 
    // Capture the location to consider the gamepad center. 
    proto.recenter = function (event) { 
        this.firstPos.x = event.touches[0].clientX; 
        this.firstPos.y = event.touches[0].clientY; 
    } 
 
    /** Detection Helper Methods **/ 
 
    proto.isSuccessfulSwipe = function () { 
        var isSuccessful; 
 
        if (this.mayBeSwiping && 
            this.swipeDirection !== undefined && 
            this.swipeDistance >= SWIPE_DISTANCE) { 
            isSuccessful = true; 
        } 
 
        return isSuccessful; 
    }; 
 
    // Examine the current state to see what direction they're swiping and 
    // if the gesture can still be considered a swipe. 
    proto.swipeStep = function (event) { 
        var currentPos, distance, currentTime; 
        var touchCount; 
 
        if (!this.mayBeSwiping) { 
            return; 
        } 
 
        currentPos = { 
            x: event.touches[0].clientX, 
            y: event.touches[0].clientY 
        }; 
        currentTime = new Date().getTime(); 
        touchCount = event.touches.length; 
 
        this.swipeDistance = this.cardinalDistance(this.firstPos, currentPos); 
        if (!this.swipeDirection) { 
            if (this.swipeDistance > SWIPE_THRESHOLD) { 
                // We've swiped far enough to decide what direction we're swiping in. 
                this.swipeDirection = this.dominantDirection(this.firstPos, currentPos); 
                this.touchCount = touchCount; 
            } 
        } else if (distance < SWIPE_DISTANCE) { 
            // Now that they've committed to the swipe, look for misfires... 
 
            direction = this.dominantDirection(this.firstPos, currentPos); 
            // Cancel the swipe if the direction changes. 
            if (direction !== this.swipeDirection) { 
                this.mayBeSwiping = false; 
            } 
            // If they're changing touch count at this point, it's a misfire. 
            if (touchCount < this.touchCount) { 
                this.mayBeSwiping = false; 
            } 
        } else if (currentTime - this.startTime > SWIPE_TIMEOUT) { 
            // Cancel the swipe if they took too long to finish. 
            this.mayBeSwiping = false; 
        } 
    }; 
 
    proto.repeatStep = function (event) { 
        var currentPos, distance, currentTime; 
        var newDistance, direction; 
 
        currentPos = { 
            x: event.touches[0].clientX, 
            y: event.touches[0].clientY 
        }; 
 
        newDistance = this.cardinalDistance(this.firstPos, currentPos); 
 
        if (newDistance >= SWIPE_DISTANCE) { 
            this.swipeDirection = this.dominantDirection(this.firstPos, currentPos); 
            this.recenter(event); 
        } 
    }; 
 
    // Find the distance traveled by the swipe along compass directions. 
    proto.cardinalDistance = function (firstPos, currentPos) { 
        var xDist, yDist; 
 
        xDist = Math.abs(firstPos.x - currentPos.x); 
        yDist = Math.abs(firstPos.y - currentPos.y); 
 
        return Math.max(xDist, yDist); 
    }; 
 
    // Decide which direction the touch has moved farthest. 
    proto.dominantDirection = function (firstPos, currentPos) { 
        var dx, dy; 
        var dominantAxis, dominantDirection; 
 
        dx = currentPos.x - firstPos.x; 
        dy = currentPos.y - firstPos.y; 
 
        dominantAxis = 'x'; 
        if (Math.abs(dy) > Math.abs(dx)) { 
            dominantAxis = 'y'; 
        } 
 
        if (dominantAxis === 'x') { 
            if (dx > 0) { 
                dominantDirection = 'right'; 
            } else { 
                dominantDirection = 'left'; 
            } 
        } else { 
            if (dy > 0) { 
                dominantDirection = 'down'; 
            } else { 
                dominantDirection = 'up'; 
            } 
        } 
 
        return dominantDirection; 
    }; 
 
    /** Action Methods **/ 
 
    // Method to be called when we've detected a swipe and some action 
    // is called for. 
    proto.handleSwipe = function (direction, touchCount) { 
        if (touchCount === 1) { 
            this.emitKeydown(this.swipeDirection); 
        } else if (touchCount > 1) { 
            // Since this was a multitouch gesture, open the menu. 
            this.toggleMenu(); 
        } 
    }; 
 
    proto.handleTap = function () { 
        this.emitKeydown('action'); 
    }; 
 
    // Fake out keypresses to acheive the desired effect. 
    proto.emitKeydown = function (input) { 
        if(!this.isMenuVisible && (input == 'undo' || input == 'restart' || input == 'quit')) { 
            return; 
        } 
 
        var event; 
 
        event = { keyCode: CODE[input] }; 
 
        this.fakeCanvasFocus(); 
        // Press, then release key. 
        onKeyDown(event); 
        onKeyUp(event); 
    }; 
 
    proto.fakeCanvasFocus = function () { 
        var canvas; 
 
        canvas = document.getElementById('gameCanvas'); 
        onMouseDown({ 
            button: 0, 
            target: canvas 
        }); 
    }; 
 
    proto.toggleMenu = function () { 
        if (this.isMenuVisible) { 
            this.hideMenu(); 
        } else { 
            this.showMenu(); 
        } 
    }; 
 
    proto.showMenu = function () { 
        if (!this.menuElem) { 
            this.buildMenu(); 
        } 
        this.getAnimatables().menu.animateUp(); 
        this.isMenuVisible = true; 
        this.hideTab(); 
    }; 
 
    proto.hideMenu = function () { 
        if (this.menuElem) { 
            this.getAnimatables().menu.animateDown(); 
        } 
        this.isMenuVisible = false; 
        this.showTab(); 
    }; 
 
    proto.getAnimatables = function () { 
        var self = this; 
        if (!this._animatables) { 
            this._animatables = { 
                tab: Animatable('tab', 0.1, self.setTabAnimationRatio), 
                menu: Animatable('menu', 0.1, self.setMenuAnimationRatio) 
            } 
        } 
        return this._animatables; 
    }; 
 
    proto.showTab = function () { 
        if (!this.tabElem) { 
            this.buildTab(); 
        } 
        this.getAnimatables().tab.animateDown(); 
    }; 
 
    proto.hideTab = function () { 
        if (this.tabElem) { 
            this.tabElem.setAttribute('style', 'display: none;'); 
        } 
        this.getAnimatables().tab.animateUp(); 
    }; 
 
    proto.buildTab = function () { 
        var self = this; 
        var tempElem, body; 
        var openCallback; 
        var tabElem; 
        var assemblyElem; 
 
        tempElem = document.createElement('div'); 
        tempElem.innerHTML = TAB_STRING; 
        assemblyElem = tempElem.children[0]; 
 
        openCallback = function (event) { 
            event.stopPropagation(); 
            self.showMenu(); 
        }; 
        this.tabAffordance = assemblyElem.getElementsByClassName('tab-affordance')[0]; 
        this.tabElem = assemblyElem.getElementsByClassName('tab-icon')[0]; 
 
        //the reason I'm adding all these empty click events is on safari to disable double-tap to zoom (also needs some other css settings. cf https://github.com/increpare/PuzzleScript/issues/599 SMGDH) 
        this.tabAffordance.addEventListener('touchstart', openCallback); 
        this.tabAffordance.addEventListener("click", event => {}); 
        this.tabElem.addEventListener('touchstart', openCallback); 
        this.tabElem.addEventListener("click", event => {}); 
 
        body = document.getElementsByTagName('body')[0]; 
        body.appendChild(assemblyElem); 
    }; 
 
    proto.buildMenu = function () { 
        var self = this; 
        var tempElem, body; 
        var undo, restart, quit; 
        var closeTab; 
        var closeCallback; 
 
        tempElem = document.createElement('div'); 
        tempElem.innerHTML = this.buildMenuString(state); 
        this.menuElem = tempElem.children[0]; 
        this.closeElem = this.menuElem.getElementsByClassName('close')[0]; 
 
        closeCallback = function (event) { 
            event.stopPropagation(); 
            self.hideMenu(); 
        }; 
        this.closeAffordance = this.menuElem.getElementsByClassName('close-affordance')[0]; 
        closeTab = this.menuElem.getElementsByClassName('close')[0]; 
        this.closeAffordance.addEventListener('touchstart', closeCallback); 
        this.closeAffordance.addEventListener("click", event => {}); 
        closeTab.addEventListener('touchstart', closeCallback); 
        closeTab.addEventListener("click", event => {}); 
 
        undo = this.menuElem.getElementsByClassName('undo')[0]; 
        if (undo) { 
            undo.addEventListener('touchstart', function (event) { 
                event.stopPropagation(); 
                self.emitKeydown('undo'); 
            }); 
            undo.addEventListener("click", event => {}); 
        } 
        restart = this.menuElem.getElementsByClassName('restart')[0]; 
        if (restart) { 
            restart.addEventListener('touchstart', function (event) { 
                event.stopPropagation(); 
                self.emitKeydown('restart'); 
            }); 
            restart.addEventListener("click", event => {}); 
        } 
 
        quit = this.menuElem.getElementsByClassName('quit')[0]; 
        quit.addEventListener('touchstart', function (event) { 
            event.stopPropagation(); 
            self.emitKeydown('quit'); 
        }); 
        quit.addEventListener("click", event => {}); 
 
        body = document.getElementsByTagName('body')[0]; 
        body.appendChild(this.menuElem); 
    }; 
 
    proto.buildMenuString = function (state) { 
    // Template for the menu. 
        var itemCount, menuLines; 
        var noUndo, noRestart; 
 
        noUndo = state.metadata.noundo; 
        noRestart = state.metadata.norestart; 
 
        itemCount = 3; 
        if (noUndo) { 
            itemCount -= 1; 
        } 
        if (noRestart) { 
            itemCount -= 1; 
        } 
 
        menuLines = [ 
            '<div class="mobile-menu item-count-' + itemCount + '">', 
            '  <div class="close-affordance"></div>', 
            '  <div class="close">', 
            '    <div class="slice"></div>', 
            '    <div class="slice"></div>', 
            '  </div>' 
        ]; 
 
        if (!noUndo) { 
            menuLines.push('  <div class="undo button">Undo</div>'); 
        } 
        if (!noRestart) { 
            menuLines.push('  <div class="restart button">Restart</div>'); 
        } 
        menuLines = menuLines.concat([ 
            '  <div class="quit button">Quit to Menu</div>', 
            '  <div class="clear"></div>', 
            '</div>' 
        ]); 
 
        return menuLines.join("\n"); 
    }; 
 
    proto.buildFocusIndicator = function () { 
        var focusElementParent; 
        this.focusIndicator = document.createElement('DIV'); 
        this.focusIndicator.setAttribute('class', 'tapFocusIndicator'); 
        this.focusIndicator.setAttribute('style', 'visibility: hidden;'); 
 
        focusElementParent = this.focusElement.parentNode; 
        focusElementParent.appendChild(this.focusIndicator); 
    }; 
 
    proto.setTabAnimationRatio = function (ratio) { 
        var LEFT = 18; 
        var RIGHT = 48 + 18; 
        var size, opacityString; 
        var style; 
 
        // Round away any exponents that might appear. 
        ratio = Math.round((ratio) * 1000) / 1000; 
        if (ratio >= 0.999) { 
            this.tabAffordance.setAttribute('style', 'display: none;'); 
        } else { 
            this.tabAffordance.setAttribute('style', 'display: block;'); 
        } 
        size = RIGHT * ratio + LEFT * (1 - ratio); 
        opacityString = 'opacity: ' + (1 - ratio) + ';'; 
        style = opacityString + ' ' + 
            'width: ' + size + 'px;'; 
        this.tabElem.setAttribute('style', style); 
    }; 
 
    proto.setMenuAnimationRatio = function (ratio) { 
        var LEFT = -48 - 18; 
        var RIGHT = -18; 
        var size, opacityString; 
        var style; 
 
        // Round away any exponents that might appear. 
        ratio = Math.round((ratio) * 1000) / 1000; 
 
        size = RIGHT * ratio + LEFT * (1 - ratio); 
        opacityString = 'opacity: ' + ratio + ';'; 
        style = 'left: ' + (size - 4) + 'px; ' + 
            opacityString + ' ' + 
            'width: ' + (-size) + 'px;'; 
        ratio = Math.round((ratio) * 1000) / 1000; 
 
        if (ratio <= 0.001) { 
            this.closeAffordance.setAttribute('style', 'display: none;'); 
            opacityString="display:none;" 
        } else { 
            this.closeAffordance.setAttribute('style', 'display: block;'); 
        } 
 
        this.closeElem.setAttribute('style', style); 
 
        this.menuElem.setAttribute('style', opacityString); 
    }; 
 
    proto.disableScrolling = function() { 
        var style = { 
            height: "100%", 
            overflow: "hidden", 
            position: "fixed", 
            width: "100%" 
        } 
         
        var styleString = ""; 
        for (var key in style) { 
            styleString += key + ": " + style[key] + "; "; 
        } 
 
        document.body.setAttribute('style', styleString) 
    } 
 
    /** Audio Methods **/ 
 
    proto.disableAudio = function () { 
        // Overwrite the playseed function to disable it. 
        window.playSeed = function () {}; 
    }; 
 
    proto.isAudioSupported = function () { 
        var isAudioSupported = true; 
 
        if (typeof webkitAudioContext !== 'undefined') { 
            // We may be on Mobile Safari, which throws up 
            // 'Operation not Supported' alerts when we attempt to 
            // play Audio elements with "data:audio/wav;base64" 
            // encoded HTML5 Audio elements. 
            // 
            // Switching to MP3 encoded audio may be the way we have 
            // to go to get Audio working on mobile devices. 
            // 
            // e.g. https://github.com/rioleo/webaudio-api-synthesizer 
            isAudioSupported = false; 
        } 
 
        return isAudioSupported; 
    }; 
 
    /** Other HTML5 Stuff **/ 
 
    proto.disableSelection = function () { 
        var body; 
        body = document.getElementsByTagName('body')[0]; 
        body.setAttribute('class', body.getAttribute('class') + ' disable-select'); 
    }; 
 
}(window.Mobile.GestureHandler.prototype)); 
 
window.Animator = function () { 
    this.initialize.apply(this, arguments); 
}; 
 
(function (proto) { 
    proto.initialize = function () { 
        this._animations = {}; 
        this.tick = this.tick.bind(this); 
    }; 
 
    proto.animate = function (key, tick) { 
        this._animations[key] = tick; 
        this.wakeup(); 
    }; 
 
    proto.wakeup = function () { 
        if (this._isAnimating) { 
            return; 
        } 
        this._isAnimating = true; 
        this.tick(); 
    }; 
 
    proto.tick = function () { 
        var key; 
        var isFinished, allFinished; 
        var toRemove, index; 
 
        toRemove = []; 
        allFinished = true; 
        for (key in this._animations) { 
            if (!this._animations.hasOwnProperty(key)) { 
                return; 
            } 
            isFinished = this._animations[key](); 
            if (!isFinished) { 
                allFinished = false; 
            } else { 
                toRemove.push(key); 
            } 
        } 
 
        if (!allFinished) { 
            requestAnimationFrame(this.tick); 
        } else { 
            for (index = 0; index < toRemove.length; toRemove++) { 
                delete this._isAnimating[toRemove[index]]; 
            } 
            this._isAnimating = false; 
        } 
    }; 
 
}(window.Animator.prototype)); 
 
window.Animator.getInstance = function () { 
    if (!window.Animator._instance) { 
        window.Animator._instance = new window.Animator(); 
    } 
    return window.Animator._instance; 
}; 
 
function Animatable(key, increment, update) { 
    var ratio; 
    var handles; 
 
    handles = { 
        animateUp: function () { 
            Animator.getInstance().animate(key, tickUp); 
        }, 
        animateDown: function () { 
            Animator.getInstance().animate(key, tickDown); 
        } 
    }; 
 
    ratio = 0; 
 
    function tickUp () { 
        var isFinished; 
        ratio += increment; 
        if (ratio >= 1.0) { 
            isFinished = true; 
            ratio = 1; 
        } 
        update(ratio); 
        return isFinished; 
    }; 
 
    function tickDown () { 
        var isFinished; 
        ratio -= increment; 
        if (ratio <= 0.0) { 
            isFinished = true; 
            ratio = 0; 
        } 
        update(ratio); 
        return isFinished; 
    }; 
 
    return handles; 
}; 
 
 
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 
 
// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel 
 
// MIT license 
 
(function() { 
    'use strict'; 
 
    var VENDORS = ['ms', 'moz', 'webkit', 'o']; 
    var index, lastTime; 
 
    for (index = 0; index < VENDORS.length && !window.requestAnimationFrame; index++) { 
        window.requestAnimationFrame = window[VENDORS[index] + 'RequestAnimationFrame']; 
        window.cancelAnimationFrame = window[VENDORS[index] + 'CancelAnimationFrame']; 
        if (!window.cancelAnimationFrame) { 
            window.cancelAnimationFrame = window[VENDORS[index] + 'CancelRequestAnimationFrame']; 
        } 
    } 
 
    if (!window.requestAnimationFrame) { 
        lastTime = 0; 
        window.requestAnimationFrame = function(callback, element) { 
            var currTime, timeToCall, id; 
 
            currTime = new Date().getTime(); 
            timeToCall = Math.max(0, 16 - (currTime - lastTime)); 
            id = window.setTimeout(function() { 
                callback(currTime + timeToCall); 
            }, timeToCall); 
            lastTime = currTime + timeToCall; 
 
            return id; 
        }; 
    } 
 
    if (!window.cancelAnimationFrame) { 
        window.cancelAnimationFrame = function(id) { 
            clearTimeout(id); 
        }; 
    } 
 
    Mobile.enable(); 
}());</script> <div style="z-index:2;background:transparent;position:absolute;top:0.5em;right:1em;"> <img alt="mute" id="muteButton" width="32px" height="32px" onclick="muteAudio()" style="display:none" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABl0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC4xOdTWsmQAAAIGSURBVFhH3ZchcsJAGIURiApEBbIH6BEqOQASWYHoESoQFcwgOAaiEsEBKip6BERlBQJRwQEqtu/tvj+TLMnAJpntTL+ZP/De7p/9k102YUCcc0tEbpbx4MHIAMcKQ2LM4ktmOCYH9gXIawXzQasLYGKnAphryErC53VJLiPvhNgjtog54tZ3bgDt7QpgTkyD/42Y+aQa2CG5APYvMdGnFTBC3COeEG8IY4M4uxtsSCqAfQ1ZhSdZATangdNCvhCVImheVQC6FFdKZHtkeQ8fB8QOsUCM5N0hPhBkQ8+gEQ5BXLUb+swSsu0cZXjFE/kswu7ElB6hCAeJS/isCDXZOcaIKeKdHvhBPKiN00F4l+zu1BaQtKkop1Ic5BCx8g3OfSJu5Fthj9LnBfB7Ckqzc/Dq1oihtA34LP0SpHuV7r0AYyXN6SC7SO+ley9gFpQ7SHNNkFifpP9fAcZaOvsUNC3ChbQtwq30eQGg88+QwLKNjRuS/e6toLl0bQGNsF+MmuwcdRuR7Ya2EfEJ6Z8JFOEQRK6tuHg8U4TDBdClr4eRn3uDRjhcCfsasgpPsgLs8uP4iBiryUMzqQDC/iXiFxJbA1zttg4IX9EqgxM2JBdAmBPT4Pf/SmYwr4y8PC+lBnMNWUn4vLbJBvNBtz8m4E//mtkGlK0IjhWG1Jj8EnRWMPhg8AvjeqPLyfe3igAAAABJRU5ErkJggg=="> <img alt="unmute" id="unMuteButton" width="32px" height="32px" onclick="unMuteAudio()" style="display:none" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTnU1rJkAAABqElEQVRYR72XW3LDMAwDc7QerTdPRWjBoW1aedXZGUYUAIlqP9LpLbjf77+jvs3vfvgUvkDMmiPHzGxg9HTXEjNjsB6A0Nar+MxYg+UPFoH5ccLCauE+3Wmqvke+Pk5YWAl3sJuXGiSx3wfSOsMsrKRm4i6DJJAEktB+L1YW1oG4xyAJJIGUSOsMs7A2xB0GSSAJpA3Sz8xgYYnh/8R5g+ZKFG6Q9yAQ9dS3JEcEkkBqkb8K2X8EcYEkkE5RxkH6rkz7VT10uu3wwcbrcOipB+hEg62ZmlR9hbL1wEkJndgRMpVgif1+j3yH6LsSOtGALZAE0rUPwBJICfJ1D0A2ylamfDxXke8QfVfCGdak6pXpNkZBvkP0XeUl3hvkFiKfP8CwT5BPIfavD0iQBNJhEPJnDzCjr6Q31sMfI4P0/gPM6BP20ivTnSClxrZFvkP0WQ4YCQuImfzNYLfId4i+VqL0ExDfgNUi3yF6V6LkC3AsQW6R7xB9rZeHmzhrkFrkO0R/qHcZZ4Plv3sROARHv1mvImbG4GxixbucmDVHMjOauf8qY/jt9gdOODPsYZA1BAAAAABJRU5ErkJggg=="> </div> <script>var sourceCode=__GAMEDAT__;compile(["restart"],sourceCode);</script> </body> </html> 