Skip to content

Technische documentatie

In deze documentatie wordt kort uitgelegd waarom een stuk code is toegevoegd en wat de code doet.

UML Class Diagram

Het huidige class diagram is als volgt:

Class_Diagram

Maken van de tilemap

// in TileGrid class
    tileMap = [
        [1, 2, 2, 1, 2, 2, 1, 1],
        [1, 2, 0, 1, 2, 2, 1, 1],
        [1, 1, 1, 1, 1, 2, 1, 1],
        [1, 2, 2, 1, 2, 0, 1, 1],
        [1, 2, 1, 1, 1, 2, 1, 1],
        [1, 1, 1, 1, 2, 2, 1, 1],
        [1, 2, 0, 2, 1, 2, 1, 1],
        [1, 1, 2, 1, 2, 0, 1, 1],
        [1, 1, 2, 1, 1, 2, 1, 1],
        [1, 2, 1, 1, 2, 2, 1, 1],
        [2, 1, 2, 1, 1, 2, 1, 2],
        [1, 2, 2, 1, 1, 2, 2, 1],
        ]

    generateTileGrid() {

        //tiles is a 2D array, meaning that it is an array of arrays.
        //see https://www.freecodecamp.org/news/javascript-2d-arrays/ for more information about 2D arrays.
        this.tiles = new Array();

        //generate tile grid here and place tiles in the 2D #tile array.
        if (!this.tileArrayMade) {
            for (let x = 0; x < this.#width; x++) {
                for (let y = 0; y < this.#height; y++) {

                    if (!this.tiles[x]) {
                        this.tiles[x] = new Array();
                    }

                    if (this.tileMap[x][y] == 0) {

                        this.tiles[x][y] = new NonMovingTile(this.tileSize, x, y, this.tileNumber)

                    } else if (this.tileMap[x][y] == 1) {
                        // Add 3 randomized normalTile's

                        this.tileNumber = Math.floor(Math.random() * 3) + 1
                        this.tiles[x][y] = new NormalTile(this.tileSize, x, y, this.tileNumber);

                    } else if (this.tileMap[x][y] == 2) {
                        // Add 3 randomized specialTile's

                        this.tileNumber = Math.floor(Math.random() * 3) + 4
                        this.tiles[x][y] = new SpecialTile(this.tileSize, x, y, this.tileNumber);

                    }
                }
            }
            this.tileArrayMade = true;
            this.checkForMatches();
            score = 0; // Checks for matches before game starts to create a normal starting tileGrid and resets score
        }
    }

Hier wordt de tilemap en dus het speelveld gemaakt, waarin 0 staat voor een “non movable tile”, de nummers 1 worden gerandomized naar 1, 2 of 3, terwijl de nummers 2 gerandomized worden naar 4, 5 en 6. De nummers staan daarna gelijk aan een symbool volgens onderstaande code. Er wordt een object gemaakt met als eigenschappen de grootte, de x en y coordinaten en de tileNumber die het symbool aangeeft. Deze wordt met behulp van een 2d array in de tilegrid gedaan.

    if (tileNumber == 6) {
        image = gameManager.getImage("Poisonous Golden Dagger");
    } else if (tileNumber == 5) {
        image = gameManager.getImage("Golden Dagger");
    } else if (tileNumber == 4) {
        image = gameManager.getImage("Dagger");
    } else if (tileNumber == 3) {
        image = gameManager.getImage("Bow");
    } else if (tileNumber == 2) {
        image = gameManager.getImage("Shield");
    } else if (tileNumber == 1) {
        image = gameManager.getImage("diamond");
    }

