Jmx0OyFkb2N0eXBlIGh0bWwmZ3Q7CiZsdDtodG1sIGxhbmc9JnF1b3Q7ZW4mcXVvdDsmZ3Q7CiZsdDtoZWFkJmd0OwombHQ7bWV0YSBjaGFyc2V0PSZxdW90O3V0Zi04JnF1b3Q7IC8mZ3Q7CiZsdDttZXRhIG5hbWU9JnF1b3Q7dmlld3BvcnQmcXVvdDsgY29udGVudD0mcXVvdDt3aWR0aD1kZXZpY2Utd2lkdGgsaW5pdGlhbC1zY2FsZT0xJnF1b3Q7IC8mZ3Q7CiZsdDt0aXRsZSZndDtFbmRsZXNzIFJ1bm5lciAmbWRhc2g7IERlbW8mbHQ7L3RpdGxlJmd0OwombHQ7c3R5bGUmZ3Q7CiAgOnJvb3QgewogICAgLS1iZzojODdDRUVCOwogICAgLS1ncm91bmQ6IzRiM2IyZjsKICAgIC0tdWk6I2ZmZmZmZmNjOwogIH0KICBodG1sLGJvZHl7aGVpZ2h0OjEwMCU7bWFyZ2luOjA7Zm9udC1mYW1pbHk6c3lzdGVtLXVpLC1hcHBsZS1zeXN0ZW0sU2Vnb2UgVUksUm9ib3RvLCZxdW90O0hlbHZldGljYSBOZXVlJnF1b3Q7LEFyaWFsO30KICAjZ2FtZVdyYXB7ZGlzcGxheTpmbGV4O2FsaWduLWl0ZW1zOmNlbnRlcjtqdXN0aWZ5LWNvbnRlbnQ6Y2VudGVyO2hlaWdodDoxMDAlO2JhY2tncm91bmQ6bGluZWFyLWdyYWRpZW50KCM3ZWM4ZmYgMCUsIHZhcigtLWJnKSA3MCUpO292ZXJmbG93OmhpZGRlbn0KICBjYW52YXN7YmFja2dyb3VuZDp0cmFuc3BhcmVudDtkaXNwbGF5OmJsb2NrO2JvcmRlci1yYWRpdXM6MTJweDtib3gtc2hhZG93OjAgOHB4IDMwcHggcmdiYSgwLDAsMCwuMjUpfQogICN1aSB7CiAgICBwb3NpdGlvbjogYWJzb2x1dGU7IHRvcDoxOHB4OyBsZWZ0OjE4cHg7IGNvbG9yOiMxMTE7IGZvbnQtd2VpZ2h0OjYwMDsKICAgIGJhY2tncm91bmQ6dmFyKC0tdWkpOyBwYWRkaW5nOi41cmVtIC44cmVtOyBib3JkZXItcmFkaXVzOjhweDsKICAgIGJveC1zaGFkb3c6MCA2cHggMTZweCByZ2JhKDAsMCwwLC4xMik7CiAgfQogICN0aXAgeyBwb3NpdGlvbjphYnNvbHV0ZTsgYm90dG9tOjE4cHg7IGxlZnQ6MThweDsgY29sb3I6IzExMTsgYmFja2dyb3VuZDp2YXIoLS11aSk7IHBhZGRpbmc6LjQ1cmVtIC43cmVtO2JvcmRlci1yYWRpdXM6OHB4O30KICAjb3ZlcmxheSB7CiAgICBwb3NpdGlvbjphYnNvbHV0ZTsgaW5zZXQ6MDsgZGlzcGxheTpmbGV4OyBhbGlnbi1pdGVtczpjZW50ZXI7IGp1c3RpZnktY29udGVudDpjZW50ZXI7IHBvaW50ZXItZXZlbnRzOm5vbmU7CiAgfQogIC5jZW50ZXJCb3h7cG9pbnRlci1ldmVudHM6YXV0bzsgYmFja2dyb3VuZDojZmZmOyBwYWRkaW5nOjE4cHggMjJweDsgYm9yZGVyLXJhZGl1czoxMnB4OyBib3gtc2hhZG93OjAgOHB4IDMwcHggcmdiYSgwLDAsMCwuMjUpOyB0ZXh0LWFsaWduOmNlbnRlcjt9CiAgYnV0dG9ue3BhZGRpbmc6MTBweCAxNHB4O2JvcmRlci1yYWRpdXM6OHB4O2JvcmRlcjowO2JhY2tncm91bmQ6IzBhODRmZjtjb2xvcjojZmZmO2ZvbnQtd2VpZ2h0OjcwMDtjdXJzb3I6cG9pbnRlcn0KICBzbWFsbHtkaXNwbGF5OmJsb2NrO29wYWNpdHk6Ljg7bWFyZ2luLXRvcDo2cHh9CiZsdDsvc3R5bGUmZ3Q7CiZsdDsvaGVhZCZndDsKJmx0O2JvZHkmZ3Q7CiZsdDtkaXYgaWQ9JnF1b3Q7Z2FtZVdyYXAmcXVvdDsmZ3Q7CiAgJmx0O2NhbnZhcyBpZD0mcXVvdDtnYW1lJnF1b3Q7IHdpZHRoPSZxdW90OzcyMCZxdW90OyBoZWlnaHQ9JnF1b3Q7MTI4MCZxdW90OyZndDsmbHQ7L2NhbnZhcyZndDsKCiAgJmx0O2RpdiBpZD0mcXVvdDt1aSZxdW90OyZndDtTY29yZTogJmx0O3NwYW4gaWQ9JnF1b3Q7c2NvcmUmcXVvdDsmZ3Q7MCZsdDsvc3BhbiZndDsgJmFtcDtuYnNwOyBTcGVlZDogJmx0O3NwYW4gaWQ9JnF1b3Q7c3BlZWQmcXVvdDsmZ3Q7MSZsdDsvc3BhbiZndDt4Jmx0Oy9kaXYmZ3Q7CiAgJmx0O2RpdiBpZD0mcXVvdDt0aXAmcXVvdDsmZ3Q7Q29udHJvbHM6ICZsYXJyOyAmcmFycjsgdG8gY2hhbmdlIGxhbmVzLCAmdWFycjsgb3IgU3BhY2UgdG8ganVtcC4gU3dpcGUgb24gbW9iaWxlLiZsdDsvZGl2Jmd0OwoKICAmbHQ7ZGl2IGlkPSZxdW90O292ZXJsYXkmcXVvdDsgc3R5bGU9JnF1b3Q7ZGlzcGxheTpub25lOyZxdW90OyZndDsKICAgICZsdDtkaXYgY2xhc3M9JnF1b3Q7Y2VudGVyQm94JnF1b3Q7Jmd0OwogICAgICAmbHQ7aDIgaWQ9JnF1b3Q7bXNnVGl0bGUmcXVvdDsmZ3Q7R2FtZSBPdmVyJmx0Oy9oMiZndDsKICAgICAgJmx0O3AgaWQ9JnF1b3Q7bXNnVGV4dCZxdW90OyZndDtZb3VyIHNjb3JlOiAmbHQ7c3BhbiBpZD0mcXVvdDtmaW5hbFNjb3JlJnF1b3Q7Jmd0OzAmbHQ7L3NwYW4mZ3Q7Jmx0Oy9wJmd0OwogICAgICAmbHQ7ZGl2IHN0eWxlPSZxdW90O21hcmdpbi10b3A6MTBweCZxdW90OyZndDsmbHQ7YnV0dG9uIGlkPSZxdW90O3Jlc3RhcnRCdG4mcXVvdDsmZ3Q7UmVzdGFydCZsdDsvYnV0dG9uJmd0OyZsdDsvZGl2Jmd0OwogICAgICAmbHQ7c21hbGwmZ3Q7VGlwOiB0cnkgdG8gY29sbGVjdCBjb2lucyBhbmQgZG9kZ2Ugb2JzdGFjbGVzLiBTcGVlZCBpbmNyZWFzZXMgb3ZlciB0aW1lLiZsdDsvc21hbGwmZ3Q7CiAgICAmbHQ7L2RpdiZndDsKICAmbHQ7L2RpdiZndDsKJmx0Oy9kaXYmZ3Q7CgombHQ7c2NyaXB0Jmd0OwovKiAtLS0gQ29uZmlnIC0tLSAqLwpjb25zdCBjb25maWcgPSB7CiAgY2FudmFzV2lkdGg6IDU0MCwgICAgICAgICAgICAgLy8gbG9naWNhbCBjYW52YXMgd2lkdGggKHdlIHNjYWxlIHRvIENTUyBzaXplKQogIGNhbnZhc0hlaWdodDogOTYwLCAgICAgICAgICAgIC8vIGxvZ2ljYWwgY2FudmFzIGhlaWdodAogIGxhbmVzOiAzLAogIGxhbmVXaWR0aFBjdDogMC4yNSwgICAgICAgICAgIC8vIHZpc3VhbCBsYW5lIHdpZHRoIHJlbGF0aXZlIHRvIGNhbnZhcyB3aWR0aAogIGdyb3VuZEhlaWdodDogMTQwLAogIGluaXRpYWxTcGVlZDogNCwgICAgICAgICAgICAgIC8vIHdvcmxkIHNjcm9sbCBzcGVlZAogIHNwZWVkSW5jcmVhc2VFdmVyeTogMzAwMCwgICAgIC8vIG1zCiAgc3BlZWRJbmNyZWFzZUFtb3VudDogMC4zLAogIGp1bXBWZWxvY2l0eTogLTE4LAogIGdyYXZpdHk6IDEsCiAgY29pblNwYXduSW50ZXJ2YWw6IDkwMCwKICBvYnN0YWNsZVNwYXduSW50ZXJ2YWw6IDEwMDAsCiAgcGxheWVyV2lkdGg6IDY0LAogIHBsYXllckhlaWdodDogOTYsCn07CgovKiAtLS0gU2V0dXAgY2FudmFzICZhbXA7IHNjYWxpbmcgZm9yIHJldGluYSAtLS0gKi8KY29uc3QgY2FudmFzID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2dhbWUnKTsKY29uc3QgY3R4ID0gY2FudmFzLmdldENvbnRleHQoJzJkJyk7CmNvbnN0IHNjb3JlRWwgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnc2NvcmUnKTsKY29uc3Qgc3BlZWRFbCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdzcGVlZCcpOwpjb25zdCBvdmVybGF5ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ292ZXJsYXknKTsKY29uc3QgZmluYWxTY29yZUVsID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2ZpbmFsU2NvcmUnKTsKY29uc3QgcmVzdGFydEJ0biA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdyZXN0YXJ0QnRuJyk7CgpmdW5jdGlvbiByZXNpemVDYW52YXMoKSB7CiAgLy8gbWFpbnRhaW4gYXNwZWN0IHJhdGlvIHVzaW5nIGxvZ2ljYWwgY2FudmFzIHNpemUsIGZpdCB3aXRoaW4gd2luZG93CiAgY29uc3Qgd3JhcCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdnYW1lV3JhcCcpOwogIGNvbnN0IG1heFcgPSBNYXRoLm1pbih3aW5kb3cuaW5uZXJXaWR0aCAtIDQwLCA0MjApOwogIGNvbnN0IHNjYWxlID0gbWF4VyAvIGNvbmZpZy5jYW52YXNXaWR0aDsKICBjYW52YXMuc3R5bGUud2lkdGggPSAoY29uZmlnLmNhbnZhc1dpZHRoICogc2NhbGUpICsgJ3B4JzsKICBjYW52YXMuc3R5bGUuaGVpZ2h0ID0gKGNvbmZpZy5jYW52YXNIZWlnaHQgKiBzY2FsZSkgKyAncHgnOwp9CnJlc2l6ZUNhbnZhcygpOwp3aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcigncmVzaXplJywgcmVzaXplQ2FudmFzKTsKCi8qIC0tLSBHYW1lIHN0YXRlIC0tLSAqLwpsZXQgc3RhdGUgPSB7CiAgcnVubmluZzogZmFsc2UsCiAgc2NvcmU6IDAsCiAgc3BlZWQ6IGNvbmZpZy5pbml0aWFsU3BlZWQsCiAgbGFzdFNwZWVkVXA6IHBlcmZvcm1hbmNlLm5vdygpLAogIHBsYXllcjogbnVsbCwKICBvYnN0YWNsZXM6IFtdLAogIGNvaW5zOiBbXSwKICBsYXN0Q29pbkF0OiAwLAogIGxhc3RPYnN0YWNsZUF0OiAwLAogIGxhc3RGcmFtZTogMCwKICBsYW5lc1g6IFtdLAogIGdyb3VuZFk6IGNvbmZpZy5jYW52YXNIZWlnaHQgLSBjb25maWcuZ3JvdW5kSGVpZ2h0LAp9OwoKLyogLS0tIFV0aWxpdHkgZnVuY3Rpb25zIC0tLSAqLwpmdW5jdGlvbiByYW5kUmFuZ2UoYSxiKXtyZXR1cm4gTWF0aC5yYW5kb20oKSooYi1hKSthfQpmdW5jdGlvbiByZWN0c0NvbGxpZGUoYSxiKXsKICByZXR1cm4gIShhLnggKyBhLncgJmx0OyBiLnggfHwgYS54ICZndDsgYi54ICsgYi53IHx8IGEueSArIGEuaCAmbHQ7IGIueSB8fCBhLnkgJmd0OyBiLnkgKyBiLmgpOwp9CgovKiAtLS0gUGxheWVyIG9iamVjdCAtLS0gKi8KZnVuY3Rpb24gY3JlYXRlUGxheWVyKCkgewogIGNvbnN0IGxhbmVQb3NpdGlvbnMgPSBzdGF0ZS5sYW5lc1g7CiAgcmV0dXJuIHsKICAgIGxhbmU6IDEsIC8vIDAuLmxhbmVzLTEsIHN0YXJ0IGNlbnRlciBsYW5lCiAgICB4OiBsYW5lUG9zaXRpb25zWzFdIC0gY29uZmlnLnBsYXllcldpZHRoLzIsCiAgICB5OiBzdGF0ZS5ncm91bmRZIC0gY29uZmlnLnBsYXllckhlaWdodCwKICAgIHZ4OiAwLAogICAgdnk6IDAsCiAgICB3OiBjb25maWcucGxheWVyV2lkdGgsCiAgICBoOiBjb25maWcucGxheWVySGVpZ2h0LAogICAgaXNKdW1waW5nOiBmYWxzZSwKICAgIHRhcmdldFg6IG51bGwsIC8vIGZvciBzbW9vdGggbGFuZSBtb3ZlcwogICAgbW92ZVByb2dyZXNzOiAwCiAgfTsKfQoKLyogLS0tIEluaXQgbGFuZXMgcG9zaXRpb25zIC0tLSAqLwpmdW5jdGlvbiBpbml0TGFuZXMoKSB7CiAgY29uc3QgVyA9IGNvbmZpZy5jYW52YXNXaWR0aDsKICAvLyB1c2UgdGhyZWUgbGFuZXMgY2VudGVyZWQgaW4gbWlkZGxlIG9mIGNhbnZhcyBob3Jpem9udGFsbHkKICBjb25zdCBjZW50ZXJYID0gVy8yOwogIGNvbnN0IHNwYWNpbmcgPSBNYXRoLm1pbigxMjAsIFcgKiAwLjIyKTsKICBjb25zdCBwb3NpdGlvbnMgPSBbXTsKICBjb25zdCBvZmYgPSBzcGFjaW5nOwogIC8vIGxhbmVzIGNlbnRlcmVkOiBsZWZ0LCBjZW50ZXIsIHJpZ2h0CiAgcG9zaXRpb25zLnB1c2goY2VudGVyWCAtIG9mZik7CiAgcG9zaXRpb25zLnB1c2goY2VudGVyWCk7CiAgcG9zaXRpb25zLnB1c2goY2VudGVyWCArIG9mZik7CiAgc3RhdGUubGFuZXNYID0gcG9zaXRpb25zOwp9CgovKiAtLS0gUmVzZXQgLyBTdGFydCAvIEdhbWUgT3ZlciAtLS0gKi8KZnVuY3Rpb24gcmVzZXRHYW1lKCkgewogIGluaXRMYW5lcygpOwogIHN0YXRlLnNjb3JlID0gMDsKICBzdGF0ZS5zcGVlZCA9IGNvbmZpZy5pbml0aWFsU3BlZWQ7CiAgc3RhdGUubGFzdFNwZWVkVXAgPSBwZXJmb3JtYW5jZS5ub3coKTsKICBzdGF0ZS5vYnN0YWNsZXMgPSBbXTsKICBzdGF0ZS5jb2lucyA9IFtdOwogIHN0YXRlLmxhc3RDb2luQXQgPSAwOwogIHN0YXRlLmxhc3RPYnN0YWNsZUF0ID0gMDsKICBzdGF0ZS5wbGF5ZXIgPSBjcmVhdGVQbGF5ZXIoKTsKICBzdGF0ZS5ydW5uaW5nID0gdHJ1ZTsKICBvdmVybGF5LnN0eWxlLmRpc3BsYXkgPSAnbm9uZSc7CiAgc2NvcmVFbC50ZXh0Q29udGVudCA9ICcwJzsKICBzcGVlZEVsLnRleHRDb250ZW50ID0gc3RhdGUuc3BlZWQudG9GaXhlZCgxKTsKICBsYXN0RnJhbWVUaW1lID0gcGVyZm9ybWFuY2Uubm93KCk7Cn0KCmZ1bmN0aW9uIGdhbWVPdmVyKCkgewogIHN0YXRlLnJ1bm5pbmcgPSBmYWxzZTsKICBmaW5hbFNjb3JlRWwudGV4dENvbnRlbnQgPSBNYXRoLmZsb29yKHN0YXRlLnNjb3JlKTsKICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbXNnVGl0bGUnKS50ZXh0Q29udGVudCA9ICdHYW1lIE92ZXInOwogIG92ZXJsYXkuc3R5bGUuZGlzcGxheSA9ICdmbGV4JzsKfQoKLyogLS0tIFNwYXduaW5nIC0tLSAqLwpmdW5jdGlvbiBzcGF3bk9ic3RhY2xlKCkgewogIC8vIG9ic3RhY2xlcyBhcHBlYXIgaW4gcmFuZG9tIGxhbmVzIHdpdGggdmFyaWFibGUgc2l6ZQogIGNvbnN0IGxhbmUgPSBNYXRoLmZsb29yKE1hdGgucmFuZG9tKCkgKiBjb25maWcubGFuZXMpOwogIGNvbnN0IHggPSBzdGF0ZS5sYW5lc1hbbGFuZV0gLSA0MDsgLy8gb2JzdGFjbGUgY2VudGVyZWQgb24gbGFuZQogIGNvbnN0IHcgPSA1NiArIE1hdGguZmxvb3IoTWF0aC5yYW5kb20oKSozMCk7CiAgY29uc3QgaCA9IDU2ICsgTWF0aC5mbG9vcihNYXRoLnJhbmRvbSgpKjM2KTsKICBjb25zdCB5ID0gc3RhdGUuZ3JvdW5kWSAtIGg7CiAgc3RhdGUub2JzdGFjbGVzLnB1c2goe3g6IHgsIHk6IHksIHc6IHcsIGg6IGgsIGxhbmV9KTsKfQoKZnVuY3Rpb24gc3Bhd25Db2luKCkgewogIGNvbnN0IGxhbmUgPSBNYXRoLmZsb29yKE1hdGgucmFuZG9tKCkgKiBjb25maWcubGFuZXMpOwogIGNvbnN0IHggPSBzdGF0ZS5sYW5lc1hbbGFuZV0gLSAyMDsKICBjb25zdCB5ID0gc3RhdGUuZ3JvdW5kWSAtIGNvbmZpZy5wbGF5ZXJIZWlnaHQgLSByYW5kUmFuZ2UoMjAsIDEyMCk7CiAgc3RhdGUuY29pbnMucHVzaCh7eDogeCwgeTogeSwgdzogMzYsIGg6IDM2LCBsYW5lfSk7Cn0KCi8qIC0tLSBJbnB1dCBoYW5kbGluZyAtLS0gKi8KbGV0IHRvdWNoU3RhcnQgPSBudWxsOwpmdW5jdGlvbiBoYW5kbGVLZXkoZSl7CiAgaWYoIXN0YXRlLnJ1bm5pbmcpIHJldHVybjsKICBpZihlLmtleSA9PT0gJnF1b3Q7QXJyb3dMZWZ0JnF1b3Q7KSBjaGFuZ2VMYW5lKC0xKTsKICBpZihlLmtleSA9PT0gJnF1b3Q7QXJyb3dSaWdodCZxdW90OykgY2hhbmdlTGFuZSgxKTsKICBpZihlLmtleSA9PT0gJnF1b3Q7QXJyb3dVcCZxdW90OyB8fCBlLmNvZGUgPT09ICZxdW90O1NwYWNlJnF1b3Q7KSBqdW1wKCk7Cn0Kd2luZG93LmFkZEV2ZW50TGlzdGVuZXIoJ2tleWRvd24nLCBoYW5kbGVLZXkpOwoKZnVuY3Rpb24gY2hhbmdlTGFuZShkaXIpewogIGNvbnN0IHAgPSBzdGF0ZS5wbGF5ZXI7CiAgY29uc3QgbmV3TGFuZSA9IE1hdGgubWF4KDAsIE1hdGgubWluKGNvbmZpZy5sYW5lcy0xLCBwLmxhbmUgKyBkaXIpKTsKICBpZihuZXdMYW5lID09PSBwLmxhbmUpIHJldHVybjsKICBwLmxhbmUgPSBuZXdMYW5lOwogIHAudGFyZ2V0WCA9IHN0YXRlLmxhbmVzWFtuZXdMYW5lXSAtIHAudy8yOwogIHAubW92ZVByb2dyZXNzID0gMDsKfQoKZnVuY3Rpb24ganVtcCgpewogIGNvbnN0IHAgPSBzdGF0ZS5wbGF5ZXI7CiAgaWYoIXAuaXNKdW1waW5nKXsKICAgIHAudnkgPSBjb25maWcuanVtcFZlbG9jaXR5OwogICAgcC5pc0p1bXBpbmcgPSB0cnVlOwogIH0KfQoKLyogVG91Y2ggLyBzd2lwZSBzdXBwb3J0ICovCmNhbnZhcy5hZGRFdmVudExpc3RlbmVyKCd0b3VjaHN0YXJ0JywgKGV2KT0mZ3Q7ewogIGlmKCFzdGF0ZS5ydW5uaW5nKSByZXR1cm47CiAgdG91Y2hTdGFydCA9IGV2LnRvdWNoZXNbMF07Cn0sIHtwYXNzaXZlOnRydWV9KTsKY2FudmFzLmFkZEV2ZW50TGlzdGVuZXIoJ3RvdWNoZW5kJywgKGV2KT0mZ3Q7ewogIGlmKCFzdGF0ZS5ydW5uaW5nKSByZXR1cm47CiAgaWYoIXRvdWNoU3RhcnQpIHJldHVybjsKICBjb25zdCBlbmQgPSBldi5jaGFuZ2VkVG91Y2hlc1swXTsKICBjb25zdCBkeCA9IGVuZC5jbGllbnRYIC0gdG91Y2hTdGFydC5jbGllbnRYOwogIGNvbnN0IGR5ID0gZW5kLmNsaWVudFkgLSB0b3VjaFN0YXJ0LmNsaWVudFk7CiAgY29uc3QgYWR4ID0gTWF0aC5hYnMoZHgpLCBhZHkgPSBNYXRoLmFicyhkeSk7CiAgaWYoYWR4ICZndDsgNDAgJmFtcDsmYW1wOyBhZHggJmd0OyBhZHkpIHsKICAgIGlmKGR4ICZndDsgMCkgY2hhbmdlTGFuZSgxKTsgZWxzZSBjaGFuZ2VMYW5lKC0xKTsKICB9IGVsc2UgaWYoYWR5ICZndDsgNDAgJmFtcDsmYW1wOyBhZHkgJmd0OyBhZHgpIHsKICAgIGlmKGR5ICZsdDsgMCkganVtcCgpOwogIH0gZWxzZSB7CiAgICAvLyB0YXA6IGxlZnQgb3IgcmlnaHQgaGFsZgogICAgY29uc3QgcmVjdCA9IGNhbnZhcy5nZXRCb3VuZGluZ0NsaWVudFJlY3QoKTsKICAgIGNvbnN0IGN4ID0gKHRvdWNoU3RhcnQuY2xpZW50WCAtIHJlY3QubGVmdCkgLyByZWN0LndpZHRoICogY29uZmlnLmNhbnZhc1dpZHRoOwogICAgaWYoY3ggJmx0OyBjb25maWcuY2FudmFzV2lkdGgvMikgY2hhbmdlTGFuZSgtMSk7IGVsc2UgY2hhbmdlTGFuZSgxKTsKICB9CiAgdG91Y2hTdGFydCA9IG51bGw7Cn0sIHtwYXNzaXZlOnRydWV9KTsKCi8qIC0tLSBHYW1lIExvb3AgLS0tICovCmxldCBsYXN0RnJhbWVUaW1lID0gcGVyZm9ybWFuY2Uubm93KCk7CmZ1bmN0aW9uIHVwZGF0ZShub3cpIHsKICBpZighc3RhdGUucnVubmluZykgewogICAgcmV0dXJuOwogIH0KICBjb25zdCBkdCA9IE1hdGgubWluKDQwLCBub3cgLSBsYXN0RnJhbWVUaW1lKTsKICBsYXN0RnJhbWVUaW1lID0gbm93OwoKICAvLyBzcGVlZCBpbmNyZWFzZSBvdmVyIHRpbWUKICBpZihub3cgLSBzdGF0ZS5sYXN0U3BlZWRVcCAmZ3Q7IGNvbmZpZy5zcGVlZEluY3JlYXNlRXZlcnkpewogICAgc3RhdGUuc3BlZWQgKz0gY29uZmlnLnNwZWVkSW5jcmVhc2VBbW91bnQ7CiAgICBzdGF0ZS5sYXN0U3BlZWRVcCA9IG5vdzsKICAgIHNwZWVkRWwudGV4dENvbnRlbnQgPSBzdGF0ZS5zcGVlZC50b0ZpeGVkKDEpOwogIH0KCiAgLy8gc3Bhd24gY29pbnMgJmFtcDsgb2JzdGFjbGVzCiAgaWYobm93IC0gc3RhdGUubGFzdENvaW5BdCAmZ3Q7IGNvbmZpZy5jb2luU3Bhd25JbnRlcnZhbCl7CiAgICBzcGF3bkNvaW4oKTsKICAgIHN0YXRlLmxhc3RDb2luQXQgPSBub3c7CiAgfQogIGlmKG5vdyAtIHN0YXRlLmxhc3RPYnN0YWNsZUF0ICZndDsgY29uZmlnLm9ic3RhY2xlU3Bhd25JbnRlcnZhbCl7CiAgICBzcGF3bk9ic3RhY2xlKCk7CiAgICBzdGF0ZS5sYXN0T2JzdGFjbGVBdCA9IG5vdzsKICB9CgogIC8vIG1vdmUgb2JzdGFjbGVzICZhbXA7IGNvaW5zIHRvd2FyZCBwbGF5ZXIgKHNpbXVsYXRlIGZvcndhcmQgbW92ZW1lbnQpCiAgY29uc3Qgc2Nyb2xsID0gc3RhdGUuc3BlZWQgKiAoZHQvMTYpOwogIGZvcihjb25zdCBvIG9mIHN0YXRlLm9ic3RhY2xlcykgewogICAgby55ICs9IHNjcm9sbCAqIDAuMDsgLy8gb2JzdGFjbGVzIGZpeGVkIG9uIGdyb3VuZCBpbiBvdXIgdmlldzsgd2UnbGwgc2ltdWxhdGUgZm9yd2FyZCBieSBkZWNyZWFzaW5nIHgKICAgIG8ueCAtPSBzY3JvbGwgKiA2OyAgIC8vIG1vdmUgbGVmdCBhcyBpZiBwbGF5ZXIgaXMgcnVubmluZyBmb3J3YXJkCiAgfQogIGZvcihjb25zdCBjIG9mIHN0YXRlLmNvaW5zKSB7CiAgICBjLnggLT0gc2Nyb2xsICogNjsKICB9CgogIC8vIHJlbW92ZSBvZmZzY3JlZW4KICBzdGF0ZS5vYnN0YWNsZXMgPSBzdGF0ZS5vYnN0YWNsZXMuZmlsdGVyKG8gPSZndDsgby54ICsgby53ICZndDsgLTUwKTsKICBzdGF0ZS5jb2lucyA9IHN0YXRlLmNvaW5zLmZpbHRlcihjID0mZ3Q7IGMueCArIGMudyAmZ3Q7IC01MCk7CgogIC8vIHBsYXllciBtb3ZlbWVudDogbGFuZSBzbW9vdGhpbmcKICBjb25zdCBwID0gc3RhdGUucGxheWVyOwogIGlmKHAudGFyZ2V0WCAhPT0gbnVsbCl7CiAgICBwLm1vdmVQcm9ncmVzcyArPSAwLjE4ICogKGR0LzE2KTsKICAgIHAueCA9IHAueCArIChwLnRhcmdldFggLSBwLngpICogTWF0aC5taW4oMSwgcC5tb3ZlUHJvZ3Jlc3MpOwogICAgaWYoTWF0aC5hYnMocC54IC0gcC50YXJnZXRYKSAmbHQ7IDEpIHsgcC54ID0gcC50YXJnZXRYOyBwLnRhcmdldFggPSBudWxsOyBwLm1vdmVQcm9ncmVzcyA9IDA7IH0KICB9IGVsc2UgewogICAgLy8gZW5zdXJlIHBsYXllciB4IHRpZWQgdG8gbGFuZQogICAgcC54ID0gc3RhdGUubGFuZXNYW3AubGFuZV0gLSBwLncvMjsKICB9CgogIC8vIGdyYXZpdHkgJmFtcDsganVtcCBwaHlzaWNzCiAgaWYocC5pc0p1bXBpbmcpewogICAgcC52eSArPSBjb25maWcuZ3Jhdml0eSAqIChkdC8xNik7CiAgICBwLnkgKz0gcC52eSAqIChkdC8xNik7CiAgICBpZihwLnkgJmd0Oz0gc3RhdGUuZ3JvdW5kWSAtIHAuaCl7CiAgICAgIHAueSA9IHN0YXRlLmdyb3VuZFkgLSBwLmg7CiAgICAgIHAudnkgPSAwOwogICAgICBwLmlzSnVtcGluZyA9IGZhbHNlOwogICAgfQogIH0KCiAgLy8gY29sbGlzaW9uczogb2JzdGFjbGVzCiAgY29uc3QgcGxheWVyUmVjdCA9IHt4OnAueCwgeTpwLnksIHc6cC53LCBoOnAuaH07CiAgLy8gb2JzdGFjbGVzCiAgZm9yKGNvbnN0IG8gb2Ygc3RhdGUub2JzdGFjbGVzKXsKICAgIGNvbnN0IG9SZWN0ID0ge3g6by54LCB5Om8ueSwgdzpvLncsIGg6by5ofTsKICAgIGlmKHJlY3RzQ29sbGlkZShwbGF5ZXJSZWN0LCBvUmVjdCkpewogICAgICAvLyBzaW1wbGUgcnVsZTogY29sbGlzaW9uIGlmIHBsYXllciBsb3cgZW5vdWdoCiAgICAgIGlmKHAueSArIHAuaCAtIDEwICZndDsgby55KXsKICAgICAgICBnYW1lT3ZlcigpOwogICAgICAgIHJldHVybjsKICAgICAgfQogICAgfQogIH0KICAvLyBjb2lucwogIGZvcihsZXQgaSA9IHN0YXRlLmNvaW5zLmxlbmd0aCAtMTsgaSZndDs9MDsgaS0tKXsKICAgIGNvbnN0IGMgPSBzdGF0ZS5jb2luc1tpXTsKICAgIGlmKHJlY3RzQ29sbGlkZShwbGF5ZXJSZWN0LCB7eDpjLngseTpjLnksdzpjLncsaDpjLmh9KSl7CiAgICAgIHN0YXRlLmNvaW5zLnNwbGljZShpLDEpOwogICAgICBzdGF0ZS5zY29yZSArPSA1MDsKICAgIH0KICB9CgogIC8vIHNjb3JpbmcgYnkgZGlzdGFuY2UKICBzdGF0ZS5zY29yZSArPSBzdGF0ZS5zcGVlZCAqIChkdC8xMDAwKSAqIDEwOwogIHNjb3JlRWwudGV4dENvbnRlbnQgPSBNYXRoLmZsb29yKHN0YXRlLnNjb3JlKTsKCiAgLy8gZHJhdwogIGRyYXcoKTsKCiAgcmVxdWVzdEFuaW1hdGlvbkZyYW1lKHVwZGF0ZSk7Cn0KCi8qIC0tLSBSZW5kZXJpbmcgLS0tICovCmZ1bmN0aW9uIGRyYXdCYWNrZ3JvdW5kKCl7CiAgY29uc3QgVyA9IGNvbmZpZy5jYW52YXNXaWR0aCwgSCA9IGNvbmZpZy5jYW52YXNIZWlnaHQ7CiAgLy8gc2t5IGdyYWRpZW50IGFscmVhZHkgaW4gQ1NTOyB3ZSdsbCBkcmF3IGRpc3RhbnQgZGV0YWlscwogIC8vIGRyYXcgZ3JvdW5kCiAgY3R4LmZpbGxTdHlsZSA9ICcjM2YyZjI1JzsKICBjdHguZmlsbFJlY3QoMCwgc3RhdGUuZ3JvdW5kWSwgVywgSCAtIHN0YXRlLmdyb3VuZFkpOwoKICAvLyBkcmF3IGxhbmVzIGxpbmVzCiAgY3R4LnN0cm9rZVN0eWxlID0gJyNmZmZmZmYyMic7CiAgY3R4LmxpbmVXaWR0aCA9IDI7CiAgZm9yKGxldCBpPTA7aSZsdDtjb25maWcubGFuZXM7aSsrKXsKICAgIGNvbnN0IHggPSBzdGF0ZS5sYW5lc1hbaV07CiAgICAvLyBicmllZiBkYXNoZWQgbWFya2VyIGFoZWFkCiAgICBjb25zdCBzZWdZID0gc3RhdGUuZ3JvdW5kWSAtIDIwOwogICAgY3R4LmJlZ2luUGF0aCgpOwogICAgZm9yKGxldCB0PTA7dCZsdDsxMDt0KyspewogICAgICBjdHgubW92ZVRvKHggKyAoaS0xKSo2LCBzZWdZIC0gdCozMCk7CiAgICAgIGN0eC5saW5lVG8oeCArIChpLTEpKjYgKyA4LCBzZWdZIC0gdCozMCAtIDEyKTsKICAgIH0KICAgIGN0eC5zdHJva2UoKTsKICB9Cn0KCmZ1bmN0aW9uIGRyYXdQbGF5ZXIoKXsKICBjb25zdCBwID0gc3RhdGUucGxheWVyOwogIC8vIHNpbXBsZSByb3VuZGVkIHJlY3RhbmdsZSBib2R5CiAgY3R4LnNhdmUoKTsKICAvLyBzaGFkb3cKICBjdHguZmlsbFN0eWxlID0gJ3JnYmEoMCwwLDAsMC4xOCknOwogIGN0eC5iZWdpblBhdGgoKTsKICBjdHguZWxsaXBzZShwLnggKyBwLncvMiwgc3RhdGUuZ3JvdW5kWSArIDgsIHAudy8yLCAxMCwgMCwgMCwgTWF0aC5QSSoyKTsKICBjdHguZmlsbCgpOwoKICAvLyBib2R5CiAgY3R4LmZpbGxTdHlsZSA9ICcjZmY1YTVmJzsKICByb3VuZFJlY3QoY3R4LCBwLngsIHAueSwgcC53LCBwLmgsIDEwLCB0cnVlLCBmYWxzZSk7CgogIC8vIGZhY2UgLyBleWVzCiAgY3R4LmZpbGxTdHlsZSA9ICcjZmZmJzsKICBjdHguZmlsbFJlY3QocC54ICsgcC53KjAuMiwgcC55ICsgcC5oKjAuMjUsIHAudyowLjIyLCBwLmgqMC4xOCk7CiAgY3R4LmZpbGxSZWN0KHAueCArIHAudyowLjU4LCBwLnkgKyBwLmgqMC4yNSwgcC53KjAuMjIsIHAuaCowLjE4KTsKICAvLyBzbWFsbCBkZXRhaWwgKGhhdCkKICBjdHguZmlsbFN0eWxlID0gJyMyMjInOwogIGN0eC5maWxsUmVjdChwLnggKyBwLncqMC4wOCwgcC55ICsgcC5oKjAuMDIsIHAudyowLjg0LCBwLmgqMC4xNCk7CiAgY3R4LnJlc3RvcmUoKTsKfQoKZnVuY3Rpb24gZHJhd09ic3RhY2xlcygpewogIGZvcihjb25zdCBvIG9mIHN0YXRlLm9ic3RhY2xlcyl7CiAgICBjdHguc2F2ZSgpOwogICAgY3R4LmZpbGxTdHlsZSA9ICcjMmQyZDJkJzsKICAgIHJvdW5kUmVjdChjdHgsIG8ueCwgby55LCBvLncsIG8uaCwgNiwgdHJ1ZSwgZmFsc2UpOwogICAgLy8gaGlnaGxpZ2h0CiAgICBjdHguZmlsbFN0eWxlID0gJyM0NDQnOwogICAgY3R4LmZpbGxSZWN0KG8ueCArIDYsIG8ueSArIDYsIG8udyAtIDEyLCBvLmggLSAxMik7CiAgICBjdHgucmVzdG9yZSgpOwogIH0KfQoKZnVuY3Rpb24gZHJhd0NvaW5zKCl7CiAgZm9yKGNvbnN0IGMgb2Ygc3RhdGUuY29pbnMpewogICAgY3R4LnNhdmUoKTsKICAgIC8vIGNvaW4gc2hhZG93CiAgICBjdHguZmlsbFN0eWxlID0gJ3JnYmEoMCwwLDAsMC4xNSknOwogICAgY3R4LmJlZ2luUGF0aCgpOwogICAgY3R4LmVsbGlwc2UoYy54ICsgYy53LzIsIGMueSArIGMuaCArIDgsIGMudy8yLCA2LCAwLCAwLCBNYXRoLlBJKjIpOwogICAgY3R4LmZpbGwoKTsKCiAgICAvLyBjb2luIGNpcmNsZQogICAgY3R4LmZpbGxTdHlsZSA9ICcjZjVkMDQyJzsKICAgIGN0eC5iZWdpblBhdGgoKTsKICAgIGN0eC5lbGxpcHNlKGMueCArIGMudy8yLCBjLnkgKyBjLmgvMiwgYy53LzIsIGMuaC8yLCAwLCAwLCBNYXRoLlBJKjIpOwogICAgY3R4LmZpbGwoKTsKCiAgICAvLyBpbm5lciBzaGluZQogICAgY3R4LmZpbGxTdHlsZSA9ICdyZ2JhKDI1NSwyNTUsMjU1LDAuNiknOwogICAgY3R4LmZpbGxSZWN0KGMueCArIGMudyowLjU1LCBjLnkgKyBjLmgqMC4yMiwgYy53KjAuMTIsIGMuaCowLjEyKTsKICAgIGN0eC5yZXN0b3JlKCk7CiAgfQp9CgpmdW5jdGlvbiBkcmF3SFVEKCl7CiAgLy8gbm90aGluZyBleHRyYSAoSFRNTCBoYW5kbGVzKQp9CgpmdW5jdGlvbiBkcmF3KCkgewogIC8vIGNsZWFyIGxvZ2ljYWwgY2FudmFzCiAgY3R4LmNsZWFyUmVjdCgwLDAsY29uZmlnLmNhbnZhc1dpZHRoLCBjb25maWcuY2FudmFzSGVpZ2h0KTsKCiAgLy8gQmFja2dyb3VuZCBza3kgc3VidGxlCiAgLy8gZ3JvdW5kICsgbGFuZXMKICBkcmF3QmFja2dyb3VuZCgpOwoKICAvLyBjb2lucwogIGRyYXdDb2lucygpOwoKICAvLyBvYnN0YWNsZXMKICBkcmF3T2JzdGFjbGVzKCk7CgogIC8vIHBsYXllcgogIGRyYXdQbGF5ZXIoKTsKfQoKLyogaGVscGVyIGZvciByb3VuZGVkIHJlY3QgKi8KZnVuY3Rpb24gcm91bmRSZWN0KGN0eCwgeCwgeSwgdywgaCwgciwgZmlsbCwgc3Ryb2tlKSB7CiAgaWYgKHR5cGVvZiByID09PSAndW5kZWZpbmVkJykgciA9IDU7CiAgY3R4LmJlZ2luUGF0aCgpOwogIGN0eC5tb3ZlVG8oeCArIHIsIHkpOwogIGN0eC5hcmNUbyh4ICsgdywgeSwgeCArIHcsIHkgKyBoLCByKTsKICBjdHguYXJjVG8oeCArIHcsIHkgKyBoLCB4LCB5ICsgaCwgcik7CiAgY3R4LmFyY1RvKHgsIHkgKyBoLCB4LCB5LCByKTsKICBjdHguYXJjVG8oeCwgeSwgeCArIHcsIHksIHIpOwogIGN0eC5jbG9zZVBhdGgoKTsKICBpZiAoZmlsbCkgY3R4LmZpbGwoKTsKICBpZiAoc3Ryb2tlKSBjdHguc3Ryb2tlKCk7Cn0KCi8qIC0tLSBTdGFydCAvIFJlc3RhcnQgVUkgLS0tICovCnJlc3RhcnRCdG4uYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKT0mZ3Q7ewogIHJlc2V0R2FtZSgpOwogIHJlcXVlc3RBbmltYXRpb25GcmFtZSh1cGRhdGUpOwp9KTsKCi8qIC0tLSBJbml0aWFsaXplICZhbXA7IGF1dG8tc3RhcnQgLS0tICovCihmdW5jdGlvbiBpbml0KCl7CiAgLy8gc2NhbGUgZHJhd2luZyBmb3IgY3Jpc3BuZXNzIG9uIGhpZ2gtRFBJIHNjcmVlbnMKICBjb25zdCByYXRpbyA9IHdpbmRvdy5kZXZpY2VQaXhlbFJhdGlvIHx8IDE7CiAgY2FudmFzLndpZHRoID0gY29uZmlnLmNhbnZhc1dpZHRoICogcmF0aW87CiAgY2FudmFzLmhlaWdodCA9IGNvbmZpZy5jYW52YXNIZWlnaHQgKiByYXRpbzsKICBjYW52YXMuc3R5bGUud2lkdGggPSBjYW52YXMuc3R5bGUud2lkdGggfHwgKGNvbmZpZy5jYW52YXNXaWR0aCArICdweCcpOwogIGNhbnZhcy5zdHlsZS5oZWlnaHQgPSBjYW52YXMuc3R5bGUuaGVpZ2h0IHx8IChjb25maWcuY2FudmFzSGVpZ2h0ICsgJ3B4Jyk7CiAgY3R4LnNldFRyYW5zZm9ybShyYXRpbywwLDAscmF0aW8sMCwwKTsgLy8ga2VlcCBsb2dpY2FsIGNvb3JkcwoKICAvLyBncm91bmQgeQogIHN0YXRlLmdyb3VuZFkgPSBjb25maWcuY2FudmFzSGVpZ2h0IC0gY29uZmlnLmdyb3VuZEhlaWdodDsKCiAgaW5pdExhbmVzKCk7CiAgc3RhdGUucGxheWVyID0gY3JlYXRlUGxheWVyKCk7CiAgc3RhdGUucnVubmluZyA9IHRydWU7CiAgbGFzdEZyYW1lVGltZSA9IHBlcmZvcm1hbmNlLm5vdygpOwogIHJlcXVlc3RBbmltYXRpb25GcmFtZSh1cGRhdGUpOwp9KSgpOwombHQ7L3NjcmlwdCZndDsKJmx0Oy9ib2R5Jmd0OwombHQ7L2h0bWwmZ3Q7
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Endless Runner — Demo</title>
<style>
:root {
--bg:#87CEEB;
--ground:#4b3b2f;
--ui:#ffffffcc;
}
html,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial;}
#gameWrap{display:flex;align-items:center;justify-content:center;height:100%;background:linear-gradient(#7ec8ff 0%, var(--bg) 70%);overflow:hidden}
canvas{background:transparent;display:block;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.25)}
#ui {
position: absolute; top:18px; left:18px; color:#111; font-weight:600;
background:var(--ui); padding:.5rem .8rem; border-radius:8px;
box-shadow:0 6px 16px rgba(0,0,0,.12);
}
#tip { position:absolute; bottom:18px; left:18px; color:#111; background:var(--ui); padding:.45rem .7rem;border-radius:8px;}
#overlay {
position:absolute; inset:0; display:flex; align-items:center; justify-content:center; pointer-events:none;
}
.centerBox{pointer-events:auto; background:#fff; padding:18px 22px; border-radius:12px; box-shadow:0 8px 30px rgba(0,0,0,.25); text-align:center;}
button{padding:10px 14px;border-radius:8px;border:0;background:#0a84ff;color:#fff;font-weight:700;cursor:pointer}
small{display:block;opacity:.8;margin-top:6px}
</style>
</head>
<body>
<div id="gameWrap">
<canvas id="game" width="720" height="1280"></canvas>
<div id="ui">Score: <span id="score">0</span> Speed: <span id="speed">1</span>x</div>
<div id="tip">Controls: ← → to change lanes, ↑ or Space to jump. Swipe on mobile.</div>
<div id="overlay" style="display:none;">
<div class="centerBox">
<h2 id="msgTitle">Game Over</h2>
<p id="msgText">Your score: <span id="finalScore">0</span></p>
<div style="margin-top:10px"><button id="restartBtn">Restart</button></div>
<small>Tip: try to collect coins and dodge obstacles. Speed increases over time.</small>
</div>
</div>
</div>
<script>
/* --- Config --- */
const config = {
canvasWidth: 540, // logical canvas width (we scale to CSS size)
canvasHeight: 960, // logical canvas height
lanes: 3,
laneWidthPct: 0.25, // visual lane width relative to canvas width
groundHeight: 140,
initialSpeed: 4, // world scroll speed
speedIncreaseEvery: 3000, // ms
speedIncreaseAmount: 0.3,
jumpVelocity: -18,
gravity: 1,
coinSpawnInterval: 900,
obstacleSpawnInterval: 1000,
playerWidth: 64,
playerHeight: 96,
};
/* --- Setup canvas & scaling for retina --- */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const speedEl = document.getElementById('speed');
const overlay = document.getElementById('overlay');
const finalScoreEl = document.getElementById('finalScore');
const restartBtn = document.getElementById('restartBtn');
function resizeCanvas() {
// maintain aspect ratio using logical canvas size, fit within window
const wrap = document.getElementById('gameWrap');
const maxW = Math.min(window.innerWidth - 40, 420);
const scale = maxW / config.canvasWidth;
canvas.style.width = (config.canvasWidth * scale) + 'px';
canvas.style.height = (config.canvasHeight * scale) + 'px';
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
/* --- Game state --- */
let state = {
running: false,
score: 0,
speed: config.initialSpeed,
lastSpeedUp: performance.now(),
player: null,
obstacles: [],
coins: [],
lastCoinAt: 0,
lastObstacleAt: 0,
lastFrame: 0,
lanesX: [],
groundY: config.canvasHeight - config.groundHeight,
};
/* --- Utility functions --- */
function randRange(a,b){return Math.random()*(b-a)+a}
function rectsCollide(a,b){
return !(a.x + a.w < b.x || a.x > b.x + b.w || a.y + a.h < b.y || a.y > b.y + b.h);
}
/* --- Player object --- */
function createPlayer() {
const lanePositions = state.lanesX;
return {
lane: 1, // 0..lanes-1, start center lane
x: lanePositions[1] - config.playerWidth/2,
y: state.groundY - config.playerHeight,
vx: 0,
vy: 0,
w: config.playerWidth,
h: config.playerHeight,
isJumping: false,
targetX: null, // for smooth lane moves
moveProgress: 0
};
}
/* --- Init lanes positions --- */
function initLanes() {
const W = config.canvasWidth;
// use three lanes centered in middle of canvas horizontally
const centerX = W/2;
const spacing = Math.min(120, W * 0.22);
const positions = [];
const off = spacing;
// lanes centered: left, center, right
positions.push(centerX - off);
positions.push(centerX);
positions.push(centerX + off);
state.lanesX = positions;
}
/* --- Reset / Start / Game Over --- */
function resetGame() {
initLanes();
state.score = 0;
state.speed = config.initialSpeed;
state.lastSpeedUp = performance.now();
state.obstacles = [];
state.coins = [];
state.lastCoinAt = 0;
state.lastObstacleAt = 0;
state.player = createPlayer();
state.running = true;
overlay.style.display = 'none';
scoreEl.textContent = '0';
speedEl.textContent = state.speed.toFixed(1);
lastFrameTime = performance.now();
}
function gameOver() {
state.running = false;
finalScoreEl.textContent = Math.floor(state.score);
document.getElementById('msgTitle').textContent = 'Game Over';
overlay.style.display = 'flex';
}
/* --- Spawning --- */
function spawnObstacle() {
// obstacles appear in random lanes with variable size
const lane = Math.floor(Math.random() * config.lanes);
const x = state.lanesX[lane] - 40; // obstacle centered on lane
const w = 56 + Math.floor(Math.random()*30);
const h = 56 + Math.floor(Math.random()*36);
const y = state.groundY - h;
state.obstacles.push({x: x, y: y, w: w, h: h, lane});
}
function spawnCoin() {
const lane = Math.floor(Math.random() * config.lanes);
const x = state.lanesX[lane] - 20;
const y = state.groundY - config.playerHeight - randRange(20, 120);
state.coins.push({x: x, y: y, w: 36, h: 36, lane});
}
/* --- Input handling --- */
let touchStart = null;
function handleKey(e){
if(!state.running) return;
if(e.key === "ArrowLeft") changeLane(-1);
if(e.key === "ArrowRight") changeLane(1);
if(e.key === "ArrowUp" || e.code === "Space") jump();
}
window.addEventListener('keydown', handleKey);
function changeLane(dir){
const p = state.player;
const newLane = Math.max(0, Math.min(config.lanes-1, p.lane + dir));
if(newLane === p.lane) return;
p.lane = newLane;
p.targetX = state.lanesX[newLane] - p.w/2;
p.moveProgress = 0;
}
function jump(){
const p = state.player;
if(!p.isJumping){
p.vy = config.jumpVelocity;
p.isJumping = true;
}
}
/* Touch / swipe support */
canvas.addEventListener('touchstart', (ev)=>{
if(!state.running) return;
touchStart = ev.touches[0];
}, {passive:true});
canvas.addEventListener('touchend', (ev)=>{
if(!state.running) return;
if(!touchStart) return;
const end = ev.changedTouches[0];
const dx = end.clientX - touchStart.clientX;
const dy = end.clientY - touchStart.clientY;
const adx = Math.abs(dx), ady = Math.abs(dy);
if(adx > 40 && adx > ady) {
if(dx > 0) changeLane(1); else changeLane(-1);
} else if(ady > 40 && ady > adx) {
if(dy < 0) jump();
} else {
// tap: left or right half
const rect = canvas.getBoundingClientRect();
const cx = (touchStart.clientX - rect.left) / rect.width * config.canvasWidth;
if(cx < config.canvasWidth/2) changeLane(-1); else changeLane(1);
}
touchStart = null;
}, {passive:true});
/* --- Game Loop --- */
let lastFrameTime = performance.now();
function update(now) {
if(!state.running) {
return;
}
const dt = Math.min(40, now - lastFrameTime);
lastFrameTime = now;
// speed increase over time
if(now - state.lastSpeedUp > config.speedIncreaseEvery){
state.speed += config.speedIncreaseAmount;
state.lastSpeedUp = now;
speedEl.textContent = state.speed.toFixed(1);
}
// spawn coins & obstacles
if(now - state.lastCoinAt > config.coinSpawnInterval){
spawnCoin();
state.lastCoinAt = now;
}
if(now - state.lastObstacleAt > config.obstacleSpawnInterval){
spawnObstacle();
state.lastObstacleAt = now;
}
// move obstacles & coins toward player (simulate forward movement)
const scroll = state.speed * (dt/16);
for(const o of state.obstacles) {
o.y += scroll * 0.0; // obstacles fixed on ground in our view; we'll simulate forward by decreasing x
o.x -= scroll * 6; // move left as if player is running forward
}
for(const c of state.coins) {
c.x -= scroll * 6;
}
// remove offscreen
state.obstacles = state.obstacles.filter(o => o.x + o.w > -50);
state.coins = state.coins.filter(c => c.x + c.w > -50);
// player movement: lane smoothing
const p = state.player;
if(p.targetX !== null){
p.moveProgress += 0.18 * (dt/16);
p.x = p.x + (p.targetX - p.x) * Math.min(1, p.moveProgress);
if(Math.abs(p.x - p.targetX) < 1) { p.x = p.targetX; p.targetX = null; p.moveProgress = 0; }
} else {
// ensure player x tied to lane
p.x = state.lanesX[p.lane] - p.w/2;
}
// gravity & jump physics
if(p.isJumping){
p.vy += config.gravity * (dt/16);
p.y += p.vy * (dt/16);
if(p.y >= state.groundY - p.h){
p.y = state.groundY - p.h;
p.vy = 0;
p.isJumping = false;
}
}
// collisions: obstacles
const playerRect = {x:p.x, y:p.y, w:p.w, h:p.h};
// obstacles
for(const o of state.obstacles){
const oRect = {x:o.x, y:o.y, w:o.w, h:o.h};
if(rectsCollide(playerRect, oRect)){
// simple rule: collision if player low enough
if(p.y + p.h - 10 > o.y){
gameOver();
return;
}
}
}
// coins
for(let i = state.coins.length -1; i>=0; i--){
const c = state.coins[i];
if(rectsCollide(playerRect, {x:c.x,y:c.y,w:c.w,h:c.h})){
state.coins.splice(i,1);
state.score += 50;
}
}
// scoring by distance
state.score += state.speed * (dt/1000) * 10;
scoreEl.textContent = Math.floor(state.score);
// draw
draw();
requestAnimationFrame(update);
}
/* --- Rendering --- */
function drawBackground(){
const W = config.canvasWidth, H = config.canvasHeight;
// sky gradient already in CSS; we'll draw distant details
// draw ground
ctx.fillStyle = '#3f2f25';
ctx.fillRect(0, state.groundY, W, H - state.groundY);
// draw lanes lines
ctx.strokeStyle = '#ffffff22';
ctx.lineWidth = 2;
for(let i=0;i<config.lanes;i++){
const x = state.lanesX[i];
// brief dashed marker ahead
const segY = state.groundY - 20;
ctx.beginPath();
for(let t=0;t<10;t++){
ctx.moveTo(x + (i-1)*6, segY - t*30);
ctx.lineTo(x + (i-1)*6 + 8, segY - t*30 - 12);
}
ctx.stroke();
}
}
function drawPlayer(){
const p = state.player;
// simple rounded rectangle body
ctx.save();
// shadow
ctx.fillStyle = 'rgba(0,0,0,0.18)';
ctx.beginPath();
ctx.ellipse(p.x + p.w/2, state.groundY + 8, p.w/2, 10, 0, 0, Math.PI*2);
ctx.fill();
// body
ctx.fillStyle = '#ff5a5f';
roundRect(ctx, p.x, p.y, p.w, p.h, 10, true, false);
// face / eyes
ctx.fillStyle = '#fff';
ctx.fillRect(p.x + p.w*0.2, p.y + p.h*0.25, p.w*0.22, p.h*0.18);
ctx.fillRect(p.x + p.w*0.58, p.y + p.h*0.25, p.w*0.22, p.h*0.18);
// small detail (hat)
ctx.fillStyle = '#222';
ctx.fillRect(p.x + p.w*0.08, p.y + p.h*0.02, p.w*0.84, p.h*0.14);
ctx.restore();
}
function drawObstacles(){
for(const o of state.obstacles){
ctx.save();
ctx.fillStyle = '#2d2d2d';
roundRect(ctx, o.x, o.y, o.w, o.h, 6, true, false);
// highlight
ctx.fillStyle = '#444';
ctx.fillRect(o.x + 6, o.y + 6, o.w - 12, o.h - 12);
ctx.restore();
}
}
function drawCoins(){
for(const c of state.coins){
ctx.save();
// coin shadow
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.beginPath();
ctx.ellipse(c.x + c.w/2, c.y + c.h + 8, c.w/2, 6, 0, 0, Math.PI*2);
ctx.fill();
// coin circle
ctx.fillStyle = '#f5d042';
ctx.beginPath();
ctx.ellipse(c.x + c.w/2, c.y + c.h/2, c.w/2, c.h/2, 0, 0, Math.PI*2);
ctx.fill();
// inner shine
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.fillRect(c.x + c.w*0.55, c.y + c.h*0.22, c.w*0.12, c.h*0.12);
ctx.restore();
}
}
function drawHUD(){
// nothing extra (HTML handles)
}
function draw() {
// clear logical canvas
ctx.clearRect(0,0,config.canvasWidth, config.canvasHeight);
// Background sky subtle
// ground + lanes
drawBackground();
// coins
drawCoins();
// obstacles
drawObstacles();
// player
drawPlayer();
}
/* helper for rounded rect */
function roundRect(ctx, x, y, w, h, r, fill, stroke) {
if (typeof r === 'undefined') r = 5;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
if (fill) ctx.fill();
if (stroke) ctx.stroke();
}
/* --- Start / Restart UI --- */
restartBtn.addEventListener('click', ()=>{
resetGame();
requestAnimationFrame(update);
});
/* --- Initialize & auto-start --- */
(function init(){
// scale drawing for crispness on high-DPI screens
const ratio = window.devicePixelRatio || 1;
canvas.width = config.canvasWidth * ratio;
canvas.height = config.canvasHeight * ratio;
canvas.style.width = canvas.style.width || (config.canvasWidth + 'px');
canvas.style.height = canvas.style.height || (config.canvasHeight + 'px');
ctx.setTransform(ratio,0,0,ratio,0,0); // keep logical coords
// ground y
state.groundY = config.canvasHeight - config.groundHeight;
initLanes();
state.player = createPlayer();
state.running = true;
lastFrameTime = performance.now();
requestAnimationFrame(update);
})();
</script>
</body>
</html>