Sunday, June 11, 2017

Tetris in <100 lines of code

Controls: W, A, S, D

0
<script>
var piece, rows = [], delay = 500, key = 0; 
onkeydown = e => key = e.key;
document.body.onload = () => (
 cvs.style.width = "100px", cvs.style.height = "300px",
 piece = makep(), frame(), setTimeout(step, delay));

// respond to player input, draw the board and piece
function frame() {
 move(({w: p => p.r = (p.r+1)%4, s: p => p.y--,
  a: p => p.x--, d: p => p.x++})[key] || (p => p));
 key = 0;
 cvs.getContext("2d").clearRect(0, 0, cvs.width, cvs.height);
 var px = (x, y) => cvs.getContext("2d").fillRect(x, cvs.height-y-1, 1, 1);
 rows.map((r, y) => Object.keys(r).map(x => px(+x, y)));
 eachblock(piece, px);
 if (delay > 0) delay -= .01;
 requestAnimationFrame(frame);
}

// try to move the piece down, clear rows and/or end the game if can't
function step() {
 setTimeout(step, delay);
 if (move(p => p.y--)) return; 
 eachblock(piece, (x, y) => (rows[y] = rows[y] || {}, rows[y][x] = 1));
 score.innerHTML = parseInt(score.innerHTML) + rows.length - 
  (rows = rows.filter(r => Object.keys(r).length < cvs.width)).length;
 if (rows.length >= cvs.height) score.innerHTML += " -- GAME OVER";
 piece = makep();
}

function move(f) {
 var newp = Object.assign({}, piece);
 f(newp);
 if (legal(newp)) {Object.assign(piece, newp); return true}
 return false;
}

var makep = () => ({y: cvs.height-1, x: cvs.width/2 -1|0, r: 0,
 blocks: ["1111", "11\n 11", " 11\n11", "11\n11",
  "111\n1", "111\n 1", "111\n  1"][Math.random()*7|0]});

function eachblock(p, f) {
 var results = [], [x, y] = [p.x, p.y];
 var r = p.blocks == "11\n11"? 0 : p.r;
 var [nextblock, nextline] = [
  [() => x++, () => {y--; x = p.x}], [() => y--, () => {x--; y = p.y}],
  [() => x--, () => {y++; x = p.x}], [() => y++, () => {x++; y = p.y}]][r];
 p.blocks.split("").map(c => {
  if (c == "\n") {nextline(); return}
  if (c == "1") results.push(f(+x + [0,1,2,0][r], +y + [0,0,-1,-1][r]));
  nextblock()});
 return results;
}

var legal = p => eachblock(p, (x, y) => x >= 0 && x < cvs.width && y >= 0
 && !(rows[y] || {})[x]).filter(x=>x).length == 4;
</script>
<canvas id="cvs" width="10" height = "30"
style = "image-rendering:pixelated; border: 1px solid black;">
</canvas> <div id="score">0</div>

2 comments:

  1. this is very aesthetically pleasing

    speaking of "what if we were doing SWE interviews?" maybe a good interview question (with the caveat that of course all interview questions are terrible) would be "Make Tetris." Any format/tools, any way you like. See what kind of monstrosities come up. (I loved "hexing the technical interview" btw :)

    ReplyDelete
  2. That is very gratifying, thank you! It took me, IDK, 4 hours? So as a take-home assignment, I'd enjoy it -- I also expect that I'd learn a fair bit by watching someone try it, but it'd be stressful.

    A few fun notes:
    - This is a good example of the data structure you choose being important -- e.g. by storing the board as a list of rows from the bottom toward the top, we can remove rows simply by filtering out full rows from that list, and the rows above automatically drop down. Compressing that logic to a single line feels great.
    - Noticing that checking if a possible move would be legal -- "trymove" applies to both falling pieces and the player's movements was satisfying.
    - Noticing that I could calculate a piece's block positions anytime I needed them, and that when I needed one block's position I needed all of them, let me use "eachblock", a function that applies a function to each block's position in a piece, which feels like a nice pattern. I started making these "custom map functions" in my Game of Life programming, and I think I'll keep using that pattern.
    - The most difficult part was piece rotation -- I'd love to have a three-line solution to that, but I just couldn't figure out how to do it. (A subtle constraint is that deep-copying objects in JS is hard, so having each property of "piece" be a built-in datatype instead of an object was helpful, leading to a string-based representation of pieces -- I couldn't figure out how another representation could make rotation simpler, so I went with it, but that might have to change for further improvement.)

    ReplyDelete