Making 16 Beads Game In HTML CSS And Javascript | battisi | letsbug
Hey everyone in this article we are making game in HTML CSS and Javascript. We are going to use canvas element to make the game. Name of the game is 16 Beads or battisi. If you don't know the rules of the game or how to play it you can view it here.
I assume you know HTML canvas and Javascript and how to work.
folder structure:
-/scripts/
-/main.js
-/piece.js
-/spot.js
-/styles/
-/main.css
-/index.html
Making 16 Beads Game
code: index.html
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>32si | letsbug</title><script src="./scripts/piece.js" defer></script><script src="./scripts/spot.js" defer></script><script src="./scripts/main.js" defer></script><link rel="stylesheet" href="./styles/main.css"></head><body><section class="winDialog-wrapper hide" id="dialog" ><article class="winDialog "><h1 id="winner">You Won!</h1><button id="close">Ok</button></article></section><main><article><h3>Current Game</h3><div class="scoreBoard red"><h3>Red</h3><p id="redScore">16</p></div><div class="scoreBoard blue"><h3>blue</h3><p id="blueScore">16</p></div><h3>Previous Game Winner</h3><ul id="games"></ul></article><canvas id="canvas" height="620px" width="430px" ></canvas><article><div><h3>Who's Turn</h3><p id="turnContainer">RED</p></div><button id="newGame">New Game</button><button id="toggleAIBtn">AI</button></article></main></body></html>
code: main.css
* {padding: 0;margin: 0;box-sizing: border-box;color: #fff;}body {width: 100vw;height: 100vh;display: grid;place-items: center;/* background: url('https://source.unsplash.com/random/1000x600/?nature'); */background: linear-gradient(to right, #3b3eed 0%, #85bbfd 100%);overflow-x: hidden;}canvas {border-radius: 1rem;box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.2);backdrop-filter: blur(5px);background-color: rgba(255, 255, 255, 0.2);}.red {background-color: rgb(237, 25, 78);}.blue {background-color: rgb(80, 150, 249);}article {padding: 1rem;display: flex;flex-direction: row;justify-content: space-around;}article h3,ul {display: none;}article .scoreBoard {display: flex;justify-content: space-evenly;align-items: center;border-radius: 1000px;padding: 1rem;}article .scoreBoard h3 {display: block;margin: auto .5rem;font-size: 1.5rem;font-weight: 600;text-transform: uppercase;}article .scoreBoard p {font-weight: 700;font-size: 1.9rem;}article button {padding: 1rem;border-radius: 999px;font-size: 1.3rem;background-color: #fff;border: 4px solid black;font-weight: 600;cursor: pointer;color: black;transition: border 0.2s ease-in-out;}.active{border:4px solid rgb(10, 156, 29);color: black;}#turnContainer {background-color: var(--turn);padding: 1rem;border-radius: 5rem;color: #fff;font-size: 2rem;margin: 0.3rem 0 1rem 0;}.winDialog-wrapper {position: absolute;width: 100vw;height: 100vh;background-color: rgba(0, 0, 0, .5);display: grid;place-items: center;z-index: 100;}.winDialog {display: grid;place-items: center;background-color: #fff;font-size: 5rem;box-shadow: 4px 5px 10px gray;padding: 3rem;border-radius: 4rem;}.winDialog h1 {color: darkorchid;text-shadow: 2px 2px 8px #ff0000, 4px 4px 10px cyan;margin-bottom: 2rem;}.winDialog button {color: darkorchid;text-shadow: 2px 2px 8px #ff0000, 4px 4px 10px cyan;box-shadow: 2px 2px 8px #ff0000, 4px 4px 10px cyan;padding: 1rem 2rem;border-radius: 3rem;font-size: 3rem;font-weight: 800;border: 5px solid violet;cursor: pointer;}.hide {display: none;}@media only screen and (min-width: 768px) {main {display: flex;flex-direction: row;padding: .5rem;border-radius: .5rem;}article h3,ul {display: block;}article {display: block;padding: 5rem .5rem;margin: .4rem;border-radius: 10rem;height: fit-content;text-align: center;margin: 0 1rem;box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.2);backdrop-filter: blur(5px);background-color: rgba(255, 255, 255, 0.2);}article h3 {font-size: 1.2rem;margin-bottom: .5rem;}article ul {list-style: none;}li h4 {font-size: 2rem;font-weight: lighter;}.scoreBoard {border-radius: 1000px;margin: .5rem auto;text-transform: capitalize;display: flex;justify-content: space-evenly;align-items: center;}.scoreBoard h3 {font-size: 2rem;font-weight: 400;margin: 0;}.scoreBoard p {font-size: 3rem;font-weight: 400;margin: 0;}}
code: main.js
"use strict";const blueScore = document.getElementById('blueScore');const redScore = document.getElementById('redScore');const turnContainer = document.getElementById('turnContainer');const newGame = document.getElementById('newGame');const games = document.getElementById('games');const toggleAIBtn = document.getElementById('toggleAIBtn');const canvas = document.querySelector("#canvas");const ctx = canvas.getContext("2d");const WIDTH = canvas.width;const HEIGHT = canvas.height;const BOARD_SIZE = 190;let RED = 16;let BLUE = 16;const previousGames = [];let HUMAN_ONE = "RED";let HUMAN_TWO = "BLUE";let AI = "BLUE";let PLAYER = HUMAN_ONE;let IS_AI_ACTIVE = false;let CURRENTLY_SELECTED_SPOT = null;let MOVES;const dialog = document.getElementById('dialog');const winner = document.getElementById('winner');const closeBtn = document.getElementById("close");closeBtn.addEventListener("click", () => {dialog.classList.toggle('hide');});turnContainer.style.setProperty("--turn", "rgb(237, 25, 78)");turnContainer.innerText = "RED";const BOARD = createBoard(ctx, BOARD_SIZE);showPreviousGames();drawRelationLines();addPiecesToBoard();canvas.addEventListener("click", (event) => {const rect = canvas.getBoundingClientRect();const x = event.clientX - rect.left;const y = event.clientY - rect.top;for (let i = 0; i < BOARD.length; i++) {for (let j = 0; j < BOARD[i].length; j++) {let spot = BOARD[i][j];if (spot.nullSpot)continue;if (spot.isOccupied) {if (spot.piece.isClicked(x, y))decideTurn(spot);}else {if (spot.clicked(x, y))movePiece(spot);}}}});newGame.addEventListener("click", () => {BOARD.forEach(col => {col.forEach(spot => {if (spot.isOccupied == true)spot.removePiece();});});addPiecesToBoard();RED = 16;BLUE = 16;updateScore();});toggleAIBtn?.addEventListener("click", () => {IS_AI_ACTIVE = !IS_AI_ACTIVE;if (IS_AI_ACTIVE)toggleAIBtn.classList.add("active");elsetoggleAIBtn.classList.remove("active");newGame?.click();});function addPiecesToBoard() {for (let i = 0; i < BOARD.length; i++) {if (i <= 3)for (let j = 0; j < BOARD[i].length; j++) {let spot = BOARD[i][j];if (!spot.nullSpot)spot.addPiece(new Piece(ctx, spot.x, spot.y, false));}if (i >= 5)for (let j = 0; j < BOARD[i].length; j++) {let spot = BOARD[i][j];if (!spot.nullSpot)spot.addPiece(new Piece(ctx, spot.x, spot.y, true));}}}function createBoard(context, size) {let x = WIDTH / 2;let y = HEIGHT / 2;const sizes = [[{ x: 0, y: 0 },{ x: x - size / 2, y: y - size - size / 2 },{ x: x, y: y - size - size / 2 },{ x: x + size / 2, y: y - size - size / 2 },{ x: 0, y: 0 },],[{ x: 0, y: 0 },{ x: x - size / 4, y: y - size - size / 4 },{ x: x, y: y - size - size / 4 },{ x: x + size / 4, y: y - size - size / 4 },{ x: 0, y: 0 },],[{ x: x - size, y: y - size },{ x: x - size / 2, y: y - size },{ x: x, y: y - size },{ x: x + size / 2, y: y - size },{ x: x + size, y: y - size },],[{ x: x - size, y: y - size / 2 },{ x: x - size / 2, y: y - size / 2 },{ x: x, y: y - size / 2 },{ x: x + size / 2, y: y - size / 2 },{ x: x + size, y: y - size / 2 },],[{ x: x - size, y: y },{ x: x - size / 2, y: y },{ x: x, y: y },{ x: x + size / 2, y: y },{ x: x + size, y: y },],[{ x: x - size, y: y + size / 2 },{ x: x - size / 2, y: y + size / 2 },{ x: x, y: y + size / 2 },{ x: x + size / 2, y: y + size / 2 },{ x: x + size, y: y + size / 2 },],[{ x: x - size, y: y + size },{ x: x - size / 2, y: y + size },{ x: x, y: y + size },{ x: x + size / 2, y: y + size },{ x: x + size, y: y + size },],[{ x: 0, y: 0 },{ x: x - size / 4, y: y + size + size / 4 },{ x: x, y: y + size + size / 4 },{ x: x + size / 4, y: y + size + size / 4 },{ x: 0, y: 0 },],[{ x: 0, y: 0 },{ x: x - size / 2, y: y + size + size / 2 },{ x: x, y: y + size + size / 2 },{ x: x + size / 2, y: y + size + size / 2 },{ x: 0, y: 0 },],];const relations = {"01": ["02", "11"],"02": ["01", "03", "12"],"03": ["02", "13"],"11": ["01", "12", "22"],"12": ["02", "11", "13", "22"],"13": ["03", "12", "22"],"20": ["21", "30", "31"],"21": ["20", "22", "31"],"22": ["21", "31", "32", "33", "23", "12", "11", "13"],"23": ["22", "24", "33"],"24": ["23", "33", "34"],"30": ["20", "31", "40"],"31": ["20", "21", "22", "32", "42", "41", "40", "30"],"32": ["22", "31", "42", "33"],"33": ["22", "23", "24", "34", "44", "43", "42", "32"],"34": ["24", "33", "44"],"40": ["30", "31", "41", "51", "50"],"41": ["31", "40", "51", "42"],"42": ["41", "31", "32", "33", "43", "53", "52", "51"],"43": ["42", "33", "44", "53"],"44": ["33", "34", "43", "53", "54"],"50": ["40", "51", "60"],"51": ["40", "41", "42", "50", "52", "60", "61", "62"],"52": ["51", "42", "53", "62"],"53": ["42", "43", "44", "52", "54", "62", "63", "64"],"54": ["44", "53", "64"],"60": ["50", "51", "61"],"61": ["51", "60", "62"],"62": ["51", "52", "53", "61", "63", "71", "72", "73"],"63": ["62", "53", "64"],"64": ["53", "54", "63"],"71": ["62", "72", "81"],"72": ["62", "71", "73", "82"],"73": ["62", "72", "83"],"81": ["71", "82"],"82": ["72", "81", "83"],"83": ["73", "82"],};const board = [];for (let i = 0; i <= 8; i++) {board[i] = [];if (i >= 2 && i <= 6)for (let j = 0; j <= 4; j++)board[i][j] = new Spot(false, context, sizes[i][j].x, sizes[i][j].y, `${i}${j}`, relations[`${i}${j}`]);elsefor (let j = 0; j <= 4; j++)if (j > 0 && j <= 3)board[i][j] = new Spot(false, context, sizes[i][j].x, sizes[i][j].y, `${i}${j}`, relations[`${i}${j}`]);elseboard[i][j] = new Spot(true);}return board;}function drawRelationLines() {for (let i = 0; i <= 8; i++)for (let j = 0; j <= 4; j++)if (!BOARD[i][j].nullSpot)BOARD[i][j].drawRelationLines(BOARD);}function updateTurn() {if (IS_AI_ACTIVE) {PLAYER === HUMAN_ONE ? PLAYER = AI : PLAYER = HUMAN_ONE;}else {PLAYER === HUMAN_ONE ? PLAYER = HUMAN_TWO : PLAYER = HUMAN_ONE;}if (PLAYER === HUMAN_ONE) {turnContainer.style.setProperty("--turn", "rgb(237, 25, 78)");turnContainer.innerText = "RED";}else {turnContainer.style.setProperty("--turn", "rgb(80, 150, 249)");turnContainer.innerText = IS_AI_ACTIVE ? "AI - BLUE" : "BLUE";IS_AI_ACTIVE ? nextAIMove() : null;}}function decideTurn(spot) {if (PLAYER === HUMAN_ONE) {if (spot.piece.isRed === true) {spot.piece.isSelected = true;possibleMoves(spot);}else {hideMoves();console.log("Blue cannot move ");}}else {if (spot.piece.isRed === false)possibleMoves(spot);else {hideMoves();console.log("Red cannot move ");}}}function hideMoves() {if (CURRENTLY_SELECTED_SPOT != null)CURRENTLY_SELECTED_SPOT.hidePossibleMoves(BOARD);}function possibleMoves(spot) {hideMoves();CURRENTLY_SELECTED_SPOT = spot;MOVES = CURRENTLY_SELECTED_SPOT.getPossibleMoves(BOARD);MOVES.forEach(move => {let [moveI, moveJ] = move.to.split("").map(Number);let possibleSpot = BOARD[moveI][moveJ];possibleSpot.isPossibleMove(true);});}function movePiece(newSpot) {if (newSpot.possibleMove == true) {CURRENTLY_SELECTED_SPOT.hidePossibleMoves(BOARD);CURRENTLY_SELECTED_SPOT.piece.newPosition(newSpot.x, newSpot.y);newSpot.addPiece(CURRENTLY_SELECTED_SPOT.piece);CURRENTLY_SELECTED_SPOT.removePiece();navigator.vibrate(50);killPiece(MOVES.filter(w => w.to === newSpot.boardPosition && w.through != '')[0]?.through);updateTurn();}}function killPiece(targetSpot) {if (targetSpot == undefined)return;let [i, j] = targetSpot.split('');let spot = BOARD[+i][+j];if (spot.piece.isRed)RED--;elseBLUE--;spot.removePiece();navigator.vibrate([50, 50, 50]);updateScore();checkWinner();}function showPreviousGames() {if (previousGames.length > 0) {if (previousGames.length == 1)games.innerHTML = ``;games.innerHTML = ``;previousGames.forEach(game => {games.innerHTML += `<li><h4>${game.winner} ${game.red} - ${game.blue}<h4><li>`;});}elsegames.innerHTML += `<li><h4>-<h4><li>`;}function updateScore() {blueScore.innerText = String(BLUE);redScore.innerText = String(RED);}function checkWinner() {if (BLUE === 0 && RED >= 1) {dialog.classList.toggle('hide');winner.textContent = `RED! Won This Game`;previousGames.push({winner: "RED",red: RED,blue: BLUE});showPreviousGames();return "RED";}else if (RED === 0 && BLUE >= 1) {dialog.classList.toggle('hide');winner.textContent = `BLUE! Won This Game`;previousGames.push({winner: "BLUE",red: RED,blue: BLUE});showPreviousGames();return "BLUE";}return null;}function nextAIMove() {let pickedSpot = null;let allMoves = [];BOARD.forEach(row => {row.forEach(spot => {if (spot.nullSpot)return;if (spot.piece?.isRed === true)return;if (!spot.isOccupied)return;allMoves.push({spot: spot,moves: spot.getPossibleMoves(BOARD)});});});let pickedMove;allMoves.forEach(spot => {if (spot.moves.length > 0) {spot.moves.forEach(move => {if (move.through != '') {pickedMove = move;pickedSpot = spot.spot;}if (pickedMove == undefined) {pickedMove = move;pickedSpot = spot.spot;}});}});decideTurn(pickedSpot);let [i, j] = pickedMove.to.split("").map(Number);let newSpot = BOARD[i][j];movePiece(newSpot);}
code: piece.js
"use strict";class Piece {constructor(context, x, y, isRed = false) {this.isRed = isRed;this.x = x;this.y = y;this.radius = 10;this.context = context;this.isSelected = false;this.draw();}selected(selected) {this.isSelected = selected;}draw() {this.context.beginPath();this.context.fillStyle = this.isRed ? 'red' : 'blue';this.context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);this.context.fill();this.context.stroke();}isClicked(mouseX, mouseY) {const distance = Math.hypot((mouseX - this.x), (mouseY - this.y));if (distance > this.radius)return false;return true;}newPosition(x, y) {this.x = x;this.y = y;this.draw();}}
code: spot.js
"use strict";class Spot {constructor(nullSpot, context = null, x = 0, y = 0, boardPosition = '', neighbours = []) {this.nullSpot = nullSpot;this.x = x;this.y = y;this.context = context;this.boardPosition = boardPosition;this.isOccupied = false;this.piece = null;this.radius = 12;this.neighbours = neighbours;this.possibleMove = false;if (!nullSpot) {this.draw();}}draw() {if (this.possibleMove) {this.context.beginPath();this.context.fillStyle = "green";this.context.arc(this.x, this.y, this.radius - 2, 0, 2 * Math.PI);this.context.fill();this.context.stroke();}else {this.context.beginPath();this.context.fillStyle = "#fff";this.context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);this.context.fill();}}drawRelationLines(board) {this.neighbours.forEach(neighbour => {let [indexI, indexJ] = neighbour.split('');let spot = board[+indexI][+indexJ];this.context.beginPath();this.context.strokeStyle = "#fff";this.context.moveTo(this.x, this.y);this.context.lineTo(spot.x, spot.y);this.context.stroke();});}getPiece() {return this.piece;}addPiece(piece) {this.isOccupied = true;this.piece = piece;}removePiece() {this.isOccupied = false;this.piece = null;this.draw();}clicked(mouseX, mouseY) {const distance = Math.hypot(mouseX - this.x, mouseY - this.y);if (distance < this.radius)return true;return false;}isPossibleMove(isIt) {this.possibleMove = isIt;this.draw();}getPossibleMoves(board) {let possibleJumps = [];let [myI, myJ] = this.boardPosition.split('').map(Number);for (let i = 0; i < this.neighbours.length; i++) {let [indexI, indexJ] = this.neighbours[i].split('').map(Number);let spot = board[indexI][indexJ];// if any of the neighbours is empty, it is a possible moveif (spot.isOccupied == false) {possibleJumps.push({through: '',to: this.neighbours[i]});}else {// if the spot's piece is of same color as the current spot's piece move to next iterationif (spot.piece.isRed === this.piece.isRed)continue;// if the neighbour is not the same colour as the current piece, it is a possible jumpspot.neighbours.forEach((neighbour) => {let [neighbourIndexI, neighbourIndexJ] = neighbour.split('').map(Number);let neighbouringSpot = board[neighbourIndexI][neighbourIndexJ];//if the neighbour's neighbour is not empty, just returnif (neighbouringSpot.isOccupied)return;// checking the neighbouring spot which are in the same row or column as the current pieceif (myI == indexI || myJ == indexJ) {// checking if the neighbouring spot's neighbour is in the same row or column as the current piece// if it is, it is a possible jumpif (myI == indexI && indexI == neighbourIndexI || myJ == indexJ && indexJ == neighbourIndexJ) {possibleJumps.push({through: spot.boardPosition,to: neighbouringSpot.boardPosition,});}}else {// checking neighbour's neighbour which are not in same row or column as the current neighbour or current piece// if it is, it is a possible jumpif ((indexI !== neighbourIndexI && indexJ !== neighbourIndexJ) &&(myI !== neighbourIndexI && myJ !== neighbourIndexJ)) {possibleJumps.push({through: spot.boardPosition,to: neighbouringSpot.boardPosition,});}}});}}return possibleJumps;}hidePossibleMoves(board) {let [myI, myJ] = this.boardPosition.split('').map(Number);this.neighbours.forEach(neighbour => {let [indexI, indexJ] = neighbour.split('').map(Number);let spot = board[indexI][indexJ];if (spot.isOccupied == false) {spot.isPossibleMove(false);}else {spot.neighbours.forEach((neighbour) => {let [neighbourIndexI, neighbourIndexJ] = neighbour.split('').map(Number);let neighbouringSpot = board[neighbourIndexI][neighbourIndexJ];if (myI == indexI || myJ == indexJ) {if ((myI == neighbourIndexI || myJ == neighbourIndexJ) &&neighbouringSpot.isOccupied == false)neighbouringSpot.isPossibleMove(false);}else {if ((indexI !== neighbourIndexI && indexJ !== neighbourIndexJ) &&(myI !== neighbourIndexI && myJ !== neighbourIndexJ) &&neighbouringSpot.isOccupied == false)neighbouringSpot.isPossibleMove(false);}});}});}}
output:
I have a added a random AI to play with. But will improve it with minimax and update with a new blog.
Thankyou.
Comments
Post a Comment