Tileswap mechanic

    /**
     * Using targetX and targetY, the x and y coordinates of the position that the player has pressed,
     * we can then check this position in the array and use the tile we get from this in the moveObject method
     * to start moving the tile.
     */
    checkClickedOnTileGrid(targetX, targetY) {
        this.targetX = targetX;
        this.targetY = targetY;

        for (let x = 0; x < this.#width; x++) {
            for (let y = 0; y < this.#height; y++) {
                let tile = this.tiles[x][y]
                if (this.firstTile && tile.x == this.targetX && tile.y == this.targetY) {
                        this.firstObjectX = tile.x;
                        this.firstObjectY = tile.y;
                        this.firstTile = false;
                        break;
                }
                if (!this.firstTile && tile.x == this.targetX && tile.y == this.targetY) {
                        this.secondObjectX = tile.x;
                        this.secondObjectY = tile.y;
                        this.firstTile = true;

                        this.moveObject();
                }
            }
        }
    }

    /**
     * Checks if the tilenumber is 0, which is the nonMovableTile. The method then checks if the pieces are adjacent and if, so it switches the tiles.
     */
    moveObject() {

        const firstTileNumber = this.tiles[this.firstObjectX][this.firstObjectY].getTileNumber();
        const secondTileNumber = this.tiles[this.secondObjectX][this.secondObjectY].getTileNumber();

        if (firstTileNumber !== 0 && secondTileNumber !== 0) {
            const distanceXCoord = Math.abs(this.tiles[this.firstObjectX][this.firstObjectY].x - this.tiles[this.secondObjectX][this.secondObjectY].x);
            const distanceYCoord = Math.abs(this.tiles[this.firstObjectX][this.firstObjectY].y - this.tiles[this.secondObjectX][this.secondObjectY].y);

            // The code below checks for every part of the array that's 1 block from the first block selected.
            if ((distanceXCoord === 1 && distanceYCoord === 0) || (distanceXCoord === 0 && distanceYCoord === 1)) {

                // Swap tile positions in the tiles array.
                const tempTile = this.tiles[this.firstObjectX][this.firstObjectY];
                this.tiles[this.firstObjectX][this.firstObjectY] = this.tiles[this.secondObjectX][this.secondObjectY];
                this.tiles[this.secondObjectX][this.secondObjectY] = tempTile;

                // Used to update the tile positions
                this.tiles[this.firstObjectX][this.firstObjectY].x = this.firstObjectX;
                this.tiles[this.firstObjectX][this.firstObjectY].y = this.firstObjectY;
                this.tiles[this.secondObjectX][this.secondObjectY].x = this.secondObjectX;
                this.tiles[this.secondObjectX][this.secondObjectY].y = this.secondObjectY;
            }
            this.timer = this.timerDefault
            this.turnUsed = true;
        }
    }

De checkClickedOnTileGrid functie zorgt ervoor dat een tile geselect kan worden, als vervolgens een 2e tile wordt aangevinkt die direct naast de selected tile is worden ze geswapt met de moveObject functie. De 1e tile wordt opgeslagen in een tijdelijke variabele waarna de 1e tile vervangen wordt met de 2e tile, de 2e tile wordt hierna vervangen met de tijdelijke variabele waarna ze verwisselt zijn. Zie ook de p5js wiki voor uitleg over bijvoorbeeld tijdelijke waardes en en memory management.

Swap delay

    countdownTimer() {
        this.timer -= 1
        if (this.timer <= 0) {
            this.checkForMatches()
            this.timer = this.timerDefault
            this.turnUsed = false;
        }
    }

    countdownTimerAfterCheck() {
        this.timer2 -= 0.7
        if (!this.secondcheck && this.timer2 <= 0) {
            this.checkForMatches()
            this.timer2 = this.timerDefault
            this.secondcheck = true;
        }
        if (this.secondcheck && this.timer2 <= 0) {
            this.checkForMatches()
            this.timer2 = this.timerDefault
            this.turnEnded = false;
            this.secondcheck = false;
        }
    }

    countdownTimer() {
        this.timer -= 1
        if (this.timer <= 0) {
            this.checkForMatches()
            this.timer = this.timerDefault
            this.turnUsed = false;
        }
    }

De code hierboven zorgt ervoor dat er na het swappen een kleine rust is voordat er voor matches wordt gecheckt. Op deze manier krijgt de speler het duidelijk te zien en weet de speler het ook als het maken van de match een domino effect veroorzaakt van meer matches. Dit gebeurt door een timer die even wacht en daarna pas de checkForMatches functie aanroept. We hebben de code op de onderstaande website bekeken om de timer functie goed werkend te krijgen. Link

Vinden van matches

    checkForMatches() {
        this.checkHorizontalMatches();
        this.checkVerticalMatches();
    }

    checkHorizontalMatches() {
        for (let x = 0; x < this.tiles.length-2; x++) {
            for (let y = 0; y < this.tiles[0].length; y++) {
                if(this.tiles[x][y].tileNumber == this.tiles[x+1][y].tileNumber && this.tiles[x][y].tileNumber == this.tiles[x+2][y].tileNumber) {
                    var matchLength = 3;
                if(x != this.tiles.length - 3) {
                    if(this.tiles[x][y].tileNumber == this.tiles[x+3][y].tileNumber) {
                        var matchLength = 4;
                    }
                if(x != this.tiles.length - 4) {
                    if(this.tiles[x][y].tileNumber == this.tiles[x+4][y].tileNumber) {
                        var matchLength = 5;
                    }
                }
                }
                    this.removeHorizontalLane(this.tiles[x][y].x, this.tiles[x][y].y, matchLength);

                    score += 10;

                    this.timer2 = this.timerDefault
                    this.turnEnded = true;
                }
            }
        }
    }

    checkVerticalMatches() {
        for (let x = 0; x < this.tiles.length; x++) {
            for (let y = 0; y < this.#height - 2; y++) {
                if(this.tiles[x][y].tileNumber == this.tiles[x][y+1].tileNumber && this.tiles[x][y].tileNumber == this.tiles[x][y+2].tileNumber) {
                    var matchLength = 3;
                if(y != this.#height - 3) {
                    if(this.tiles[x][y].tileNumber == this.tiles[x][y+3].tileNumber) {
                        var matchLength = 4;
                    }
                    if(y != this.#height && y != this.#height - 4) {
                        if(this.tiles[x][y].tileNumber == this.tiles[x][y+4].tileNumber) {
                            var matchLength = 5;
                        }
                    }
                }
                for(let n = 0; n < matchLength; n++) {
                    this.removeVerticalLane(this.tiles[x][y].x, this.tiles[x][y].y + matchLength - 1, matchLength);
                }
                    score += 10;
                    this.timer2 = this.timerDefault
                    this.turnEnded = true;
                }
            }
        }
    }
