To demonstrate my proficiency in vanilla Javascript, HTML and CSS.
Candy Crush is a single player, 3 tile-matching game which is well loved.
Candy Crush Saga has today turned 10, and one of the world's most popular casual games
is celebrating it with new in-game content. The developer of Candy Crush Saga, King,
is promising celebratory features and updated, which include newly added audio tracks.
Even after 10 years since its release, Candy Crush Saga is still among the most downloaded
games in 2022 and cracks the top10 most downloaded mobile games regularly.
- Javascript
- event handlers, including drap and drop
- regex
- HTML
- audio element
- CSS
- flex and grid
- shake animation
- background gradient animation
- vitest
- unit tests of functions and classes
- 1.Player swipes candy to match 3 tiles. resulting in candy being removed, and refilled from the top.
- 2.Game to have sound effects and background music. This will give a great experience.
- 3.Player wins the round when Player matches "X" number of "Y" colour candy in "Z" number of moves. Player loses the round when "Z" number of moves is zero.
- 4.Score is calculated based on 3's, 4's and 5's, number of cascades. Resulting in grading of 1 star, 2 star, 3 star.
- 5.Game to have start screen(x), game screen(x). Toggle menus for settings and level selection.
- 6.Player can progress to level 2 to 9. Levels get harder with blocker tiles, more colours, larger board size, increase "X", decrease "Z".
- 7.Make the app mobile responsive. Vary the layout of the elements
- 1.Four(4) in a line yields a striped candy. This will wipe out one line or column.
- 2.Five(5) in a line yields a colour bomb. This will wipe out all of the same colour.
- 3.CSS Animation for the candy pop, candy fill, wiping laser effects.
- 1. Implement 8 x 8 grid with cells
- 2. Study and implement drag and drop listeners
- 3. Candy can only move to up, down, left, right
- 4. Candy can only move if there is at least 3 candy matches after moving. Save a copy of the grid, revert to previous grid if invalid
- 5. Check all rows and columns for 3 or more in a line.
- 6. Remove the marked candy and render the grid.
- 7. Drop the candy vertically. And fill in random candy from the top.
- 8. Repeat steps 5 to 7 recursively. (this may be challenging)
- 9. Countdown the Number of Moves;
- 10. Countdown the number of blue candy removed from 10.
- 11. Game win if blue candy = 0;
- 12. Game over if number of rounds = 0;
- 13. Count points per item removed.
- 14. Award 1 star for >18 moves, 2 star for >14 moves, 3 star for 10 moves
markLineThree(str) {
let newStr = str
.replace("H", "A")
.replace("N", "A")
.replace("I", "B")
.replace("O", "B")
.replace("J", "C")
.replace("P", "C")
.replace("K", "D")
.replace("Q", "D")
.replace("L", "E")
.replace("R", "E")
.replace("M", "F")
.replace("S", "F");
const regex = /([A-Z])\1{2,}/g;
let numDuplicates = 0;
const markedString = newStr.replace(regex, (match) => {
numDuplicates = match.length;
return "1".repeat(numDuplicates);
});
return markedString;
}
The below code is easily understood. However it is tedious to type 2 for loops.
#### original function ####
fillGridArrayBlanks() {
for (let i = 0; i < this.rowCount; i++) {
for (let j = 0; j < this.colCount; j++) {
if (this.grid[i][j] === " ") {
this.grid[i][j] = this.getRandomCandy();
}
}
}
}
I refactored it to have 2 reduce functions. It worked, but REDUCE is hard to understand. So I scrapped it!
//refactored with reduce function
fillGridArrayBlanks2() {
const result = this.grid.reduce((prevRows, currRow) => {
const resultRow = currRow.reduce((prevItem, currItem) => {
if (currItem === " ") {
prevItem.push(this.getRandomCandy());
} else {
prevItem.push(currItem);
}
return prevItem;
}, []);
prevRows.push(resultRow);
return prevRows;
}, []);
this.grid = result;
return result;
}
For this 3rd version I use the map function 2 times. And I put the conditional function separately so it is modular. It is cleaner and more extendable. Different conditionals can be passed into the generic 2D array mapping function. What do you think?
//refactored wth map function and conditional function is abstracted for clarity
fillGridArrayBlanks3() {
const conditionalFunction = (item) =>
(item === " " ? this.getRandomCandy() : item);
const result = this.grid.map((row) => row.map(conditionalFunction));
this.grid = result;
return result;
}
Actually it is preferred to use nested for loop since it is easier to be understood. It is more obvious I'm changing the row or the column.
import { describe, it, expect } from "vitest";
import { GameData } from "GameData.js";
describe("GameData class", () => {
it("initGridArray() returns array of 6 x 6 blanks", () => {
let gd = new GameData();
gd.grid = gd.initGridArray(6,6);
const testGrid = [
[" ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " "],
[" ", " ", " ", " ", " ", " "],
];
console.log(testGrid);
console.log(gd.grid);
gd.grid.map((row, rowIndex) => {
row.map((item, colIndex) =>
expect(item).toBe(testGrid[rowIndex][colIndex])
);
});
});
it("fillGridArray() returns array of 6 x 6 letters from A to F", () => {
let gd = new GameData();
gd.initGridArray();
gd.fillGridArray();
console.log(gd.grid);
gd.grid.map((row, rowIndex) => {
row.map((item, colIndex) => expect(item).toMatch(/[A-G]/));
});
});
PS C:\Users\ernes\Coding2\SEI\Projects\candycrush> npm run test
> test
> vitest --run --reporter verbose --globals
RUN v0.28.5 C:/Users/ernes/Coding2/SEI/Projects/candycrush
✓ utils.test.js (4)
✓ math functions (4)
✓ max() gives the maximum of 2 numbers
✓ min() gives the minimum of 2 numbers
✓ findLength(from,to) gives the length between 2 points
✓ findAngle(from,to) gives the angle with horizontal
· GameData.test.js (23)
· GameData class (23)
stdout | GameData.test.js > GameData class > initGridArray() returns array of 6 x 6 blanks
[
[ ' ', ' ', ' ', ' ', ' ', ' ' ],
[ ' ', ' ', ' ', ' ', ' ', ' ' ],
[ ' ', ' ', ' ', ' ', ' ', ' ' ],
[ ' ', ' ', ' ', ' ', ' ', ' ' ],
[ ' ', ' ', ' ', ' ', ' ', ' ' ],
[ ' ', ' ', ' ', ' ', ' ', ' ' ]
]
[
[ ' ', ' ', ' ', ' ', ' ', ' ' ],
[ ' ', ' ', ' ', ' ', ' ', ' ' ],
[ ' ', ' ', ' ', ' ', ' ', ' ' ],
[ ' ', ' ', ' ', ' ', ' ', ' ' ],
[ ' ', ' ', ' ', ' ', ' ', ' ' ],
[ ' ', ' ', ' ', ' ', ' ', ' ' ]
]
stdout | GameData.test.js > GameData class > fillGridArray() returns array of 6 x 6 letters from A to F
[
[ 'B', 'F', 'C', 'B', 'F', 'F' ],
[ 'C', 'D', 'E', 'E', 'E', 'E' ],
[ 'E', 'C', 'A', 'C', 'E', 'D' ],
[ 'D', 'A', 'A', 'B', 'E', 'B' ],
[ 'B', 'E', 'C', 'A', 'E', 'C' ],
[ 'E', 'F', 'C', 'F', 'D', 'C' ]
]
stdout | GameData.test.js > GameData class > getRow(id) gets row number from 'r1c1'
r10c10 getRow: 10
rc10 getRow: undefined
'' getRow: undefined
stdout | GameData.test.js > GameData class > getCol(id) gets column number from 'r1c1'
r10c10 getCol: 10
r10c getCol: undefined
'' getCol: undefined
stdout | GameData.test.js > GameData class > getRandomStripeCandy(color) should convert A to H or N, B to I or O, C t J or P, D to K or Q, E to L or R, F to M or S randomly
getRandomStripeCandy: error
stdout | GameData.test.js > GameData class > checkFiveInALine() finds 5 in a line and returns gridFive with 1 for each item
[
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'A', 'A', 'A', 'A', 'A', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
] [
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ '1', '1', '1', '1', '1', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
] []
[
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'D', 'E', 'F', 'E', 'B', 'C' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'E', 'A', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
] [
[ 'B', 'C', 'D', '1', 'F', 'A' ],
[ 'D', 'E', 'F', '1', 'B', 'C' ],
[ 'B', 'C', 'D', '1', 'F', 'A' ],
[ 'C', 'D', 'E', '1', 'A', 'B' ],
[ 'B', 'C', 'D', '1', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
] [
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ '1', '1', '1', '1', '1', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
]
[
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'D', 'E', 'F', 'A', 'B', 'C' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
] [
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'D', 'E', 'F', 'A', 'B', 'C' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
] [
[ 'B', 'C', 'D', '1', 'F', 'A' ],
[ 'D', 'E', 'F', '1', 'B', 'C' ],
[ 'B', 'C', 'D', '1', 'F', 'A' ],
[ 'C', 'D', 'E', '1', 'A', 'B' ],
[ 'B', 'C', 'D', '1', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
]
stdout | GameData.test.js > GameData class > checkFourInALine() finds 4 in a line and returns gridFour with a 1 for each item
[
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'A', 'A', 'A', 'A', 'B', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
] [
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ '1', '1', '1', '1', 'B', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
] []
[
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'A', 'A', 'A', 'E', 'B', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'E', 'A', 'B' ],
[ 'B', 'C', 'D', 'F', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
] [
[ 'B', 'C', 'D', '1', 'F', 'A' ],
[ 'A', 'A', 'A', '1', 'B', 'B' ],
[ 'B', 'C', 'D', '1', 'F', 'A' ],
[ 'C', 'D', 'E', '1', 'A', 'B' ],
[ 'B', 'C', 'D', 'F', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
] [
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ '1', '1', '1', '1', 'B', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
]
[
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'D', 'E', 'F', 'A', 'B', 'C' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
] [
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'D', 'E', 'F', 'A', 'B', 'C' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
] [
[ 'B', 'C', 'D', '1', 'F', 'A' ],
[ 'A', 'A', 'A', '1', 'B', 'B' ],
[ 'B', 'C', 'D', '1', 'F', 'A' ],
[ 'C', 'D', 'E', '1', 'A', 'B' ],
[ 'B', 'C', 'D', 'F', 'F', 'A' ],
[ 'C', 'D', 'E', 'F', 'A', 'B' ]
]
stdout | GameData.test.js > GameData class > fillGridArrayBlanks() fills in the spaces with random letters from A to F
[
[ ' ', ' ', ' ', ' ', ' ', ' ' ],
[ 'A', 'B', ' ', 'D', 'E', ' ' ],
[ 'A', 'B', 'C', 'E', 'F', ' ' ],
[ 'B', 'C', 'C', 'D', 'E', 'F' ],
[ 'A', 'B', 'C', 'D', 'E', 'A' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ]
]
[
[ 'E', 'D', 'B', 'A', 'E', 'A' ],
[ 'A', 'B', 'F', 'D', 'E', 'D' ],
[ 'A', 'B', 'C', 'E', 'F', 'B' ],
[ 'B', 'C', 'C', 'D', 'E', 'F' ],
[ 'A', 'B', 'C', 'D', 'E', 'A' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ]
]
stdout | GameData.test.js > GameData class > dropCandy() shifts all characters in the column towards the bottom
[
[ 'A', 'B', 'C', 'D', 'E', 'F' ],
[ ' ', ' ', ' ', 'E', 'F', 'A' ],
[ 'A', 'B', 'C', 'D', 'E', ' ' ],
[ 'B', 'C', ' ', ' ', ' ', ' ' ],
[ 'A', 'B', 'C', 'D', 'E', ' ' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ]
]
[
[ ' ', ' ', ' ', ' ', ' ', ' ' ],
[ 'A', 'B', ' ', 'D', 'E', ' ' ],
[ 'A', 'B', 'C', 'E', 'F', ' ' ],
[ 'B', 'C', 'C', 'D', 'E', 'F' ],
[ 'A', 'B', 'C', 'D', 'E', 'A' ],
[ 'B', 'C', 'D', 'E', 'F', 'A' ]
]
stdout | GameData.test.js > GameData class > scanRows(list) returns list of {start(line),end(line),color,gdStart(user move), gdEnd(user move)}
{
start: { row: 1, col: 0 },
end: { row: 1, col: 3 },
color: 'C',
gdStart: { row: 1, col: 4, color: 'F' },
gdEnd: { row: 1, col: 3, color: 'C' }
} {
start: { row: 1, col: 0 },
end: { row: 1, col: 3 },
color: 'C',
gdStart: { row: 1, col: 4, color: 'F' },
gdEnd: { row: 1, col: 3, color: 'C' }
}
stdout | GameData.test.js > GameData class > scanColumns(list) returns list of {start(line),end(line),color,gdStart(user move), gdEnd(user move)}
{
start: { row: 1, col: 3 },
end: { row: 4, col: 3 },
color: 'B',
gdStart: { row: 1, col: 4, color: 'F' },
gdEnd: { row: 1, col: 3, color: 'B' }
} {
start: { row: 1, col: 3 },
end: { row: 4, col: 3 },
color: 'B',
gdStart: { row: 1, col: 4, color: 'F' },
gdEnd: { row: 1, col: 3, color: 'B' }
}
✓ utils.test.js (4)
✓ math functions (4)
✓ max() gives the maximum of 2 numbers
✓ min() gives the minimum of 2 numbers
✓ findLength(from,to) gives the length between 2 points
✓ findAngle(from,to) gives the angle with horizontal
✓ GameData.test.js (23)
✓ GameData class (23)
✓ initGridArray() returns array of 6 x 6 blanks
✓ fillGridArray() returns array of 6 x 6 letters from A to F
✓ setStartId returns {id, row, col, color}
✓ setEndId returns {id, row, col, color}
✓ getRow(id) gets row number from 'r1c1'
✓ getCol(id) gets column number from 'r1c1'
✓ getRandomCandy(): returns a letter from 'A' to'F'
✓ getDistance(start,end): returns distance between 2 points by Pythagoras Theorem
✓ checkValidMoveAdjacent(): return true if distance is 1, return false otherwise
✓ stripedToNormal(str) should convert H and N to A, I and O to B, J and P to C, K and Q to D, L and R to E, M and S to F
✓ getRandomStripeCandy(color) should convert A to H or N, B to I or O, C t J or P, D to K or Q, E to L or R, F to M or S randomly
✓ isPointOnLine(point,start,end) returns true if the point is on the line, otherwise returns false
✓ markLineFive(str) replace a string, all letters between A and F with a 1
✓ checkFiveInALine() finds 5 in a line and returns gridFive with 1 for each item
✓ markLineFour(str) replaces a string, all letters between A and F with a 1
✓ checkFourInALine() finds 4 in a line and returns gridFour with a 1 for each item
✓ fillGridArrayBlanks() fills in the spaces with random letters from A to F
✓ dropCandy() shifts all characters in the column towards the bottom
✓ scanRows(list) returns list of {start(line),end(line),color,gdStart(user move), gdEnd(user move)}
✓ scanColumns(list) returns list of {start(line),end(line),color,gdStart(user move), gdEnd(user move)}
✓ assignCandy({list, gridFour, start, end}) returns gridFour with 'H to M'(horizontal) or 'N to S'(vertical)
✓ giveStripedCandy() scans gridFour for 1 and assigns striped candy returns gridFour with 'H to M'(horizontal) or 'N to S'(vertical)
✓ checkNormalWithStripedMove(start,end) returns true if start.color === end.color
Test Files 2 passed (2)
Tests 27 passed (27)
Start at 09:37:20
Duration 1.24s (transform 223ms, setup 0ms, collect 251ms, tests 71ms)