Skip to content

Instantly share code, notes, and snippets.

@amalmurali47
Created June 17, 2024 17:08
Show Gist options
  • Select an option

  • Save amalmurali47/56bcdfa20f2f129ff4754bb6f1d00e54 to your computer and use it in GitHub Desktop.

Select an option

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
<!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