De bovenstaande code regelt het vinden van matches, voor horizontaal en verticaal zijn verschillende functies gemaakt. De checkHorizontalMatches checkt voor elke rij of de x coordinaat, de x coordinaat + 1 en de x coordinaat + 2 hetzelfde tileNumber hebben. Zoja dan wordt de removeHorizontalLane functie aangeroepen met als parameters de x en y coordinaten en de matchLength. Ook wordt er gekeken of match 4 en match 5 mogelijk is door te kijken of x + 3 en x + 4 hetzelfde tileNumber hebben als de rest van de match. De checkVerticalMatches werkt hetzelfde, maar dan verticaal.

Weghalen van rijen

    removeHorizontalLane(x, y, i) {
        for(let m = x; m < x + i; m++) {
            for(let n = y; n > 0; n--) {
                if(this.tiles[m][n-1].tileNumber != 0 && this.tiles[m][n].tileNumber != 0) {
                    this.tiles[m][n] = new NormalTile(this.tileSize, m, n, this.tiles[m][n-1].tileNumber);
                }
                else if(this.tiles[m][n-2].tileNumber != 0 && this.tiles[m][n].tileNumber != 0) {
                    this.tiles[m][n] = new NormalTile(this.tileSize, m, n, this.tiles[m][n-2].tileNumber);
                }
            }
            for(let n = 0; n < this.#width; n++) {
                this.tiles[m][0] = new NormalTile(this.tileSize, m, 0, Math.floor(random(1, 6)));
            }
        }
    }

    removeVerticalLane(x, y, i) {
        for(let n = y; n > 0; n--) {
            if(this.tiles[x][n].tileNumber != 0 && this.tiles[x][n-1].tileNumber != 0) {
                this.tiles[x][n] = new NormalTile(this.tileSize, x, n, this.tiles[x][n-1].tileNumber);
            }
            else if(this.tiles[x][n].tileNumber != 0 && this.tiles[x][n-2].tileNumber != 0) {
                this.tiles[x][n] = new NormalTile(this.tileSize, x, n, this.tiles[x][n-2].tileNumber);
            }
        }
            this.tiles[x][0] = new NormalTile(this.tileSize, x, 0, Math.floor(random(1, 6)));
    }
De bovenstaande functies nemen als parameters de x en y coordinaten en de matchLength. Op deze manier wordt de correcte tile gekozen en kunnen er meer tiles verwijderd worden als er sprake is van match 4 of 5. We hebben de volgende website gebruikt om meer te leren over het aanpassen van 2d arrays: codecademy.

Swipe controls

function handleSwipeAction() {
    let differenceX = endPosX - startPosX;
    let differenceY = endPosY - startPosY;

    // Max distance swiped in between 2 connected tiles (Left end to right end) still checks for the closest tile connection.
    if (differenceX < 160 && differenceX > 80) {
        differenceX = 80;
    } else if (differenceY < 160 && differenceY > 80) {
        differenceY = 80;
    } else if (differenceX > -160 && differenceX < -80) {
        differenceX = -80;
    } else if (differenceY > -160 && differenceY < -80) {
        differenceY = -80;
    }

    // if the differences aren't 0 (they can be negative as well), and the differences are below 81 (so 80 and lower)
    if ((!differenceX == 0 || !differenceY == 0) &&
        (differenceX < 81 && differenceY < 81) && (differenceX > -81 && differenceY > -81)) {

        const firstTile = tileGrid.getTileAtPosition(createVector(startPosX, startPosY));
        const secondTile = tileGrid.getTileAtPosition(createVector(endPosX, endPosY));

        tileGrid.checkClickedOnTileGrid(firstTile.x, firstTile.y);
        tileGrid.checkClickedOnTileGrid(secondTile.x, secondTile.y);
    }
}

