// We need an anonymous scope function because the keydown listener has to have // access to nextCodeChar, and we don't want to store it globally. We could also // store it on the handler function itself, but "arguments.callee" is awfully // long... (function() { var doc = document, addEventListener = 'addEventListener', charCodeAt = 'charCodeAt', keyCode = 'keyCode', keydown = 'keydown', nextCodeChar = 0, state; // 0|undefined=QUIT, 1=CLEARING, 2=LOST, 3=PLAYING doc[addEventListener](keydown, function(e) { nextCodeChar = e[keyCode] == "&&((%'%'BA"[charCodeAt](nextCodeChar) ? nextCodeChar + 1 : 0; if (!state && nextCodeChar > 9) { state = 3; (function() { var win = window, createElement = 'createElement', removeEventListener = 'removeEventListener', keyup = 'keyup', math = Math, baseBass = 'LSOSLSOSKSNSKSNS', music = [ // http://www.theoreticallycorrect.com/Helmholtz-Pitch-Numbering/ // // Lowest note in treble: 60 // Highest note in treble: 83 // // Subtract 32, then: // - % 24 gives MIDI note number minus 60 // - / 24 gives duration: eighth, dotted quarter, quarter, half // // Other way: // note number - 28 + 24 * [0:eighth, 1:dotted quarter, 2:quarter, 3:half] // // http://i.ytimg.com/vi/bpBePVCUM7E/maxresdefault.jpg // https://www.youtube.com/watch?v=IBkH5_gLF8Q '`+,^,+Y),`.,C,^`\\Yq^.1e31H,01.,C,^`\\Yq', 'T$$T,+)$),Y))<$TTYTl.).1^..D,\\.,<$TTTTT`', // All notes are eighths. // Subtract 64 to get MIDI note number - 33. 'GNKNGNKN' + baseBass + 'LSNSL@BCESOSESOSCOCOCOCOBNBN?J?J@CGL@@@@' ], thirdVerse = [ // Treble voices 'xtvstqpsxtvs\\`}|x', 'tqspqqpptqspY`xx\u007f', // Bass voice baseBass+baseBass+baseBass+baseBass ], // bits 0-3: fade-out speed (0 slowest, 15 fastest) // bits 4-9: period 2 // bits 10-11: period 1 sounds = [], // First 2 invisible lines, then 20 visible lines, then 2 for the Next display. grid = [], shadowGrid = [], w = 10, h = 22, s = w*h+20, // x I J L O S T Z colors = '08080890dd936f9e809dd090e09c0c9f22'.split(9), // http://tetris.wikia.com/wiki/SRS // // Base-64 encoded string of bytes, consisting of 8 bytes for each tetromino. // Each 8-byte block is 4 words of 2 bytes, each word representing a rotation: // // 7 6 5 4 <- first byte // 3 2 1 0 <- first byte // 7 6 5 4 <- second byte // 3 2 1 0 <- second byte shapes = atob('8ABERAAPIiJxACYCcAQiA3QAIgZwASMCZgBmAGYAZgA2AGIEYAMxAnIAYgJwAjICYwBkAjAGMgE'), // Wall kick tables: http://tetrisconcept.net/wiki/SRS#Wall_Kicks // // Each table is represented as 4 * 5 characters. // The first 4 are for orientation 0, etc., and the 5 characters are the // possible offsets for a clockwise rotation from that orientation. (For // counterclockwise, just negate the values.) // // Subtract 32, then: // - bits 0-2 give x offset plus 2 (range: 0..4) // - bits 3-5 give INVERTED y offset plus 2 (range: 0..4) // (inverted because the reference page above does that) // // y\x -2 -1 0 +1 +2 // -2 sp ! " # $ // -1 ( ) * + , // 0 0 1 2 3 4 // +1 8 9 : ; < // +2 @ A B C D wallKickTableI = '203(C214A,241', // 1pc = 16px html = divStyleMargin + '-14pc -10pc;position:fixed;width:20pc;left:50%;top:50%;font:12px Arial;background:rgba(0,0,0,.8);box-shadow:0 0 2pc #000;border-radius:1pc">' + divStyleMargin + '1pc 2pc;color:#888">' + 'Tis: Tetris clone in just 4 kB of JavaScript

