Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
411 lines (388 sloc) 16 KB
<!DOCTYPE html>
<html lang="en">
<head><title>javascript game of tron in 219 bytes</title>
<meta property="fb:admins" content="536181839"/>
<meta name="author" content="Alok Menghrajani"/>
<link href="../bootstrap.min.css" rel="stylesheet"/>
<script type="text/javascript" src="../jquery-1.7.1.min.js"></script>
<style>
body { margin-top: 40px; }
section { margin-top: 80px; }
code { white-space:nowrap; color: black; }
</style>
</head>
<body>
<div class="container">
<div class="hero-unit">
<h1>219 bytes tron</h1>
<p>
With some coworkers, we challenged each other to write the smallest
possible game of tron in javascript (an exercice known
as javascript golfing).
</p>
<p>
This page explains our final version (219 bytes). We initially worked alone but then exchanged ideas
and tricks, so erling & mathewsb deserve most of the credits!
</p>
<p>
our code was originally 226 bytes, but "Cosmologicon" pointed out a way to save three whole bytes, bringing us
to 223 bytes.
</p>
<p>
With p01, we then came up with a way to save another 11 bytes (making the game 212 bytes).
He also suggested keeping track of score, which takes 9 bytes but is totally worth it!
</p>
<p>
skrounge found a way to save 2 more bytes, brining the game to 219 bytes.
</p>
<div style="position:relative; top:60px;">
<iframe src="//www.facebook.com/plugins/like.php?href=http%3A%2F%2Falokmenghrajani.github.com%2Ftron&amp;send=false&amp;layout=standard&amp;width=800&amp;show_faces=true&amp;action=like&amp;colorscheme=light&amp;font&amp;height=80&amp;appId=202717489767277" scrolling="no" frameborder="0" style="border:none; overflow:hidden; width:800px; height:80px;" allowTransparency="true"></iframe>
</div>
</div>
<section id="rules">
<div class="page-header"><h1>rules</h1></div>
<div>We started with some basic rules:
<ol>
<li>the tron must start in the center, facing any direction.</li>
<li>the game controls must be 'i', 'j', 'k', 'l'.</li>
<li>when the tron hits it's trail or an edge, "game over" must be shown to the user.</li>
<li>the code need only run on Chrome 17.</li>
</ol>
</div>
<p>
Unlike some js size optimization competitions, we did not use a shim. We took into account the entire page's size. This
forced us to explore novel tricks.
</p>
<p>
My coworkers represented the board as ascii-art (which resulted in shorter code but an unplayable game).
I preferred to keep the game nicer and use a &lt;canvas&gt; tag.
</p>
</section>
<section id="screenshot">
<div class="page-header"><h1>screenshot</h1></div>
<div class="thumbnail" style="width: 150px; height: 150px"><img src="tron.png"></div>
</section>
<section id="code">
<div class="page-header"><h1>code</h1></div>
<div class="slide" id="slide1">
<pre>
&lt;body id=b onkeyup=e=event onload=
z=c.getContext('2d');
z.fillRect(s=0,0,n=150,x=11325);
setInterval("
0&lt;x%n
&x&lt;n*n
&(z[x+=[1,-n,-1,n][e.which&3]]^=1)
?z.clearRect(x%n,x/n,1,1,s++)
:b.innerHTML='game&#x2B1C;over:'+s
",9)
&gt;&lt;canvas id=c&gt;
</pre>
<p>The entire code, with only a few extra newlines for readibility.</p>
</div>
<div class="slide" id="slide2">
<pre>
<font color="red">&lt;body id=b</font> onkeyup=e=event onload=
z=c.getContext('2d');
z.fillRect(s=0,0,n=150,x=11325);
setInterval("
0&lt;x%n
&x&lt;n*n
&(z[x+=[1,-n,-1,n][e.which&3]]^=1)
?z.clearRect(x%n,x/n,1,1,s++)
:b.innerHTML='game&#x2B1C;over:'+s
",9)
&gt;&lt;canvas id=c&gt;
</pre>
<p>By assigning an <code>id</code> to the body, we are going to be able to write
<code>b.innerHTML="game over"</code>, instead
of having to write <code>document.body.innerHTML="game over"</code>*. Saves us 7 bytes.</p>
<p>We can also drop the quotes around the attribute, since browsers are able to fix that for us.</p>
<p>* In Firefox, this only works in Quirks Mode (which is enabled since there isn't a doctype in the demo).</p>
</div>
<div class="slide" id="slide3">
<pre>
&lt;body id=b <font color="red">onkeyup=e=event</font> onload=
z=c.getContext('2d');
z.fillRect(s=0,0,n=150,x=11325);
setInterval("
0&lt;x%n
&x&lt;n*n
&(z[x+=[1,-n,-1,n][e.which&3]]^=1)
?z.clearRect(x%n,x/n,1,1,s++)
:b.innerHTML='game&#x2B1C;over:'+s
",9)
&gt;&lt;canvas id=c&gt;
</pre>
<p>We use <code>onkeyup</code> instead of <code>onkeydown</code>, since it saves 2 bytes. The game
feels a little different, but who cares?</p>
<p><code>e</code> is going to contain the keyboard event. Implies the game is going to spew js errors until the
first key is released. It also means the game doesn't start until you press the first key. This
can be improved, but requires 2 extra bytes.</p>
</div>
<div class="slide" id="slide4">
<pre>
&lt;body id=b onkeyup=e=event <font color="red">onload=
z=c.getContext('2d');
z.fillRect(s=0,0,n=150,x=11325);
setInterval("
0&lt;x%n
&x&lt;n*n
&(z[x+=[1,-n,-1,n][e.which&3]]^=1)
?z.clearRect(x%n,x/n,1,1,s++)
:b.innerHTML='game&#x2B1C;over:'+s
",9)
</font>&gt;&lt;canvas id=c&gt;
</pre>
<p>We put our main code in a <code>onload=</code>, since it saves us the &lt;script&gt; and &lt;/script&gt; tags
(saves 10 bytes). We are not going to use quotes, so there is a set of characters we can't use in the code (as
it would end the attribute). The set of forbidden characters includes &gt;, space, tab.</p>
<p>Note: the <code>&lt;script&gt;</code> tag always requires a closing tag.</p>
</div>
<div class="slide" id="slide5">
<pre>
&lt;body id=b onkeyup=e=event onload=
<font color="red">z=c.getContext('2d');</font>
z.fillRect(s=0,0,n=150,x=11325);
setInterval("
0&lt;x%n
&x&lt;n*n
&(z[x+=[1,-n,-1,n][e.which&3]]^=1)
?z.clearRect(x%n,x/n,1,1,s++)
:b.innerHTML='game&#x2B1C;over:'+s
",9)
&gt;&lt;canvas id=c&gt;
</pre>
<p><code>z</code> is going to contain the context for the canvas. There is no way to avoid this expensive expression. We are however also going
to use <code>z</code> to store our grid to detect collisions. JavaScript lets you access properties on objects using the array [] syntax, and
everything works out as long as you don't need to call array functions (i.e. we can't do <code>z.join(&hellip;);</code>).</p>
<p>Reusing <code>z</code> to store our grid saves 2 to 5 bytes, depending on how things are done. The grid is going to be a single dimension, n*n grid.</p>
<p>Note: we tried using the canvas to detect collisions. Accessing the pixel values of a canvas
turned out to require lots of bytes.</p>
</div>
<div class="slide" id="slide6">
<pre>
&lt;body id=b onkeyup=e=event onload=
z=c.getContext('2d');
<font color="red">z.fillRect(s=0,0,n=150,x=11325);</font>
setInterval("
0&lt;x%n
&x&lt;n*n
&(z[x+=[1,-n,-1,n][e.which&3]]^=1)
?z.clearRect(x%n,x/n,1,1,s++)
:b.innerHTML='game&#x2B1C;over:'+s
",9)
&gt;&lt;canvas id=c&gt;
</pre>
<p>Draws the black background.</p>
<p>By default, an empty canvas has a black foreground color, white background color and is 300x150 pixels wide.</p>
<p>We also initialize 3 variables, <code>n</code> is set to 150 (which is going to be our arena size).</p>
<p><code>x</code> is going to be the tron's position, and is set to 11325 (11325=75*75+75=center of the grid).</p>
<p><code>s</code> is going to keep track of the score and is set to 0.</p>
<p>We are effectively drawing a 150x11325 box, but the game is going to look &amp; feel 150x150.</p>
<p>Simultaneously calling a function
and setting a variable is a very common trick in js golf and saves lots of bytes.</p>
</div>
<div class="slide" id="slide7">
<pre>
&lt;body id=b onkeyup=e=event onload=
z=c.getContext('2d');
z.fillRect(s=0,0,n=150,x=11325);
<font color="red">setInterval("
0&lt;x%n
&x&lt;n*n
&(z[x+=[1,-n,-1,n][e.which&3]]^=1)
?z.clearRect(x%n,x/n,1,1,s++)
:b.innerHTML='game&#x2B1C;over:'+s
",9)</font>
&gt;&lt;canvas id=c&gt;
</pre>
<p><code>setInterval</code> causes our code to get called every 9ms. The code inside <code>setInterval</code> is the main game loop. It updates the
tron's position and detects collisions.</p>
<p>The code is written as a string, as it's shorter than writing <code>function(){&hellip;}</code> (saves 10 bytes). This implies
the main loop cannot use the &#34;, &gt;, space and tab characters.</p>
</div>
<div class="slide" id="slide8">
<pre>
&lt;body id=b onkeyup=e=event onload=
z=c.getContext('2d');
z.fillRect(s=0,0,n=150,x=11325);
setInterval("
<font color="red">0&lt;x%n
&x&lt;n*n</font>
&(z[x+=[1,-n,-1,n][e.which&3]]^=1)
?z.clearRect(x%n,x/n,1,1,s++)
:b.innerHTML='game&#x2B1C;over:'+s
",9)
&gt;&lt;canvas id=c&gt;
</pre>
<p>Checks if we are within the game boundary. We can't use the <code>&gt;</code> operator (we are inside an attribute),
but <code>&lt;</code> works.</p>
<ul>
<li>
<p>If the tron hits the left edge, <code>x%n</code> will return <code>0</code>.</p>
</li>
<li>
<p>If the tron hits the right edge, <code>x</code> will wrap around and <code>x%n</code> will return <code>0</code>.</p>
</li>
<li>
<p>If the tron hits the top edge, <code>x</code> will become negative and <code>x%n</code> will return a negative value.</p>
</li>
<li>
<p>If the tron hits the bottom edge, <code>x</code> will be larger than <code>n*n</code>.</p>
</li>
</ul>
<p>skrounge pointed out that we can use the <code>&</code> (binary and) instead of <code>&&</code> (logical and)
since both sides of the operator are boolean values.</p>
</div>
<div class="slide" id="slide9">
<pre>
&lt;body id=b onkeyup=e=event onload=
z=c.getContext('2d');
z.fillRect(s=0,0,n=150,x=11325);
setInterval("
0&lt;x%n
&x&lt;n*n
&(z[<font color="red">x+=[1,-n,-1,n][e.which&3]</font>]^=1)
?z.clearRect(x%n,x/n,1,1,s++)
:b.innerHTML='game&#x2B1C;over:'+s
",9)
&gt;&lt;canvas id=c&gt;
</pre>
<p>Updates the variable <code>x</code> based on the value of <code>e</code> (remember, onkeyup saves the event in the <code>e</code>
variable).</p>
<p>The grid is a single dimension, going up or down requires us to add or subtract <code>n</code> units.</p>
<p>Besides being places in a cross shape on the keyboard, 'i', 'j', 'k', 'l' are also consecutive letters. We convert the key code
into an index by simply doing <code&3</code> (notice how <code>e.which</code> is shorter than <code>e.keyCode</code> by 2 bytes).</p>
<p>Note: hitting any other key will also move the tron in various directions. Filtering all other keys requires 4 more bytes.</p>
</div>
<div class="slide" id="slide10">
<pre>
&lt;body id=b onkeyup=e=event onload=
z=c.getContext('2d');
z.fillRect(s=0,0,n=150,x=11325);
setInterval("
0&lt;x%n
&x&lt;n*n
&<font color="red">(z[</font>x+=[1,-n,-1,n][e.which&3]<font color="red">]^=1)</font>
?z.clearRect(x%n,x/n,1,1,s++)
:b.innerHTML='game&#x2B1C;over:'+s
",9)
&gt;&lt;canvas id=c&gt;
</pre>
<p>This expression is one of the nicest parts of the code. There's a lot going on here:</p>
<ul>
<li>
<p>The grid is never initialized, so it starts out with all the values set to <code>undefined</code>.</p>
</li>
<li>
<p>The <code>^=</code> (bitwise xor assignment) operator lets us update the grid while checking for a collision:</p>
</li>
<li>
<p>If the value in the grid has never been visited, we set the grid value to <code>1</code> and return <code>1</code>.</p>
</li>
<li>
<p>If the value in the grid has been visited, we return <code>0</code>.</p>
</li>
</ul>
</div>
<div class="slide" id="slide11">
<pre>
&lt;body id=b onkeyup=e=event onload=
z=c.getContext('2d');
z.fillRect(s=0,0,n=150,x=11325);
setInterval("
0&lt;x%n
&x&lt;n*n
&(z[x+=[1,-n,-1,n][e.which&3]]^=1)
<font color="red">?z.clearRect(x%n,x/n,1,1,s++)</font>
:b.innerHTML='game&#x2B1C;over:'+s
",9)
&gt;&lt;canvas id=c&gt;
</pre>
<p>Using the ternary <code>?:</code> operator, we draw a white pixel at the tron's position when the game has not ended.
Since our grid and <code>x</code> are a single dimension, we need to use <code>/</code>
and <code>%</code> operators to get each coordinate's value.</p>
<p>
We also increment the score. Another common trick to save bytes is to add extra parameters to function calls.
</p>
</div>
<div class="slide" id="slide12">
<pre>
&lt;body id=b onkeyup=e=event onload=
z=c.getContext('2d');
z.fillRect(s=0,0,n=150,x=11325);
setInterval("
0&lt;x%n
&x&lt;n*n
&(z[x+=[1,-n,-1,n][e.which&3]]^=1)
?z.clearRect(x%n,x/n,1,1,s++)
<font color="red">:b.innerHTML='game&#x2B1C;over:'+s</font>
",9)
&gt;&lt;canvas id=c&gt;
</pre>
<p>We replace the page's content with "game over" and the score when a collision occurs.</p>
<p>Since we are inside an attribute we cannot use a space character. We instead use U+00A0 (non breaking space) and shown here as &#x2B1C;. In
iso88591 this only takes one byte. The browser auto detects iso88591 if we don't serve the page with another content type.</p>
<p>Note: the game still exists in memory and is still running.</p>
</div>
<div class="slide" id="slide13">
<pre>
&lt;body id=b onkeyup=e=event onload=
z=c.getContext('2d');
z.fillRect(s=0,0,n=150,x=11325);
setInterval("
0&lt;x%n
&x&lt;n*n
&(z[x+=[1,-n,-1,n][e.which&3]]^=1)
?z.clearRect(x%n,x/n,1,1,s++)
:b.innerHTML='game&#x2B1C;over:'+s
",9)
<font color="red">&gt;&lt;canvas id=c&gt;</font>
</pre>
<p>Defines the canvas. We use the same trick of setting an <code>id</code> on canvas, which reappears in the global space.</p>
<p>note: we removed any unnecessary tags, like <code>&lt;html&gt;</code>, <code>&lt;head&gt;</code>, <code>&lt;/head&gt;</code>,
<code>&lt;/canvas&gt;</code>, <code>&lt;/body&gt;</code>, <code>&lt;/head&gt;</code> and <code>&lt;/html&gt;</code>.</p>
</div>
<div class="btn-toolbar">
<div class="btn-group">
<button class="btn" id="btn1" onclick="slide(1)">1</button>
<button class="btn" id="btn2" onclick="slide(2)">2</button>
<button class="btn" id="btn3" onclick="slide(3)">3</button>
<button class="btn" id="btn4" onclick="slide(4)">4</button>
<button class="btn" id="btn5" onclick="slide(5)">5</button>
<button class="btn" id="btn6" onclick="slide(6)">6</button>
<button class="btn" id="btn7" onclick="slide(7)">7</button>
<button class="btn" id="btn8" onclick="slide(8)">8</button>
<button class="btn" id="btn9" onclick="slide(9)">9</button>
<button class="btn" id="btn10" onclick="slide(10)">10</button>
<button class="btn" id="btn11" onclick="slide(11)">11</button>
<button class="btn" id="btn12" onclick="slide(12)">12</button>
<button class="btn" id="btn13" onclick="slide(13)">13</button>
</div>
</div>
</section>
<script>
function slide(n) {
$('.btn').removeClass("active");
$('.slide').hide();
$('#btn'+n).addClass("active");
$('#slide'+n).show();
}
var m = -1;
$('.slide').each(function(_,e){j=$(e);m=m>j.height()?m:j.height();});
$('.slide').height(m);
slide(1);
</script>
<section id="play">
<div class="page-header"><h1>play</h1></div>
<ul>
<li><a href="https://raw.github.com/alokmenghrajani/alokmenghrajani.github.com/master/tron/tron.html">view source (219 bytes)</a></li>
<li><a href="tron.html">play (press 'i', 'j', 'k' or 'l' to start)</a></li>
</ul>
</section>
<footer class="footer">
<p class="pull-right"><a href="#">Back to top</a></p>
</footer>
</body>
</html>