document.addEventListener('mousedown', e => {
    isClick = true; // Sets isClick to true to change later if the mouse moves too much after holding down the mouse button.
    startPosX = e.clientX;
    startPosY = e.clientY;
});

// If the mouse has moved more then the specified number below (5), the mousedown counts as a click.
document.addEventListener('mousemove', e => {
    if (Math.abs(e.clientX - startPosX) > 5 || Math.abs(e.clientY - startPosY) > 5) {
        isClick = false;
    }
});

document.addEventListener('mouseup', e => {
    endPosX = e.clientX;
    endPosY = e.clientY;
    if (isClick) { // if the click is a normal click and not a swipe, it uses the normal way to move tiles.
        const tile = tileGrid.getTileAtPosition(createVector(e.clientX, e.clientY));
        tileGrid.checkClickedOnTileGrid(tile.x, tile.y);
    } else { // If it's a swipe it uses the function below.
        handleSwipeAction();
    }

    isClick = false;
});

document.addEventListener('touchstart', e => {
    isClick = true; // Sets isClick to true to change later if the player touch moves too much after holding down on the screen.
    startPosX = e.touches[0].clientX;
    startPosY = e.touches[0].clientY;

});

document.addEventListener('touchmove', e => {
    // If the touch moves significantly, consider it as a swipe
    if (Math.abs(e.touches[0].clientX - startPosX) > 5 || Math.abs(e.touches[0].clientY - startPosY) > 5) {
        isClick = false;
        e.preventDefault();
    }

}, {passive: false} );

document.addEventListener('touchend', e => {

    endPosX = e.changedTouches[0].clientX;
    endPosY = e.changedTouches[0].clientY;

    if (isClick) { // if the touch isn't a swipe but just a touch, it uses a different function to move the tiles.
        const tile = tileGrid.getTileAtPosition(createVector(e.changedTouches[0].clientX, e.changedTouches[0].clientY));
        tileGrid.checkClickedOnTileGrid(tile.x, tile.y);
        e.preventDefault();
    } else { // if it's a swipe it uses the function below.
        handleSwipeAction();
    }

    isClick = false; // reset the isClick variable
});
De bovenstaande code gebruikt event listeners om swappen door swipen mogelijk te maken. De event listener “luistert” of er geklikt wordt op het scherm, slaat de x en y coordinaat op en registreert de beweging. Als de 2e opgeslagen x en y coordinaat naast de 1e x en y coordinaat zit worden ze omgewisselt door de handleSwipe functie. We hebben ook p5js gebruikt om meer te leren over deze event listeners.

Toggleable fps counter en start game button

function displayPreGameScreen() {
    displayStartButton();
    displayFPSButton();
}

function displayStartButton() {
    startButton = createButton("Start game");
    startButton.position(windowWidth / 2 - startButtonWidth / 2, 400);
    startButton.size(startButtonWidth, startButtonHeight);
    startButton.mousePressed(startGame);
    startButton.style("background-color", "black");
    startButton.style("color", "white");
}

function displayFPSButton() {
    fpsButton = createButton("Display FPS");
    fpsButton.position(windowWidth / 2 - startButtonWidth / 2, 600);
    fpsButton.size(startButtonWidth, startButtonHeight);
    fpsButton.mousePressed(toggleFPS);
    fpsButton.style("background-color", "black");
    fpsButton.style("color", "white");
}

function toggleFPS() {
    if(showFPS) {
        showFPS = false;
        document.getElementById("fps").innerHTML = "";
    }

    else {
        showFPS = true;
    }
}

function calculateFPS() {
     delta = (Date.now() - lastCalledTime) / 1000;
     lastCalledTime = Date.now();
     fps = 1 / delta;
     fps = floor(fps);
}

function displayFPS() {
    if(frameCount % 15 == 0) { // updates the fps 4 times a second
        document.getElementById("fps").innerHTML = fps;
    }
}

function startGame() {
    mode = 1;
}
In de bovenstaande code worden html en css elementen binnen javascript gebruikt om buttons te maken, de positie wordt bepaald in de css code.
De fps wordt uitgerekent met behulp van tijdverschillen om het gemiddelde uit te rekenen, dit wordt 4 keer per seconde geupdate om het leesbaar te maken. (Anders verandert de FPS te snel om te lezen)
Als de startButton wordt aangeklikt, wordt de startGame functie gecalled en wordt de mode veranderd naar 1. Mode 0 is het beginscherm met de start button en fps button, mode 1 bevat ook het speelveld. We hebben hiervoor de volgende bronnen gebruikt:
Github Growing With The Web p5js references

Last update: January 8, 2024