' + 'Left/right: move | Up/Ctrl/Alt: rotate | Esc: quit
' + 'Down/space: soft/hard drop | M: music' + divEnd + divStyleMargin + '0 1pc;float:right;color:#eee;font-size:1pc">' + '
' + divEnd + 'Next' + divStyleMargin + '8px 0;width:4pc">' ; tmp2 = divStyleMargin + '0;width:1pc;height:1pc;float:left;box-shadow:-2px -2px 8px rgba(0,0,0,.4) inset,0 0 2px #000 inset" id="tis-'; for (i = 220; i < s; i++) { if (i % w < 4) { html += tmp2 + i + '">' + divEnd; } } html += divEnd + divEnd + divStyleMargin + '0 2pc 2pc;background:#000;width:10pc;height:20pc">'; for (i = 0; i < s; i++) { grid.push(0); if (i > 19 && i < 220) { html += tmp2 + i + '">' + divEnd; } } html += divEnd + divEnd; tmp = doc[createElement]('div'); tmp.innerHTML = html; doc.body.appendChild(html = tmp); // Music! // 4593 samples/eighth * 8 eighths/bar * 24 bars = 881856 samples tmp = new Uint8Array(881856); // delta is voice index; note that it is a string! for (delta in music) { music[delta] += music[delta] + thirdVerse[delta]; for (i = 0, j = 0; j < music[delta].length;) { tmp3 = music[delta][charCodeAt](j++) - (delta == 2 ? 64 : 32); // 2 * pi * 55 Hz / 22050 Hz = 0.0156723443 // But we divide out the pi because we don't use sin: // 2 * 55 Hz / 22050 Hz = 0.0049886621 x = .00499 * math.pow(2, (tmp3 % 24 + (delta == 2 ? 0 : 27)) / 12); tmp2 = [15, 9, 9][delta]; for (y = 0; y < 4593 * [1, 3, 2, 4][~~(tmp3 / 24)];) { // || works because we don't have the amplitude to reach sample value 0. // y * x is the phase times two: [0, 2). tmp[i++] = (tmp[i] || 127) + (y++ * x % 2 < 1 ? tmp2 : -tmp2); tmp2 *= 0.9999; } } } music = makeAudio(tmp); music.play(); music.loop = 1; function playSoundEffect(encoding) { if (!(tmp5 = sounds[encoding])) { tmp5 = new Uint8Array(9e3); tmp4 = 50; // amplitude for (j in tmp5) { tmp3 = j > 1e3 ? (encoding >> 4) & 63 : encoding >> 10; // period tmp5[j] = 127 + (tmp4 *= 1 - (encoding&15) / 1e4) * (j/10%tmp3 < tmp3 / 2 ? -1 : 1); } tmp5 = sounds[i] = makeAudio(tmp5); } else { tmp5.currentTime = 0; } tmp5.play(); } function makeAudio(wavArray) { tmp5 = wavArray.length; // https://ccrma.stanford.edu/courses/422/projects/WaveFormat/ // // wavArray.set([ // 0x52, 0x49, 0x46, 0x46, // "RIFF" // (tmp + 36) & 0xff, ((tmp + 36) >> 8) & 0xff, ((tmp + 36) >> 16), 0, // data size + 36 (little-endian) // 0x57, 0x41, 0x56, 0x45, // "WAVE" // // 0x66, 0x6d, 0x74, 0x20, // "fmt " // 16, 0, 0, 0, // size of this subchunk // 1, 0, // PCM // 1, 0, // mono // 34, 86, 0, 0, // sample rate: 22050 Hz // 34, 86, 0, 0, // byte rate: 22050 bytes/s // 1, 0, // block align // 8, 0, // bits per sample // // 0x64, 0x61, 0x74, 0x61, // "data" // tmp & 0xFF, (tmp >> 8) & 0xff, tmp >> 16, 0 // data size // ]); return new Audio(URL.createObjectURL(new Blob([ 'RIFF', // Assuming that this will be stored in little-endian. // It's actually platform-specific... new Uint32Array([tmp5 + 36, 0x45564157, 0x20746d66, 16, 65537, 22050, 22050, 524289, 0x61746164, tmp5]), wavArray ], {type: 'audio/wav'}))); } function isSolidAt(x, y, rotation, tetromino) { return currentTetromino && !(x & ~3) && !(y & ~3) && // range check for [0, 4) (shapes[charCodeAt](8*(tetromino || currentTetromino) - 8 + 2*rotation + (y>>1)) & (1 << (4 * (y&1) + x))); } function render() { tmp = currentY; while (currentTetromino && tryMove(currentX, currentY+1, currentRotation, 1)); for (y = 0; y < h+4; y++) { for (x = 0; x < w; x++) { i = y*w + x; if (y >= h) { grid[i] = isSolidAt(x, y-h, 0, bag[0]) ? bag[0] : 0; } shadowGrid[i] = isSolidAt(x-currentX, y-tmp, currentRotation) ? currentTetromino : isSolidAt(x-currentX, y-currentY, currentRotation) ? currentTetromino + 8 : grid[i] || 0; if (tmp3 = win['tis-' + i]) { tmp3 = tmp3.style; tmp3.background = '#' + ( state == 1 && stateTime % 4 < 2 && linesClearing[y] ? 'fff' : colors[shadowGrid[i] % 8]); tmp3.opacity = shadowGrid[i] > 7 ? 0.2 : 1; } } } currentY = tmp; tmp = divStyleMargin + '0;text-align:right;font-size:150%">'; win['tis-status'].innerHTML = 'Score' + tmp + score + divEnd + 'Lines' + tmp + lines + divEnd + 'Level' + tmp + level + divEnd; } function tryMove(posX, posY, rotation, doNotRender) { for (j = 0; x = (j&3), y = (j>>2), j < 16; j++) { if (isSolidAt(x, y, rotation) && ((x += posX) < 0 || x >= w || (y += posY) < 0 || y >= h || grid[y*w + x])) { return; } } currentX = posX; currentY = posY; currentRotation = rotation; lockTimer = 0; doNotRender || render(); return 1; } function frame(now) { if (!state) return; delta = (now - lastFrame) / 1e3 || 0; if (delta > .1) delta = .1; lastFrame = now; if (state == 2) { // Game over if (stateTime-- > 4 && !(stateTime % 4)) { for (x = 0; x < w;) { grid[stateTime*w/4 + x++] = 1 + ~~(math.random() * 7); } render(); } } else if (state == 1) { // Clearing if (--stateTime < 0) { for (y in linesClearing) { for (i = y*w+w-1; i >= 0; i--) { grid[i] = grid[i-w]; } } state = 3; } render(); } else { // state == 3: Regular gameplay // Handle keyboard input for (tmp2 in keysPressed) { if (tmp2 == 37 || tmp2 == 39) { // Move if (keysPressed[tmp2] >= 0) { keysPressed[tmp2] -= keysPressed[tmp2] ? .05 : .2; tryMove(currentX + 1 - 2 * (tmp2 == 37), currentY, currentRotation); } } if (tmp2 == 32) { // Space: hard drop if (!keysPressed[tmp2]) { while (tryMove(currentX, currentY + 1, currentRotation)); lockTimer = 9; } } if (tmp2 == 38 || tmp2 == 18 || tmp2 == 17) { // Rotate // -1 for left, 1 for right tmp4 = 1 - 2 * (tmp2 == 17); if (!keysPressed[tmp2]) { for (i = 0; i < 5;) { tmp = (currentTetromino == 1 ? wallKickTableI : wallKickTableRest)[charCodeAt](((currentRotation + 4 + (tmp4-1)/2))%4 * 5 + i++) - 32; if (tryMove(currentX + tmp4 * ((tmp & 7) - 2), currentY + tmp4 * (2 - (tmp >> 3)), (currentRotation+4+tmp4) % 4)) { playSoundEffect(8303); break; } } } } keysPressed[tmp2] += delta; } // Apply gravity gravityTimer += math.max( keysPressed[40] ? 0.2 : 0, delta * math.pow(1.23, level)); if (gravityTimer > 1) { gravityTimer = 0; tryMove(currentX, currentY + 1, currentRotation); } if (lockTimer > 1) { if (currentTetromino) playSoundEffect(31445); // Lock it in place; we assume that the render was just done for (i in grid) grid[i] = shadowGrid[i]; // Find full rows tmp2 = 0; linesClearing = []; rowNotFull: for (y = 0; y < h; y++) { for (x = 0; x < w;) { if (!grid[y*w + x++]) { continue rowNotFull; } } linesClearing[y] = state = 1; tmp2++; stateTime = 6; } if (tmp2) playSoundEffect([, 8392, 8260, 8242, 8225][tmp2]); score += 100 * [0, 1, 3, 5, 8][tmp2] * level; lines += tmp2; level = 1 + ~~(lines / 10); // Shuffle bag if needed if (bag.length < 2) { // tmp is a bitmask that tracks which tetrominos we've already added. // bit 0 is just a sentinel, bits 1-7 correspond to tetrominos. for (tmp = 1; tmp != 255;) { for (j = 0; tmp & (1 << j); j = 1 + ~~(math.random() * 7)); tmp |= 1 << j; bag.push(j); } } // Spawn new tetromino currentTetromino = bag.shift(); gravityTimer = 0; if (!tryMove(3, 0, 0)) { // Game over currentTetromino = 0; state = 2; stateTime = 4*h; playSoundEffect(31360); } } lockTimer += delta; } requestAnimationFrame(frame); } frame(0); function onKeyDown(e) { tmp = e[keyCode]; if (tmp == 77) { music[music.paused ? 'play' : 'pause'](); } if (tmp == 27) { // Quit doc.body.removeChild(html); doc[removeEventListener](keydown, onKeyDown); doc[removeEventListener](keyup, onKeyUp); music.pause(); state = 0; } keysPressed[tmp] = keysPressed[tmp] || 0; if ([17, 18, 27, 37, 38, 39, 40, 77].indexOf(tmp) >= 0) { e.preventDefault(); } } function onKeyUp(e) { delete keysPressed[e[keyCode]]; } doc[addEventListener](keydown, onKeyDown); doc[addEventListener](keyup, onKeyUp); })(); } }); })();