-
-
Save amalmurali47/56bcdfa20f2f129ff4754bb6f1d00e54 to your computer and use it in GitHub Desktop.
Source code for AFPC Level B1. Play here: https://files.ircpuzzles.org/2024/6hKxyCuygaCoGsx2.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>blocks"r"us</title> | |
| <style> | |
| html, body { | |
| margin: 0; | |
| padding: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: black; | |
| display: flex; | |
| overflow: hidden; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="canvas" width="800" height="600"></canvas> | |
| <script> | |
| var WIDTH = 12; | |
| var HEIGHT = 24; | |
| var MARGIN = 4; | |
| function shape(v) { | |
| return v.split(' ').map(s => s.split('').map(c => c == '@')); | |
| } | |
| var SHAPES = [ | |
| shape('@@. .@. .@@'), | |
| shape('..@.. ..@.. ..@.. ..@.. ..@..'), | |
| shape('..@. .@@. ..@. ..@.'), | |
| shape('..@. ..@. .@@. .@..'), | |
| shape('@@@ .@. .@.'), | |
| shape('@.. @.. @@@'), | |
| shape('@.@ @@@ ...'), | |
| shape('.@@ .@@ .@.'), | |
| shape('@.. @@. .@@'), | |
| shape('.@. @@@ .@.'), | |
| shape('.@@ @@. .@.'), | |
| shape('.@.. .@.. .@.. .@@.'), | |
| ]; | |
| var COLOR_MAP = [ | |
| '#000', '#999', '#0FF', '#FF0', '#F0F', '#00F', '#F90', | |
| '#0F0', '#F00', '#09F', '#90F', '#9F0', '#990', '#099', | |
| ]; | |
| var well = [...Array(HEIGHT)].map(_ => Array(WIDTH).fill(0)); | |
| for (var y = 0; y < HEIGHT; y++) { | |
| well[y][0] = 1; | |
| well[y][WIDTH - 1] = 1; | |
| } | |
| for (var x = 0; x < WIDTH; x++) { | |
| well[HEIGHT - 1][x] = 1; | |
| } | |
| var current, nextChoice; | |
| var totalClears = 0; | |
| function inBounds(x, y) { | |
| return 0 <= x && x < WIDTH && 0 <= y && y < HEIGHT; | |
| } | |
| function writeShape(shape, ox, oy, value) { | |
| for (let y = 0; y < shape.length; y++) { | |
| for (let x = 0; x < shape[y].length; x++) { | |
| if (shape[y][x] && inBounds(ox + x, oy + y)) { | |
| well[oy + y][ox + x] = value; | |
| } | |
| } | |
| } | |
| } | |
| function intersectWell(shape, ox, oy) { | |
| for (let y = 0; y < shape.length; y++) { | |
| for (let x = 0; x < shape[y].length; x++) { | |
| if (shape[y][x]) { | |
| if (!inBounds(ox + x, oy + y)) return true; | |
| if (well[oy + y][ox + x] != 0) return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| function rotateShape(shape) { | |
| var res = []; | |
| for (var y = 0; y < shape.length; y++) { | |
| res[y] = []; | |
| for (var x = 0; x < shape[y].length; x++) { | |
| res[y][x] = shape[x][shape[y].length - y - 1]; | |
| } | |
| } | |
| return res; | |
| } | |
| var rngState = 1; | |
| function rngNext() { | |
| var res = rngState; | |
| rngState = (1202*rngState + 954) % 1469; | |
| return res; | |
| } | |
| function spawnCurrent() { | |
| var choice = nextChoice; | |
| nextChoice = rngNext() % 12; | |
| if (!nextChoice) { | |
| rngState = 1; | |
| nextChoice = rngNext() % 12; | |
| } | |
| var shape = SHAPES[choice]; | |
| current = { | |
| shape: shape, | |
| value: 2 + choice, | |
| x: (WIDTH - SHAPES[choice][0].length) / 2 | 0, | |
| y: 0, | |
| }; | |
| } | |
| function eraseCurrent() { | |
| writeShape(current.shape, current.x, current.y, 0); | |
| } | |
| function writeCurrent() { | |
| writeShape(current.shape, current.x, current.y, current.value); | |
| } | |
| function intersectCurrent() { | |
| return intersectWell(current.shape, current.x, current.y); | |
| } | |
| function clearFullLines() { | |
| var shift = 0; | |
| for (var i = HEIGHT - 2; i > 0; i--) { | |
| while (i - shift >= 0 && well[i - shift].every(v => v != 0)) shift += 1; | |
| if (i - shift >= 0) { | |
| well[i] = well[i - shift].slice(); | |
| } else { | |
| well[i] = Array(WIDTH).fill(0); | |
| well[i][0] = 1; | |
| well[i][WIDTH - 1] = 1; | |
| } | |
| } | |
| totalClears += shift; | |
| } | |
| function gameOver() { | |
| state = 'gameover'; | |
| counters.gameover = HEIGHT - 1; | |
| } | |
| function renderGameOverLine(y) { | |
| for (var x = 0; x < WIDTH; x++) { | |
| if (well[y][x] != 0) well[y][x] = 1; | |
| } | |
| } | |
| function spawn() { | |
| spawnCurrent(); | |
| if (intersectCurrent()) { | |
| gameOver(); | |
| } else { | |
| writeCurrent(); | |
| } | |
| } | |
| function hardDrop() { | |
| eraseCurrent(); | |
| while (!intersectCurrent()) { | |
| current.y += 1; | |
| } | |
| current.y -= 1; | |
| state = 'lock'; | |
| counters.lock = 0; | |
| } | |
| var canvas = document.getElementById('canvas'); | |
| var ctx = canvas.getContext('2d'); | |
| function render() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| var scale = Math.min( | |
| window.innerWidth / (WIDTH + 2 + 6), | |
| window.innerHeight / (HEIGHT + 2) | |
| ) | 0; | |
| var renderLeft = (canvas.width - (WIDTH + 6) * scale) / 2 | 0; | |
| var renderTop = (canvas.height - HEIGHT * scale) / 2 | 0; | |
| ctx.fillStyle = COLOR_MAP[0]; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| for (var y = 0; y < HEIGHT; y++) { | |
| for (var x = 0; x < WIDTH; x++) { | |
| // don't render walls above margin | |
| if (y < MARGIN && well[y][x] == 1) continue; | |
| ctx.fillStyle = COLOR_MAP[well[y][x]]; | |
| ctx.fillRect( | |
| renderLeft + x*scale + 1, | |
| renderTop + y*scale + 1, | |
| scale - 2, | |
| scale - 2 | |
| ); | |
| } | |
| } | |
| ctx.fillStyle = '#CCC'; | |
| ctx.font = '30pt monospace'; | |
| ctx.fillText( | |
| 'Next:', | |
| renderLeft + (WIDTH + 1)*scale, | |
| renderTop + 5*scale, | |
| ); | |
| ctx.fillText( | |
| 'Clears: ' + totalClears, | |
| renderLeft + (WIDTH + 1)*scale, | |
| renderTop + 15*scale, | |
| ); | |
| var nextShape = SHAPES[nextChoice]; | |
| ctx.fillStyle = COLOR_MAP[2 + nextChoice]; | |
| for (var y = 0; y < nextShape.length; y++) { | |
| for (var x = 0; x < nextShape[y].length; x++) { | |
| if (nextShape[y][x]) { | |
| ctx.fillRect( | |
| renderLeft + (WIDTH + 1)*scale + x*scale + 1, | |
| renderTop + 6*scale + y*scale + 1, | |
| scale - 2, | |
| scale - 2 | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| var FRAME_TEMPO = 25; | |
| var DELAY_LOCK = 20; | |
| var INTERVAL_GRAVITY = 10; | |
| var INTERVAL_MODULUS = 10; | |
| var counters = { | |
| frame: 0, | |
| lock: 0, | |
| gameover: 0, | |
| }; | |
| var state = 'active'; | |
| // active: regular play | |
| // lock: in lock delay | |
| function applyGravity() { | |
| eraseCurrent(); | |
| if (intersectWell(current.shape, current.x, current.y + 1)) { | |
| state = 'lock'; | |
| counters.lock = DELAY_LOCK; | |
| } else { | |
| current.y += 1; | |
| } | |
| writeCurrent(); | |
| } | |
| function tick() { | |
| counters.frame = (counters.frame + 1) % INTERVAL_MODULUS; | |
| if (state == 'active') { | |
| if (counters.frame % INTERVAL_GRAVITY == 0) { | |
| applyGravity(); | |
| } | |
| } else if (state == 'lock') { | |
| if (counters.lock == 0) { | |
| state = 'active'; | |
| clearFullLines(); | |
| spawn(); | |
| } else { | |
| counters.lock -= 1; | |
| } | |
| } else if (state == 'gameover') { | |
| renderGameOverLine(counters.gameover); | |
| if (counters.gameover == 0) clearInterval(tickInterval); | |
| counters.gameover -= 1; | |
| } | |
| render(); | |
| } | |
| var tickInterval = setInterval(tick, FRAME_TEMPO); | |
| window.addEventListener('keydown', (ev) => { | |
| if (state != 'active' && state != 'lock') return; | |
| var prev = {...current}; | |
| eraseCurrent(); | |
| switch (ev.keyCode) { | |
| case 32: hardDrop(); break; | |
| case 37: current.x -= 1; break; | |
| case 38: current.shape = rotateShape(current.shape); break; | |
| case 39: current.x += 1; break; | |
| case 40: current.y += 1; break; | |
| } | |
| if (state == 'lock' && | |
| !intersectCurrent() && | |
| !intersectWell(current.shape, current.x, current.y + 1)) { | |
| state = 'active'; | |
| } | |
| if (intersectCurrent()) { | |
| current = prev; | |
| if (ev.keyCode == 40) { | |
| state = 'lock'; | |
| counters.lock = 0; | |
| } | |
| } | |
| writeCurrent(); | |
| }, false); | |
| nextChoice = rngNext(); | |
| spawn(); | |
| render(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment