Last active
October 11, 2025 23:41
-
-
Save straker/afc5bedc7f4b4bc65ba8b05c435f6d32 to your computer and use it in GitHub Desktop.
Revisions
-
straker revised this gist
Jul 14, 2023 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -45,3 +45,4 @@ Basic HTML Games are made possible by users like you. When you become a [Patron] - Karar Al-Remahy - UnbrandedTech - Innkeeper Games - Nezteb -
straker revised this gist
Mar 3, 2023 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -34,6 +34,7 @@ This is a basic implementation of the game Bust-a-Move / Puzzle Bobble / Bubble - [Sokoban](https://gist.github.com/straker/2fddb507d4bb6bec54ea2fdb022d020c) - [Doodle Jump](https://gist.github.com/straker/b96a4a68bd6d79cf75a833d98a2b654f) - [Helicopter](https://gist.github.com/straker/0d25ae9d235f6a62f8287fd36a097043) - [Block Dude](https://gist.github.com/straker/df855f22e57576c80d6126aa5609654e) ## Support -
straker revised this gist
Jan 27, 2023 . 1 changed file with 2 additions and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -42,4 +42,5 @@ Basic HTML Games are made possible by users like you. When you become a [Patron] ### Top Patrons - Karar Al-Remahy - UnbrandedTech - Innkeeper Games -
straker revised this gist
Jun 22, 2022 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -33,7 +33,7 @@ This is a basic implementation of the game Bust-a-Move / Puzzle Bobble / Bubble - [Missile Command](https://gist.github.com/straker/afc4e2a30b6df772a5f9f6ef01751d41) - [Sokoban](https://gist.github.com/straker/2fddb507d4bb6bec54ea2fdb022d020c) - [Doodle Jump](https://gist.github.com/straker/b96a4a68bd6d79cf75a833d98a2b654f) - [Helicopter](https://gist.github.com/straker/0d25ae9d235f6a62f8287fd36a097043) ## Support -
straker revised this gist
Jun 21, 2022 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -33,6 +33,7 @@ This is a basic implementation of the game Bust-a-Move / Puzzle Bobble / Bubble - [Missile Command](https://gist.github.com/straker/afc4e2a30b6df772a5f9f6ef01751d41) - [Sokoban](https://gist.github.com/straker/2fddb507d4bb6bec54ea2fdb022d020c) - [Doodle Jump](https://gist.github.com/straker/b96a4a68bd6d79cf75a833d98a2b654f) - [Helicopter](https://gist.github.com/straker/straker/0d25ae9d235f6a62f8287fd36a097043) ## Support -
straker revised this gist
Apr 4, 2022 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,4 @@ # Basic Bust-a-Move / Puzzle Bobble / Bubble Shooter HTML and JavaScript Game This is a basic implementation of the game Bust-a-Move / Puzzle Bobble / Bubble Shooter, but it's missing a few things intentionally and they're left as further exploration for the reader. -
straker revised this gist
Apr 3, 2022 . No changes.There are no files selected for viewing
-
straker revised this gist
Apr 3, 2022 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -2,7 +2,7 @@ This is a basic implementation of the game Bust-a-Move / Puzzle Bobble / Bubble Shooter, but it's missing a few things intentionally and they're left as further exploration for the reader. <img width="276" height="400" alt="" src="https://user-images.githubusercontent.com/2433219/161411816-910d3717-987c-44ef-8b61-f49127373ad5.png"> ## Further Exploration -
straker revised this gist
Apr 3, 2022 . No changes.There are no files selected for viewing
-
straker revised this gist
Apr 3, 2022 . 2 changed files with 465 additions and 2 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,11 +1,17 @@ # Basic Bust-a-Move / Puzzle Bobble / Bubble Shooter HTML Game This is a basic implementation of the game Bust-a-Move / Puzzle Bobble / Bubble Shooter, but it's missing a few things intentionally and they're left as further exploration for the reader. <img width="276" height="400" alt="" src="ttps://user-images.githubusercontent.com/2433219/161411816-910d3717-987c-44ef-8b61-f49127373ad5.png"> ## Further Exploration - More levels - Add more levels and have the next level start once the last one is finished - Next Piece Preview - Show the next bubble that will be shot - Ceiling Drop - Every so often, the ceiling will drop down 1 grid space and move all the bubbles down the screen with it - Mobile and touchscreen support - Allow the game to be scaled down to a phone size. See https://codepen.io/straker/pen/VazMaL - Support [touch controls](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events) @@ -25,6 +31,7 @@ This is a basic implementation of the game Puzzle Bobble / Bubble Shooter, but i - [Bomberman](https://gist.github.com/straker/769fb461e066147ea16ac2cb9463beae) - [Frogger](https://gist.github.com/straker/82a4368849cbd441b05bd6a044f2b2d3) - [Missile Command](https://gist.github.com/straker/afc4e2a30b6df772a5f9f6ef01751d41) - [Sokoban](https://gist.github.com/straker/2fddb507d4bb6bec54ea2fdb022d020c) - [Doodle Jump](https://gist.github.com/straker/b96a4a68bd6d79cf75a833d98a2b654f) ## Support 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,456 @@ <!DOCTYPE html> <html> <head> <title>Basic Bust-a-Move / Puzzle Bobble / Bubble Shooter HTML Game</title> <meta charset="UTF-8"> <style> html, body { height: 100%; margin: 0; } body { background: black; display: flex; align-items: center; justify-content: center; } </style> </head> <body> <canvas width="271" height="392" id="game"></canvas> <script> const canvas = document.getElementById('game'); const context = canvas.getContext('2d'); // puzzle bubble is played on a hex grid. instead of doing complicated // math of working with a hex grid, we can just fill the screen with // bubbles in their correct positions. each bubble will start inactive, // meaning we pretend the bubble isn't there (don't draw it or count // it for collision). when the bubble we shoot collides with a wall // or another active bubble, we just find the closest inactive bubble // and make it active with the same color as the shot bubble. this // gives the illusion of the bubble snapping to a grid const grid = 32; // each even row is 8 bubbles long and each odd row is 7 bubbles long. // the level consists of 4 rows of bubbles of 4 colors: red, orange, // green, and yellow const level1 = [ ['R','R','Y','Y','B','B','G','G'], ['R','R','Y','Y','B','B','G'], ['B','B','G','G','R','R','Y','Y'], ['B','G','G','R','R','Y','Y'] ]; // create a mapping between color short code (R, G, B, Y) and color name const colorMap = { 'R': 'red', 'G': 'green', 'B': 'blue', 'Y': 'yellow' }; const colors = Object.values(colorMap); // use a 1px gap between each bubble const bubbleGap = 1; // the size of the outer walls for the game const wallSize = 4; const bubbles = []; let particles = []; // helper function to convert deg to radians function degToRad(deg) { return (deg * Math.PI) / 180; } // rotate a point by an angle function rotatePoint(x, y, angle) { let sin = Math.sin(angle); let cos = Math.cos(angle); return { x: x * cos - y * sin, y: x * sin + y * cos }; } // get a random integer between the range of [min,max] // @see https://stackoverflow.com/a/1527820/2124254 function getRandomInt(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } // get the distance between two points function getDistance(obj1, obj2) { const distX = obj1.x - obj2.x; const distY = obj1.y - obj2.y; return Math.sqrt(distX * distX + distY * distY); } // check for collision between two circles function collides(obj1, obj2) { return getDistance(obj1, obj2) < obj1.radius + obj2.radius; } // find the closest bubbles that collide with the object function getClosestBubble(obj, activeState = false) { const closestBubbles = bubbles .filter(bubble => bubble.active == activeState && collides(obj, bubble)); if (!closestBubbles.length) { return; } return closestBubbles // turn the array of bubbles into an array of distances .map(bubble => { return { distance: getDistance(obj, bubble), bubble } }) .sort((a, b) => a.distance - b.distance)[0].bubble; } // create the bubble grid bubble. passing a color will create // an active bubble function createBubble(x, y, color) { const row = Math.floor(y / grid); const col = Math.floor(x / grid); // bubbles on odd rows need to start half-way on the grid const startX = row % 2 === 0 ? 0 : 0.5 * grid; // because we are drawing circles we need the x/y position // to be the center of the circle instead of the top-left // corner like you would for a square const center = grid / 2; bubbles.push({ x: wallSize + (grid + bubbleGap) * col + startX + center, // the bubbles are closer on the y axis so we subtract 4 on every // row y: wallSize + (grid + bubbleGap - 4) * row + center, radius: grid / 2, color: color, active: color ? true : false }); } // get all bubbles that touch the passed in bubble function getNeighbors(bubble) { const neighbors = []; // check each of the 6 directions by "moving" the bubble by a full // grid in each of the 6 directions (60 degree intervals) // @see https://www.redblobgames.com/grids/hexagons/#angles const dirs = [ // right rotatePoint(grid, 0, 0), // up-right rotatePoint(grid, 0, degToRad(60)), // up-left rotatePoint(grid, 0, degToRad(120)), // left rotatePoint(grid, 0, degToRad(180)), // down-left rotatePoint(grid, 0, degToRad(240)), // down-right rotatePoint(grid, 0, degToRad(300)) ]; for (let i = 0; i < dirs.length; i++) { const dir = dirs[i]; const newBubble = { x: bubble.x + dir.x, y: bubble.y + dir.y, radius: bubble.radius }; const neighbor = getClosestBubble(newBubble, true); if (neighbor && neighbor !== bubble && !neighbors.includes(neighbor)) { neighbors.push(neighbor); } } return neighbors; } // remove bubbles that create a match of 3 colors function removeMatch(targetBubble) { const matches = [targetBubble]; bubbles.forEach(bubble => bubble.processed = false); targetBubble.processed = true; // loop over the neighbors of matching colors for more matches let neighbors = getNeighbors(targetBubble); for (let i = 0; i < neighbors.length; i++) { let neighbor = neighbors[i]; if (!neighbor.processed) { neighbor.processed = true; if (neighbor.color === targetBubble.color) { matches.push(neighbor); neighbors = neighbors.concat(getNeighbors(neighbor)); } } } if (matches.length >= 3) { matches.forEach(bubble => { bubble.active = false; }); } } // make any floating bubbles (bubbles that don't have a bubble chain // that touch the ceiling) drop down the screen function dropFloatingBubbles() { const activeBubbles = bubbles.filter(bubble => bubble.active); activeBubbles.forEach(bubble => bubble.processed = false); // start at the bubbles that touch the ceiling let neighbors = activeBubbles .filter(bubble => bubble.y - grid <= wallSize); // process all bubbles that form a chain with the ceiling bubbles for (let i = 0; i < neighbors.length; i++) { let neighbor = neighbors[i]; if (!neighbor.processed) { neighbor.processed = true; neighbors = neighbors.concat(getNeighbors(neighbor)); } } // any bubble that is not processed doesn't touch the ceiling activeBubbles .filter(bubble => !bubble.processed) .forEach(bubble => { bubble.active = false; // create a particle bubble that falls down the screen particles.push({ x: bubble.x, y: bubble.y, color: bubble.color, radius: bubble.radius, active: true }); }); } // fill the grid with inactive bubbles for (let row = 0; row < 10; row++) { for (let col = 0; col < (row % 2 === 0 ? 8 : 7); col++) { // if the level has a bubble at the location, create an active // bubble rather than an inactive one const color = level1[row]?.[col]; createBubble(col * grid, row * grid, colorMap[color]); } } const curBubblePos = { // place the current bubble horizontally in the middle of the screen x: canvas.width / 2, y: canvas.height - grid * 1.5 }; const curBubble = { x: curBubblePos.x, y: curBubblePos.y, color: 'red', radius: grid / 2, // a circles radius is half the width (diameter) // how fast the bubble should go in either the x or y direction speed: 8, // bubble velocity dx: 0, dy: 0 }; // angle (in radians) of the shooting arrow let shootDeg = 0; // min/max angle (in radians) of the shooting arrow const minDeg = degToRad(-60); const maxDeg = degToRad(60); // the direction of movement for the arrow (-1 = left, 1 = right) let shootDir = 0; // reset the bubble to shoot to the bottom of the screen function getNewBubble() { curBubble.x = curBubblePos.x; curBubble.y = curBubblePos.y; curBubble.dx = curBubble.dy = 0; const randInt = getRandomInt(0, colors.length - 1); curBubble.color = colors[randInt]; } // handle collision between the current bubble and another bubble function handleCollision(bubble) { bubble.color = curBubble.color; bubble.active = true; getNewBubble(); removeMatch(bubble); dropFloatingBubbles(); } // game loop function loop() { requestAnimationFrame(loop); context.clearRect(0,0,canvas.width,canvas.height); // move the shooting arrow shootDeg = shootDeg + degToRad(2) * shootDir; // prevent shooting arrow from going below/above min/max if (shootDeg < minDeg) { shootDeg = minDeg; } else if (shootDeg > maxDeg) { shootDeg = maxDeg } // move current bubble by it's velocity curBubble.x += curBubble.dx; curBubble.y += curBubble.dy; // prevent bubble from going through walls by changing its velocity if (curBubble.x - grid / 2 < wallSize) { curBubble.x = wallSize + grid / 2; curBubble.dx *= -1; } else if (curBubble.x + grid / 2 > canvas.width - wallSize) { curBubble.x = canvas.width - wallSize - grid / 2; curBubble.dx *= -1; } // check to see if bubble collides with the top wall if (curBubble.y - grid / 2 < wallSize) { // make the closest inactive bubble active const closestBubble = getClosestBubble(curBubble); handleCollision(closestBubble); } // check to see if bubble collides with another bubble for (let i = 0; i < bubbles.length; i++) { const bubble = bubbles[i]; if (bubble.active && collides(curBubble, bubble)) { const closestBubble = getClosestBubble(curBubble); if (!closestBubble) { window.alert('Game Over'); window.location.reload(); } if (closestBubble) { handleCollision(closestBubble); } } } // move bubble particles particles.forEach(particle => { particle.y += 8; }); // remove particles that went off the screen particles = particles.filter(particles => particles.y < canvas.height - grid / 2); // draw walls context.fillStyle = 'lightgrey'; context.fillRect(0, 0, canvas.width, wallSize); context.fillRect(0, 0, wallSize, canvas.height); context.fillRect(canvas.width - wallSize, 0, wallSize, canvas.height); // draw bubbles and particles bubbles.concat(particles).forEach(bubble => { if (!bubble.active) return; context.fillStyle = bubble.color; // draw a circle context.beginPath(); context.arc(bubble.x, bubble.y, bubble.radius, 0, 2 * Math.PI); context.fill(); }); // draw fire arrow. since we're rotating the canvas we need to save // the state and restore it when we're done context.save(); // move to the center of the rotation (the middle of the bubble) context.translate(curBubblePos.x, curBubblePos.y); context.rotate(shootDeg); // move to the top-left corner of or fire arrow context.translate(0, -grid / 2 * 4.5); // draw arrow ↑ context.strokeStyle = 'white'; context.lineWidth = 2; context.beginPath(); context.moveTo(0, 0); context.lineTo(0, grid * 2); context.moveTo(0, 0); context.lineTo(-10, grid * 0.4); context.moveTo(0, 0); context.lineTo(10, grid * 0.4); context.stroke(); context.restore(); // draw current bubble context.fillStyle = curBubble.color; context.beginPath(); context.arc(curBubble.x, curBubble.y, curBubble.radius, 0, 2 * Math.PI); context.fill(); } // listen for keyboard events to move the fire arrow document.addEventListener('keydown', (e) => { if (e.code === 'ArrowLeft') { shootDir = -1; } else if (e.code === 'ArrowRight') { shootDir = 1; } // if the current bubble is not moving we can launch it if (e.code === 'Space' && curBubble.dx === 0 && curBubble.dy === 0) { // convert an angle to x/y curBubble.dx = Math.sin(shootDeg) * curBubble.speed; curBubble.dy = -Math.cos(shootDeg) * curBubble.speed; } }); // listen for keyboard events to stop moving the fire arrow if key is // released document.addEventListener('keyup', (e) => { if ( // only reset shoot dir if the released key is also the current // direction of movement. otherwise if you press down both arrow // keys at the same time and then release one of them, the arrow // stops moving even though you are still pressing a key (e.code === 'ArrowLeft' && shootDir === -1) || (e.code === 'ArrowRight' && shootDir === 1) ) { shootDir = 0; } }); // start the game requestAnimationFrame(loop); </script> </body> </html> -
straker created this gist
Mar 31, 2022 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,37 @@ # Basic Sokban HTML Game This is a basic implementation of the game Puzzle Bobble / Bubble Shooter, but it's missing a few things intentionally and they're left as further exploration for the reader. ## Further Exploration - More levels - Add more levels and have the next level start once the last one is finished - Mobile and touchscreen support - Allow the game to be scaled down to a phone size. See https://codepen.io/straker/pen/VazMaL - Support [touch controls](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events) **Important note:** I will answer questions about the code but will not add more features or answer questions about adding more features. This series is meant to give a basic outline of the game but nothing more. ## License (CC0 1.0 Universal) You're free to use this game and code in any project, personal or commercial. There's no need to ask permission before using these. Giving attribution is not required, but appreciated. ## Other Basic Games - [Snake](https://gist.github.com/straker/ff00b4b49669ad3dec890306d348adc4) - [Pong](https://gist.github.com/straker/81b59eecf70da93af396f963596dfdc5) - [Breakout](https://gist.github.com/straker/98a2aed6a7686d26c04810f08bfaf66b) - [Tetris](https://gist.github.com/straker/3c98304f8a6a9174efd8292800891ea1) - [Bomberman](https://gist.github.com/straker/769fb461e066147ea16ac2cb9463beae) - [Frogger](https://gist.github.com/straker/82a4368849cbd441b05bd6a044f2b2d3) - [Missile Command](https://gist.github.com/straker/afc4e2a30b6df772a5f9f6ef01751d41) - [Doodle Jump](https://gist.github.com/straker/b96a4a68bd6d79cf75a833d98a2b654f) ## Support Basic HTML Games are made possible by users like you. When you become a [Patron](https://www.patreon.com/straker), you get access to behind the scenes development logs, the ability to vote on which games I work on next, and early access to the next Basic HTML Game. ### Top Patrons - Karar Al-Remahy - UnbrandedTech